@opengsd/gsd-path 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (476) hide show
  1. package/CHANGELOG.md +460 -341
  2. package/DOCS.md +16 -5
  3. package/FULL.md +67 -6
  4. package/HOOKS.md +158 -10
  5. package/MIGRATE.md +12 -0
  6. package/QUICK.md +4 -2
  7. package/README.md +136 -116
  8. package/RUNTIME.md +24 -6
  9. package/UPDATE.md +39 -3
  10. package/WORKFLOW.md +80 -4
  11. package/package.json +5 -2
  12. package/platforms/shared-agents/dispatch.md +13 -0
  13. package/scripts/_common.py +256 -2
  14. package/scripts/archive_milestone.py +14 -13
  15. package/scripts/build_recovery.py +155 -58
  16. package/scripts/build_state.py +274 -8
  17. package/scripts/check_docs_audit.py +77 -4
  18. package/scripts/check_handoffs.py +635 -57
  19. package/scripts/check_task_briefs.py +293 -32
  20. package/scripts/detect_project.py +260 -20
  21. package/scripts/discussion_records.py +9 -1
  22. package/scripts/discussion_validate.py +93 -35
  23. package/scripts/dispatch_driver.py +549 -53
  24. package/scripts/git_guard.py +27 -8
  25. package/scripts/guard_hook.py +2630 -210
  26. package/scripts/install.mjs +99 -11
  27. package/scripts/install.py +231 -46
  28. package/scripts/integration.py +244 -65
  29. package/scripts/isolation.py +539 -70
  30. package/scripts/loop_run.py +16 -4
  31. package/scripts/members.py +77 -32
  32. package/scripts/migrate_core.py +2 -0
  33. package/scripts/model_policy.py +5 -1
  34. package/scripts/path_config.py +3 -3
  35. package/scripts/pipeline_diagnose.py +17 -2
  36. package/scripts/pipeline_git.py +131 -42
  37. package/scripts/pipeline_state.py +139 -39
  38. package/scripts/pipeline_undo.py +118 -16
  39. package/scripts/promote_lookahead.py +3 -3
  40. package/scripts/review_findings.py +180 -44
  41. package/scripts/roadmap.py +9 -1
  42. package/scripts/runtime_store.py +21 -2
  43. package/scripts/skill-resources.json +9 -2
  44. package/scripts/state_checkpoint.py +39 -36
  45. package/scripts/state_promote.py +15 -4
  46. package/scripts/status_runtime.py +22 -4
  47. package/scripts/task_context.py +14 -3
  48. package/scripts/wizard.mjs +1 -0
  49. package/scripts/workflow_run.py +14 -4
  50. package/scripts/worktree_paths.py +54 -2
  51. package/skills/gsd-path/DEFINE.md +34 -5
  52. package/skills/gsd-path/PLAN.md +14 -3
  53. package/skills/gsd-path/RESEARCH.md +11 -0
  54. package/skills/gsd-path/ROADMAP.md +16 -6
  55. package/skills/gsd-path/SHIP.md +32 -5
  56. package/skills/gsd-path/SKILL.md +52 -24
  57. package/skills/gsd-path/references/build-native.md +112 -10
  58. package/skills/gsd-path/references/build-recovery.md +13 -0
  59. package/skills/gsd-path/references/coder.md +7 -3
  60. package/skills/gsd-path/references/dispatch.md +13 -0
  61. package/skills/gsd-path/references/docs-auditor.md +5 -0
  62. package/skills/gsd-path/references/operating-rules.md +11 -3
  63. package/skills/gsd-path/references/planner.md +26 -4
  64. package/skills/gsd-path/references/reviewer-final.md +20 -2
  65. package/skills/gsd-path/references/reviewer-wave.md +56 -12
  66. package/skills/gsd-path/references/reviewer.md +3 -1
  67. package/skills/gsd-path/references/runtime-selection.md +9 -0
  68. package/skills/gsd-path/references/spec-probes.md +150 -0
  69. package/skills/gsd-path/scripts/_common.py +256 -2
  70. package/skills/gsd-path/scripts/archive_milestone.py +14 -13
  71. package/skills/gsd-path/scripts/build_recovery.py +155 -58
  72. package/skills/gsd-path/scripts/build_state.py +274 -8
  73. package/skills/gsd-path/scripts/check_docs_audit.py +77 -4
  74. package/skills/gsd-path/scripts/check_handoffs.py +635 -57
  75. package/skills/gsd-path/scripts/check_task_briefs.py +293 -32
  76. package/skills/gsd-path/scripts/detect_project.py +260 -20
  77. package/skills/gsd-path/scripts/discussion_records.py +9 -1
  78. package/skills/gsd-path/scripts/discussion_validate.py +93 -35
  79. package/skills/gsd-path/scripts/dispatch_driver.py +549 -53
  80. package/skills/gsd-path/scripts/git_guard.py +27 -8
  81. package/skills/gsd-path/scripts/integration.py +244 -65
  82. package/skills/gsd-path/scripts/isolation.py +539 -70
  83. package/skills/gsd-path/scripts/loop_run.py +16 -4
  84. package/skills/gsd-path/scripts/members.py +77 -32
  85. package/skills/gsd-path/scripts/model_policy.py +5 -1
  86. package/skills/gsd-path/scripts/path_config.py +3 -3
  87. package/skills/gsd-path/scripts/pipeline_git.py +131 -42
  88. package/skills/gsd-path/scripts/pipeline_state.py +139 -39
  89. package/skills/gsd-path/scripts/promote_lookahead.py +3 -3
  90. package/skills/gsd-path/scripts/review_findings.py +180 -44
  91. package/skills/gsd-path/scripts/roadmap.py +9 -1
  92. package/skills/gsd-path/scripts/state_checkpoint.py +39 -36
  93. package/skills/gsd-path/scripts/state_promote.py +15 -4
  94. package/skills/gsd-path/scripts/status_runtime.py +22 -4
  95. package/skills/gsd-path/scripts/task_context.py +14 -3
  96. package/skills/gsd-path/scripts/workflow_run.py +14 -4
  97. package/skills/gsd-path/scripts/worktree_paths.py +54 -2
  98. package/skills/gsd-path/templates/docs-audit.md +5 -1
  99. package/skills/gsd-path/templates/final-review.md +11 -2
  100. package/skills/gsd-path/templates/intent.md +30 -0
  101. package/skills/gsd-path/templates/patch-findings.md +3 -3
  102. package/skills/gsd-path/templates/plan.md +17 -2
  103. package/skills/gsd-path/templates/research-handoff.md +14 -3
  104. package/skills/gsd-path/templates/state.md +2 -1
  105. package/skills/gsd-path/templates/task.md +1 -0
  106. package/skills/gsd-path/templates/wave-review.md +28 -9
  107. package/skills/gsd-path-build/references/build-native.md +112 -10
  108. package/skills/gsd-path-build/references/build-recovery.md +13 -0
  109. package/skills/gsd-path-build/references/coder.md +7 -3
  110. package/skills/gsd-path-build/references/dispatch.md +13 -0
  111. package/skills/gsd-path-build/references/operating-rules.md +11 -3
  112. package/skills/gsd-path-build/references/reviewer-final.md +20 -2
  113. package/skills/gsd-path-build/references/reviewer-wave.md +56 -12
  114. package/skills/gsd-path-build/references/reviewer.md +3 -1
  115. package/skills/gsd-path-build/references/runtime-selection.md +9 -0
  116. package/skills/gsd-path-build/references/spec-probes.md +150 -0
  117. package/skills/gsd-path-build/scripts/_common.py +256 -2
  118. package/skills/gsd-path-build/scripts/archive_milestone.py +14 -13
  119. package/skills/gsd-path-build/scripts/build_recovery.py +155 -58
  120. package/skills/gsd-path-build/scripts/build_state.py +274 -8
  121. package/skills/gsd-path-build/scripts/check_handoffs.py +635 -57
  122. package/skills/gsd-path-build/scripts/check_task_briefs.py +293 -32
  123. package/skills/gsd-path-build/scripts/detect_project.py +260 -20
  124. package/skills/gsd-path-build/scripts/discussion_records.py +9 -1
  125. package/skills/gsd-path-build/scripts/discussion_validate.py +93 -35
  126. package/skills/gsd-path-build/scripts/dispatch_driver.py +549 -53
  127. package/skills/gsd-path-build/scripts/integration.py +244 -65
  128. package/skills/gsd-path-build/scripts/isolation.py +539 -70
  129. package/skills/gsd-path-build/scripts/loop_run.py +16 -4
  130. package/skills/gsd-path-build/scripts/members.py +77 -32
  131. package/skills/gsd-path-build/scripts/model_policy.py +5 -1
  132. package/skills/gsd-path-build/scripts/path_config.py +3 -3
  133. package/skills/gsd-path-build/scripts/pipeline_git.py +131 -42
  134. package/skills/gsd-path-build/scripts/pipeline_state.py +139 -39
  135. package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -3
  136. package/skills/gsd-path-build/scripts/review_findings.py +180 -44
  137. package/skills/gsd-path-build/scripts/roadmap.py +9 -1
  138. package/skills/gsd-path-build/scripts/state_checkpoint.py +39 -36
  139. package/skills/gsd-path-build/scripts/state_promote.py +15 -4
  140. package/skills/gsd-path-build/scripts/task_context.py +14 -3
  141. package/skills/gsd-path-build/scripts/workflow_run.py +14 -4
  142. package/skills/gsd-path-build/scripts/worktree_paths.py +54 -2
  143. package/skills/gsd-path-build/templates/task.md +1 -0
  144. package/skills/gsd-path-build/templates/wave-review.md +28 -9
  145. package/skills/gsd-path-decide/references/dispatch.md +13 -0
  146. package/skills/gsd-path-decide/references/operating-rules.md +11 -3
  147. package/skills/gsd-path-decide/references/runtime-selection.md +9 -0
  148. package/skills/gsd-path-decide/scripts/_common.py +256 -2
  149. package/skills/gsd-path-decide/scripts/archive_milestone.py +14 -13
  150. package/skills/gsd-path-decide/scripts/build_recovery.py +155 -58
  151. package/skills/gsd-path-decide/scripts/check_handoffs.py +635 -57
  152. package/skills/gsd-path-decide/scripts/check_task_briefs.py +293 -32
  153. package/skills/gsd-path-decide/scripts/detect_project.py +260 -20
  154. package/skills/gsd-path-decide/scripts/discussion_records.py +9 -1
  155. package/skills/gsd-path-decide/scripts/discussion_validate.py +93 -35
  156. package/skills/gsd-path-decide/scripts/integration.py +244 -65
  157. package/skills/gsd-path-decide/scripts/isolation.py +539 -70
  158. package/skills/gsd-path-decide/scripts/loop_run.py +16 -4
  159. package/skills/gsd-path-decide/scripts/members.py +77 -32
  160. package/skills/gsd-path-decide/scripts/model_policy.py +5 -1
  161. package/skills/gsd-path-decide/scripts/path_config.py +3 -3
  162. package/skills/gsd-path-decide/scripts/pipeline_git.py +131 -42
  163. package/skills/gsd-path-decide/scripts/pipeline_state.py +139 -39
  164. package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -3
  165. package/skills/gsd-path-decide/scripts/roadmap.py +9 -1
  166. package/skills/gsd-path-decide/scripts/state_checkpoint.py +39 -36
  167. package/skills/gsd-path-decide/scripts/state_promote.py +15 -4
  168. package/skills/gsd-path-decide/scripts/worktree_paths.py +54 -2
  169. package/skills/gsd-path-define/SKILL.md +34 -5
  170. package/skills/gsd-path-define/references/build-recovery.md +13 -0
  171. package/skills/gsd-path-define/references/operating-rules.md +11 -3
  172. package/skills/gsd-path-define/references/runtime-selection.md +9 -0
  173. package/skills/gsd-path-define/references/spec-probes.md +150 -0
  174. package/skills/gsd-path-define/scripts/_common.py +256 -2
  175. package/skills/gsd-path-define/scripts/archive_milestone.py +14 -13
  176. package/skills/gsd-path-define/scripts/build_recovery.py +155 -58
  177. package/skills/gsd-path-define/scripts/check_handoffs.py +635 -57
  178. package/skills/gsd-path-define/scripts/check_task_briefs.py +293 -32
  179. package/skills/gsd-path-define/scripts/detect_project.py +260 -20
  180. package/skills/gsd-path-define/scripts/discussion_records.py +9 -1
  181. package/skills/gsd-path-define/scripts/discussion_validate.py +93 -35
  182. package/skills/gsd-path-define/scripts/integration.py +244 -65
  183. package/skills/gsd-path-define/scripts/isolation.py +539 -70
  184. package/skills/gsd-path-define/scripts/loop_run.py +16 -4
  185. package/skills/gsd-path-define/scripts/members.py +77 -32
  186. package/skills/gsd-path-define/scripts/model_policy.py +5 -1
  187. package/skills/gsd-path-define/scripts/path_config.py +3 -3
  188. package/skills/gsd-path-define/scripts/pipeline_git.py +131 -42
  189. package/skills/gsd-path-define/scripts/pipeline_state.py +139 -39
  190. package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -3
  191. package/skills/gsd-path-define/scripts/roadmap.py +9 -1
  192. package/skills/gsd-path-define/scripts/state_checkpoint.py +39 -36
  193. package/skills/gsd-path-define/scripts/state_promote.py +15 -4
  194. package/skills/gsd-path-define/scripts/worktree_paths.py +54 -2
  195. package/skills/gsd-path-define/templates/intent.md +30 -0
  196. package/skills/gsd-path-define/templates/state.md +2 -1
  197. package/skills/gsd-path-discuss/references/operating-rules.md +11 -3
  198. package/skills/gsd-path-discuss/references/runtime-selection.md +9 -0
  199. package/skills/gsd-path-discuss/scripts/_common.py +256 -2
  200. package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
  201. package/skills/gsd-path-discuss/scripts/loop_run.py +16 -4
  202. package/skills/gsd-path-discuss/scripts/model_policy.py +5 -1
  203. package/skills/gsd-path-discuss/scripts/path_config.py +3 -3
  204. package/skills/gsd-path-discuss/scripts/worktree_paths.py +54 -2
  205. package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
  206. package/skills/gsd-path-docs-audit/references/docs-auditor.md +5 -0
  207. package/skills/gsd-path-docs-audit/references/operating-rules.md +11 -3
  208. package/skills/gsd-path-docs-audit/references/runtime-selection.md +9 -0
  209. package/skills/gsd-path-docs-audit/scripts/_common.py +256 -2
  210. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +14 -13
  211. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +155 -58
  212. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +77 -4
  213. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +635 -57
  214. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +293 -32
  215. package/skills/gsd-path-docs-audit/scripts/detect_project.py +260 -20
  216. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +9 -1
  217. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +93 -35
  218. package/skills/gsd-path-docs-audit/scripts/integration.py +244 -65
  219. package/skills/gsd-path-docs-audit/scripts/isolation.py +539 -70
  220. package/skills/gsd-path-docs-audit/scripts/loop_run.py +16 -4
  221. package/skills/gsd-path-docs-audit/scripts/members.py +77 -32
  222. package/skills/gsd-path-docs-audit/scripts/model_policy.py +5 -1
  223. package/skills/gsd-path-docs-audit/scripts/path_config.py +3 -3
  224. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +131 -42
  225. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +139 -39
  226. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -3
  227. package/skills/gsd-path-docs-audit/scripts/roadmap.py +9 -1
  228. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +39 -36
  229. package/skills/gsd-path-docs-audit/scripts/state_promote.py +15 -4
  230. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +54 -2
  231. package/skills/gsd-path-docs-audit/templates/docs-audit.md +5 -1
  232. package/skills/gsd-path-forensics/references/operating-rules.md +11 -3
  233. package/skills/gsd-path-forensics/references/runtime-selection.md +9 -0
  234. package/skills/gsd-path-forensics/scripts/_common.py +256 -2
  235. package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
  236. package/skills/gsd-path-forensics/scripts/loop_run.py +16 -4
  237. package/skills/gsd-path-forensics/scripts/model_policy.py +5 -1
  238. package/skills/gsd-path-forensics/scripts/path_config.py +3 -3
  239. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +17 -2
  240. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +118 -16
  241. package/skills/gsd-path-forensics/scripts/worktree_paths.py +54 -2
  242. package/skills/gsd-path-inspect/references/dispatch.md +13 -0
  243. package/skills/gsd-path-inspect/references/docs-auditor.md +5 -0
  244. package/skills/gsd-path-inspect/references/operating-rules.md +11 -3
  245. package/skills/gsd-path-inspect/references/runtime-selection.md +9 -0
  246. package/skills/gsd-path-inspect/scripts/_common.py +256 -2
  247. package/skills/gsd-path-inspect/scripts/archive_milestone.py +14 -13
  248. package/skills/gsd-path-inspect/scripts/build_recovery.py +155 -58
  249. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +77 -4
  250. package/skills/gsd-path-inspect/scripts/check_handoffs.py +635 -57
  251. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +293 -32
  252. package/skills/gsd-path-inspect/scripts/detect_project.py +260 -20
  253. package/skills/gsd-path-inspect/scripts/discussion_records.py +9 -1
  254. package/skills/gsd-path-inspect/scripts/discussion_validate.py +93 -35
  255. package/skills/gsd-path-inspect/scripts/integration.py +244 -65
  256. package/skills/gsd-path-inspect/scripts/isolation.py +539 -70
  257. package/skills/gsd-path-inspect/scripts/loop_run.py +16 -4
  258. package/skills/gsd-path-inspect/scripts/members.py +77 -32
  259. package/skills/gsd-path-inspect/scripts/model_policy.py +5 -1
  260. package/skills/gsd-path-inspect/scripts/path_config.py +3 -3
  261. package/skills/gsd-path-inspect/scripts/pipeline_git.py +131 -42
  262. package/skills/gsd-path-inspect/scripts/pipeline_state.py +139 -39
  263. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -3
  264. package/skills/gsd-path-inspect/scripts/roadmap.py +9 -1
  265. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +39 -36
  266. package/skills/gsd-path-inspect/scripts/state_promote.py +15 -4
  267. package/skills/gsd-path-inspect/scripts/workflow_run.py +14 -4
  268. package/skills/gsd-path-inspect/scripts/worktree_paths.py +54 -2
  269. package/skills/gsd-path-inspect/templates/docs-audit.md +5 -1
  270. package/skills/gsd-path-inspect/templates/state.md +2 -1
  271. package/skills/gsd-path-loop/references/operating-rules.md +11 -3
  272. package/skills/gsd-path-loop/references/runtime-selection.md +9 -0
  273. package/skills/gsd-path-loop/scripts/_common.py +256 -2
  274. package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
  275. package/skills/gsd-path-loop/scripts/loop_run.py +16 -4
  276. package/skills/gsd-path-loop/scripts/model_policy.py +5 -1
  277. package/skills/gsd-path-loop/scripts/path_config.py +3 -3
  278. package/skills/gsd-path-loop/scripts/worktree_paths.py +54 -2
  279. package/skills/gsd-path-migrate/SKILL.md +8 -0
  280. package/skills/gsd-path-migrate/references/operating-rules.md +11 -3
  281. package/skills/gsd-path-migrate/references/runtime-selection.md +9 -0
  282. package/skills/gsd-path-migrate/scripts/_common.py +256 -2
  283. package/skills/gsd-path-migrate/scripts/discussion_records.py +9 -1
  284. package/skills/gsd-path-migrate/scripts/loop_run.py +16 -4
  285. package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -0
  286. package/skills/gsd-path-migrate/scripts/model_policy.py +5 -1
  287. package/skills/gsd-path-migrate/scripts/path_config.py +3 -3
  288. package/skills/gsd-path-plan/SKILL.md +14 -3
  289. package/skills/gsd-path-plan/references/build-recovery.md +13 -0
  290. package/skills/gsd-path-plan/references/dispatch.md +13 -0
  291. package/skills/gsd-path-plan/references/operating-rules.md +11 -3
  292. package/skills/gsd-path-plan/references/planner.md +26 -4
  293. package/skills/gsd-path-plan/references/runtime-selection.md +9 -0
  294. package/skills/gsd-path-plan/references/spec-probes.md +150 -0
  295. package/skills/gsd-path-plan/scripts/_common.py +256 -2
  296. package/skills/gsd-path-plan/scripts/archive_milestone.py +14 -13
  297. package/skills/gsd-path-plan/scripts/build_recovery.py +155 -58
  298. package/skills/gsd-path-plan/scripts/check_handoffs.py +635 -57
  299. package/skills/gsd-path-plan/scripts/check_task_briefs.py +293 -32
  300. package/skills/gsd-path-plan/scripts/detect_project.py +260 -20
  301. package/skills/gsd-path-plan/scripts/discussion_records.py +9 -1
  302. package/skills/gsd-path-plan/scripts/discussion_validate.py +93 -35
  303. package/skills/gsd-path-plan/scripts/integration.py +244 -65
  304. package/skills/gsd-path-plan/scripts/isolation.py +539 -70
  305. package/skills/gsd-path-plan/scripts/loop_run.py +16 -4
  306. package/skills/gsd-path-plan/scripts/members.py +77 -32
  307. package/skills/gsd-path-plan/scripts/model_policy.py +5 -1
  308. package/skills/gsd-path-plan/scripts/path_config.py +3 -3
  309. package/skills/gsd-path-plan/scripts/pipeline_git.py +131 -42
  310. package/skills/gsd-path-plan/scripts/pipeline_state.py +139 -39
  311. package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -3
  312. package/skills/gsd-path-plan/scripts/roadmap.py +9 -1
  313. package/skills/gsd-path-plan/scripts/state_checkpoint.py +39 -36
  314. package/skills/gsd-path-plan/scripts/state_promote.py +15 -4
  315. package/skills/gsd-path-plan/scripts/workflow_run.py +14 -4
  316. package/skills/gsd-path-plan/scripts/worktree_paths.py +54 -2
  317. package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
  318. package/skills/gsd-path-plan/templates/plan.md +17 -2
  319. package/skills/gsd-path-plan/templates/task.md +1 -0
  320. package/skills/gsd-path-research/SKILL.md +11 -0
  321. package/skills/gsd-path-research/references/dispatch.md +13 -0
  322. package/skills/gsd-path-research/references/operating-rules.md +11 -3
  323. package/skills/gsd-path-research/references/runtime-selection.md +9 -0
  324. package/skills/gsd-path-research/scripts/_common.py +256 -2
  325. package/skills/gsd-path-research/scripts/archive_milestone.py +14 -13
  326. package/skills/gsd-path-research/scripts/build_recovery.py +155 -58
  327. package/skills/gsd-path-research/scripts/check_handoffs.py +635 -57
  328. package/skills/gsd-path-research/scripts/check_task_briefs.py +293 -32
  329. package/skills/gsd-path-research/scripts/detect_project.py +260 -20
  330. package/skills/gsd-path-research/scripts/discussion_records.py +9 -1
  331. package/skills/gsd-path-research/scripts/discussion_validate.py +93 -35
  332. package/skills/gsd-path-research/scripts/integration.py +244 -65
  333. package/skills/gsd-path-research/scripts/isolation.py +539 -70
  334. package/skills/gsd-path-research/scripts/loop_run.py +16 -4
  335. package/skills/gsd-path-research/scripts/members.py +77 -32
  336. package/skills/gsd-path-research/scripts/model_policy.py +5 -1
  337. package/skills/gsd-path-research/scripts/path_config.py +3 -3
  338. package/skills/gsd-path-research/scripts/pipeline_git.py +131 -42
  339. package/skills/gsd-path-research/scripts/pipeline_state.py +139 -39
  340. package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -3
  341. package/skills/gsd-path-research/scripts/roadmap.py +9 -1
  342. package/skills/gsd-path-research/scripts/state_checkpoint.py +39 -36
  343. package/skills/gsd-path-research/scripts/state_promote.py +15 -4
  344. package/skills/gsd-path-research/scripts/worktree_paths.py +54 -2
  345. package/skills/gsd-path-research/templates/research-handoff.md +14 -3
  346. package/skills/gsd-path-roadmap/SKILL.md +16 -6
  347. package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
  348. package/skills/gsd-path-roadmap/references/operating-rules.md +11 -3
  349. package/skills/gsd-path-roadmap/references/runtime-selection.md +9 -0
  350. package/skills/gsd-path-roadmap/scripts/_common.py +256 -2
  351. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +14 -13
  352. package/skills/gsd-path-roadmap/scripts/build_recovery.py +155 -58
  353. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +635 -57
  354. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +293 -32
  355. package/skills/gsd-path-roadmap/scripts/detect_project.py +260 -20
  356. package/skills/gsd-path-roadmap/scripts/discussion_records.py +9 -1
  357. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +93 -35
  358. package/skills/gsd-path-roadmap/scripts/integration.py +244 -65
  359. package/skills/gsd-path-roadmap/scripts/isolation.py +539 -70
  360. package/skills/gsd-path-roadmap/scripts/loop_run.py +16 -4
  361. package/skills/gsd-path-roadmap/scripts/members.py +77 -32
  362. package/skills/gsd-path-roadmap/scripts/model_policy.py +5 -1
  363. package/skills/gsd-path-roadmap/scripts/path_config.py +3 -3
  364. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +131 -42
  365. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +139 -39
  366. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -3
  367. package/skills/gsd-path-roadmap/scripts/roadmap.py +9 -1
  368. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +39 -36
  369. package/skills/gsd-path-roadmap/scripts/state_promote.py +15 -4
  370. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +54 -2
  371. package/skills/gsd-path-ship/SKILL.md +32 -5
  372. package/skills/gsd-path-ship/references/dispatch.md +13 -0
  373. package/skills/gsd-path-ship/references/operating-rules.md +11 -3
  374. package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
  375. package/skills/gsd-path-ship/references/reviewer-wave.md +56 -12
  376. package/skills/gsd-path-ship/references/reviewer.md +3 -1
  377. package/skills/gsd-path-ship/references/runtime-selection.md +9 -0
  378. package/skills/gsd-path-ship/references/spec-probes.md +150 -0
  379. package/skills/gsd-path-ship/scripts/_common.py +256 -2
  380. package/skills/gsd-path-ship/scripts/archive_milestone.py +14 -13
  381. package/skills/gsd-path-ship/scripts/build_recovery.py +155 -58
  382. package/skills/gsd-path-ship/scripts/build_state.py +274 -8
  383. package/skills/gsd-path-ship/scripts/check_handoffs.py +635 -57
  384. package/skills/gsd-path-ship/scripts/check_task_briefs.py +293 -32
  385. package/skills/gsd-path-ship/scripts/detect_project.py +260 -20
  386. package/skills/gsd-path-ship/scripts/discussion_records.py +9 -1
  387. package/skills/gsd-path-ship/scripts/discussion_validate.py +93 -35
  388. package/skills/gsd-path-ship/scripts/integration.py +244 -65
  389. package/skills/gsd-path-ship/scripts/isolation.py +539 -70
  390. package/skills/gsd-path-ship/scripts/loop_run.py +16 -4
  391. package/skills/gsd-path-ship/scripts/members.py +77 -32
  392. package/skills/gsd-path-ship/scripts/model_policy.py +5 -1
  393. package/skills/gsd-path-ship/scripts/path_config.py +3 -3
  394. package/skills/gsd-path-ship/scripts/pipeline_git.py +131 -42
  395. package/skills/gsd-path-ship/scripts/pipeline_state.py +139 -39
  396. package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -3
  397. package/skills/gsd-path-ship/scripts/roadmap.py +9 -1
  398. package/skills/gsd-path-ship/scripts/state_checkpoint.py +39 -36
  399. package/skills/gsd-path-ship/scripts/state_promote.py +15 -4
  400. package/skills/gsd-path-ship/scripts/workflow_run.py +14 -4
  401. package/skills/gsd-path-ship/scripts/worktree_paths.py +54 -2
  402. package/skills/gsd-path-ship/templates/final-review.md +11 -2
  403. package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
  404. package/skills/gsd-path-ship/templates/wave-review.md +28 -9
  405. package/skills/gsd-path-undo/references/operating-rules.md +11 -3
  406. package/skills/gsd-path-undo/references/runtime-selection.md +9 -0
  407. package/skills/gsd-path-undo/scripts/_common.py +256 -2
  408. package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
  409. package/skills/gsd-path-undo/scripts/loop_run.py +16 -4
  410. package/skills/gsd-path-undo/scripts/model_policy.py +5 -1
  411. package/skills/gsd-path-undo/scripts/path_config.py +3 -3
  412. package/skills/gsd-path-undo/scripts/pipeline_undo.py +118 -16
  413. package/skills/gsd-path-undo/scripts/worktree_paths.py +54 -2
  414. package/skills/path/DEFINE.md +34 -5
  415. package/skills/path/PLAN.md +14 -3
  416. package/skills/path/RESEARCH.md +11 -0
  417. package/skills/path/ROADMAP.md +16 -6
  418. package/skills/path/SHIP.md +32 -5
  419. package/skills/path/SKILL.md +52 -24
  420. package/skills/path/references/build-native.md +112 -10
  421. package/skills/path/references/build-recovery.md +13 -0
  422. package/skills/path/references/coder.md +7 -3
  423. package/skills/path/references/dispatch.md +13 -0
  424. package/skills/path/references/docs-auditor.md +5 -0
  425. package/skills/path/references/operating-rules.md +11 -3
  426. package/skills/path/references/planner.md +26 -4
  427. package/skills/path/references/reviewer-final.md +20 -2
  428. package/skills/path/references/reviewer-wave.md +56 -12
  429. package/skills/path/references/reviewer.md +3 -1
  430. package/skills/path/references/runtime-selection.md +9 -0
  431. package/skills/path/references/spec-probes.md +150 -0
  432. package/skills/path/scripts/_common.py +256 -2
  433. package/skills/path/scripts/archive_milestone.py +14 -13
  434. package/skills/path/scripts/build_recovery.py +155 -58
  435. package/skills/path/scripts/build_state.py +274 -8
  436. package/skills/path/scripts/check_docs_audit.py +77 -4
  437. package/skills/path/scripts/check_handoffs.py +635 -57
  438. package/skills/path/scripts/check_task_briefs.py +293 -32
  439. package/skills/path/scripts/detect_project.py +260 -20
  440. package/skills/path/scripts/discussion_records.py +9 -1
  441. package/skills/path/scripts/discussion_validate.py +93 -35
  442. package/skills/path/scripts/dispatch_driver.py +549 -53
  443. package/skills/path/scripts/git_guard.py +27 -8
  444. package/skills/path/scripts/integration.py +244 -65
  445. package/skills/path/scripts/isolation.py +539 -70
  446. package/skills/path/scripts/loop_run.py +16 -4
  447. package/skills/path/scripts/members.py +77 -32
  448. package/skills/path/scripts/model_policy.py +5 -1
  449. package/skills/path/scripts/path_config.py +3 -3
  450. package/skills/path/scripts/pipeline_git.py +131 -42
  451. package/skills/path/scripts/pipeline_state.py +139 -39
  452. package/skills/path/scripts/promote_lookahead.py +3 -3
  453. package/skills/path/scripts/review_findings.py +180 -44
  454. package/skills/path/scripts/roadmap.py +9 -1
  455. package/skills/path/scripts/state_checkpoint.py +39 -36
  456. package/skills/path/scripts/state_promote.py +15 -4
  457. package/skills/path/scripts/status_runtime.py +22 -4
  458. package/skills/path/scripts/task_context.py +14 -3
  459. package/skills/path/scripts/workflow_run.py +14 -4
  460. package/skills/path/scripts/worktree_paths.py +54 -2
  461. package/skills/path/templates/docs-audit.md +5 -1
  462. package/skills/path/templates/final-review.md +11 -2
  463. package/skills/path/templates/intent.md +30 -0
  464. package/skills/path/templates/patch-findings.md +3 -3
  465. package/skills/path/templates/plan.md +17 -2
  466. package/skills/path/templates/research-handoff.md +14 -3
  467. package/skills/path/templates/state.md +2 -1
  468. package/skills/path/templates/task.md +1 -0
  469. package/skills/path/templates/wave-review.md +28 -9
  470. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  471. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  472. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  473. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  474. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  475. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  476. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
@@ -426,10 +426,10 @@ def frontmatter_value(content: str, key: str) -> Optional[str]:
426
426
  for line in lines[1:]:
427
427
  if line == "---":
428
428
  break
429
- match = re.match(rf"^{re.escape(key)}:\s*([^#]*?)\s*(?:#.*)?$", line)
429
+ match = re.match(rf"^{re.escape(key)}:(.*)$", line)
430
430
  if match:
431
- value = match.group(1).strip().strip('"').strip("'")
432
- return value
431
+ value = _common.strip_yaml_comment(match.group(1))
432
+ return value.strip().strip('"').strip("'")
433
433
  return None
434
434
 
435
435
 
@@ -586,14 +586,11 @@ def resolved_archive_target(
586
586
  expected_number = milestone_number(branch)
587
587
  configured = parsed_state.archive
588
588
  entries = archive_sequence_entries(project)
589
- missing_prior = [
590
- number for number in range(1, expected_number) if number not in entries
591
- ]
592
589
  later = sorted(number for number in entries if number > expected_number)
593
- if missing_prior or later:
590
+ if later:
594
591
  raise ArchiveError(
595
592
  f"archive sequence does not match bound branch {branch}: "
596
- f"missing={missing_prior}, later={later}"
593
+ f"later={later}"
597
594
  )
598
595
 
599
596
  if configured is not None:
@@ -1065,9 +1062,10 @@ def prepare_locked(project: Path, active_root: Path, slug: str) -> dict:
1065
1062
  final = active_root / "review" / "FINAL.md"
1066
1063
  reviewed = re.search(r"(?m)^Reviewed HEAD:\s*(\S+)", final.read_text(encoding="utf-8")) if final.is_file() else None
1067
1064
  head = require_git_success(run_git(project, "rev-parse", "HEAD"), "resolve HEAD").strip()
1068
- if reviewed and reviewed.group(1) != head:
1065
+ if reviewed and not _common.reviewed_head_covers(project, reviewed.group(1), head):
1069
1066
  raise ArchiveError(
1070
1067
  f"FINAL.md Reviewed HEAD {reviewed.group(1)} does not match current HEAD {head}; "
1068
+ f"{_common.RUNTIME_PIN_ALLOWANCE}; "
1071
1069
  "re-run the final review on the current commit before archiving"
1072
1070
  )
1073
1071
  target = resolved_archive_target(project, slug, parsed_state)
@@ -1680,6 +1678,7 @@ def abandon_locked(
1680
1678
 
1681
1679
 
1682
1680
  run_git = _common.run_git
1681
+ reviewed_head_covers = _common.reviewed_head_covers
1683
1682
  run_command = _common.run_command
1684
1683
 
1685
1684
 
@@ -1899,9 +1898,10 @@ def preflight(repo: Path) -> dict:
1899
1898
  require_stageable_carry_forward(project, archive)
1900
1899
  require_clean_older_archives(project, configured)
1901
1900
  head = require_git_success(run_git(project, "rev-parse", "HEAD"), "resolve preflight HEAD")
1902
- if reviewed_head != head:
1901
+ if not reviewed_head_covers(project, reviewed_head, head):
1903
1902
  raise ArchiveError(
1904
- f"FINAL.md Reviewed HEAD {reviewed_head} does not match current HEAD {head}"
1903
+ f"FINAL.md Reviewed HEAD {reviewed_head} does not match current HEAD {head}; "
1904
+ f"{_common.RUNTIME_PIN_ALLOWANCE}"
1905
1905
  )
1906
1906
  return {
1907
1907
  "archive": configured,
@@ -2093,9 +2093,10 @@ def validate(repo: Path, *, historical: bool = False) -> dict:
2093
2093
 
2094
2094
  members = member_ship_rows(project, archive, ship_commit)
2095
2095
  ship_parent = require_git_success(run_git(project, "rev-parse", f"{ship_commit}^"), "resolve ship parent")
2096
- if reviewed_head != ship_parent:
2096
+ if not reviewed_head_covers(project, reviewed_head, ship_parent):
2097
2097
  raise ArchiveError(
2098
- f"FINAL.md Reviewed HEAD {reviewed_head} does not match ship parent {ship_parent}"
2098
+ f"FINAL.md Reviewed HEAD {reviewed_head} does not match ship parent {ship_parent}; "
2099
+ f"{_common.RUNTIME_PIN_ALLOWANCE}"
2099
2100
  )
2100
2101
  require_canonical_commit_body(
2101
2102
  project,
@@ -1,5 +1,5 @@
1
1
  # gsd-path project runtime
2
- """Guard a blocked build's return to Define or Plan using its committed base."""
2
+ """Guard return to Define or Plan using a committed recovery base."""
3
3
 
4
4
  from __future__ import annotations
5
5
 
@@ -17,7 +17,13 @@ EVENTS = {
17
17
  "define": "build intent corrections requested",
18
18
  "plan": "build plan repair requested",
19
19
  }
20
+ PLAN_INTENT_EVENT = "plan intent corrections requested"
20
21
  MARKER = "build recovery: "
22
+ RECOVERY_KEYS = frozenset({"base", "branch", "kind", "source"})
23
+
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.,$/:]*")
21
27
 
22
28
 
23
29
  def runtime():
@@ -36,9 +42,51 @@ def checkpoint_runtime():
36
42
  return state_checkpoint
37
43
 
38
44
 
45
+ def _parse_recovery_record(value: object) -> dict:
46
+ if (
47
+ not isinstance(value, dict)
48
+ or not {"base", "branch", "kind"}.issubset(value)
49
+ or not set(value).issubset(RECOVERY_KEYS)
50
+ or not isinstance(value["base"], str)
51
+ or not re.fullmatch(r"[0-9a-f]{40}", value["base"])
52
+ ):
53
+ raise runtime().PipelineStateError("invalid build recovery record")
54
+ source = value.get("source", "build")
55
+ if source not in {"build", "plan"}:
56
+ raise runtime().PipelineStateError("invalid build recovery record")
57
+ if source == "plan":
58
+ if value["kind"] != "define":
59
+ raise runtime().PipelineStateError("invalid build recovery record")
60
+ elif value["kind"] not in EVENTS:
61
+ raise runtime().PipelineStateError("invalid build recovery record")
62
+ return {**value, "source": source}
63
+
64
+
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
+ ):
76
+ return False
77
+ if source == "plan":
78
+ return (before.phase, before.status) in {("plan", "active"), ("plan", "blocked")}
79
+ return (before.phase, before.status) == ("build", "blocked")
80
+
81
+
39
82
  def context(repo: Path, text: str | None = None) -> dict | None:
40
83
  state = runtime()
41
- current, loaded, _ = state.load_state(repo)
84
+ try:
85
+ current, loaded, _ = state.load_state(repo)
86
+ except state.PipelineStateError as error:
87
+ if "STATE.md must be a real file" not in str(error):
88
+ raise
89
+ return None
42
90
  text = loaded if text is None else text
43
91
  recovery = None
44
92
  for line in text.splitlines():
@@ -47,26 +95,26 @@ def context(repo: Path, text: str | None = None) -> dict | None:
47
95
  event = line.split(" — ", 2)[-1]
48
96
  if event.startswith(MARKER):
49
97
  try:
50
- value = json.loads(event[len(MARKER):])
98
+ value = _parse_recovery_record(json.loads(event[len(MARKER):]))
51
99
  except ValueError as error:
52
100
  raise state.PipelineStateError("invalid build recovery record") from error
53
- if (not isinstance(value, dict) or set(value) != {"base", "branch", "kind"}
54
- or value["kind"] not in EVENTS
55
- or not isinstance(value["base"], str)
56
- or not re.fullmatch(r"[0-9a-f]{40}", value["base"])):
57
- raise state.PipelineStateError("invalid build recovery record")
58
101
  recovery = {**value, "active": True} if value["branch"] == current.branch else None
59
- elif event == "build started" and recovery:
60
- recovery["active"] = False
102
+ elif recovery and recovery.get("active"):
103
+ source = recovery.get("source", "build")
104
+ if source == "build" and event == "build started":
105
+ recovery["active"] = False
106
+ elif source == "plan" and event == "plan approved":
107
+ recovery["active"] = False
61
108
  if recovery:
62
109
  if not state._is_ancestor(repo, recovery["base"], "HEAD"):
63
110
  raise state.PipelineStateError("build recovery base is not an ancestor of HEAD")
64
- source = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/STATE.md")
65
- if source is None:
111
+ base_text = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/STATE.md")
112
+ if base_text is None:
66
113
  raise state.PipelineStateError("build recovery base has no STATE.md")
67
- before = state._state_from_text(source)
68
- if ((before.phase, before.status) != ("build", "blocked")
69
- or before.branch != current.branch or before.milestone != current.milestone):
114
+ before = state._state_from_text(base_text)
115
+ if not _recovery_base_proves_milestone(
116
+ before, current, recovery.get("source", "build"), recovery["active"]
117
+ ):
70
118
  raise state.PipelineStateError("build recovery base does not prove this blocked milestone")
71
119
  return recovery
72
120
 
@@ -108,18 +156,20 @@ def settled_tasks(repo: Path) -> dict[str, tuple[str, dict]]:
108
156
  return tasks
109
157
 
110
158
 
111
- def begin(repo: Path, before, after, event: str) -> dict:
159
+ def _begin_common(repo: Path, before, event: str) -> str:
112
160
  state = runtime()
113
- if (before.phase, before.status) != ("build", "blocked") or after.phase not in EVENTS:
114
- raise state.PipelineStateError("build recovery requires build/blocked")
115
- if event != EVENTS[after.phase]:
116
- raise state.PipelineStateError(f"state transition requires event: {EVENTS[after.phase]}")
117
161
  if before.archive is not None or before.branch != state._current_branch(repo):
118
162
  raise state.PipelineStateError("build recovery requires the active bound branch")
119
163
  if state._worktree_changes(repo):
120
164
  raise state.PipelineStateError("checkpoint the blocked build before recovery")
121
165
  if any(state.transaction_journals(repo).values()):
122
166
  raise state.PipelineStateError("finish pending transactions before build recovery")
167
+ state._read_real_file(repo / ".project/intent/INTENT.md", "INTENT.md")
168
+ return state._run_git(repo, "rev-parse", "HEAD").stdout.strip()
169
+
170
+
171
+ def _settle_build_dispatch(repo: Path) -> None:
172
+ state = runtime()
123
173
  records = records_root(repo)
124
174
  for directory in {path.parent.parent for path in records.rglob("attempt-*/state.json")}:
125
175
  latest = max(directory.glob("attempt-*/state.json"), key=lambda path: int(path.parent.name.split("-")[1]))
@@ -128,19 +178,49 @@ def begin(repo: Path, before, after, event: str) -> dict:
128
178
  or (record.get("worktree") and record.get("branch") and not record.get("cleanup_complete"))
129
179
  or record.get("outcome") not in {"landed", "collected", "blocked", "resolved", "redispatched"}):
130
180
  raise state.PipelineStateError(f"settle dispatch records before recovery: {latest}")
131
- tasks = settled_tasks(repo)
132
- state._read_real_file(repo / ".project/plan/PLAN.md", "PLAN.md")
133
- state._read_real_file(repo / ".project/intent/INTENT.md", "INTENT.md")
134
- base = state._run_git(repo, "rev-parse", "HEAD").stdout.strip()
181
+
182
+
183
+ def _verify_done_tasks(repo: Path, tasks: dict[str, tuple[str, dict]], base: str) -> None:
184
+ state = runtime()
135
185
  done = [repo / ".project/tasks" / name for name, (_, fields) in tasks.items()
136
186
  if fields["status"] == "done"]
137
- if done:
138
- if __package__:
139
- from scripts.isolation import verify_landed_task_files
140
- else:
141
- from isolation import verify_landed_task_files
142
- verify_landed_task_files(repo, done, ".project/tasks", base)
143
- return {"kind": after.phase, "base": base, "branch": before.branch}
187
+ if not done:
188
+ return
189
+ if __package__:
190
+ from scripts.isolation import verify_landed_task_files
191
+ else:
192
+ from isolation import verify_landed_task_files
193
+ verify_landed_task_files(repo, done, ".project/tasks", base)
194
+
195
+
196
+ def begin(repo: Path, before, after, event: str) -> dict:
197
+ state = runtime()
198
+ if (before.phase, before.status) == ("build", "blocked") and after.phase in EVENTS:
199
+ if event != EVENTS[after.phase]:
200
+ raise state.PipelineStateError(f"state transition requires event: {EVENTS[after.phase]}")
201
+ _settle_build_dispatch(repo)
202
+ tasks = settled_tasks(repo)
203
+ state._read_real_file(repo / ".project/plan/PLAN.md", "PLAN.md")
204
+ base = _begin_common(repo, before, event)
205
+ _verify_done_tasks(repo, tasks, base)
206
+ return {"kind": after.phase, "base": base, "branch": before.branch, "source": "build"}
207
+ if (
208
+ before.phase == "plan"
209
+ and before.status in {"active", "blocked"}
210
+ and (after.phase, after.status) == ("define", "active")
211
+ ):
212
+ if event != PLAN_INTENT_EVENT:
213
+ raise state.PipelineStateError(f"state transition requires event: {PLAN_INTENT_EVENT}")
214
+ state._read_real_file(repo / ".project/plan/PLAN.md", "PLAN.md")
215
+ tasks_dir = repo / ".project/tasks"
216
+ tasks = settled_tasks(repo) if tasks_dir.is_dir() and any(
217
+ path.is_file() and not state._common.is_ignored_junk(path)
218
+ for path in tasks_dir.iterdir()
219
+ ) else {}
220
+ base = _begin_common(repo, before, event)
221
+ _verify_done_tasks(repo, tasks, base)
222
+ return {"kind": "define", "base": base, "branch": before.branch, "source": "plan"}
223
+ raise state.PipelineStateError("build recovery requires build/blocked")
144
224
 
145
225
 
146
226
  def records_root(repo: Path) -> Path:
@@ -224,61 +304,78 @@ def validate_plan(repo: Path) -> None:
224
304
  intent = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/intent/INTENT.md")
225
305
  if state._read_real_file(repo / ".project/intent/INTENT.md", "INTENT.md") != intent:
226
306
  raise state.PipelineStateError("plan repair cannot change approved intent")
227
- unchanged = unchanged_waves(repo, recovery)
228
- for path in (repo / ".project/review").iterdir():
229
- match = re.match(r"wave-([1-9]\d*)[.]", path.name)
230
- if match and int(match[1]) not in unchanged:
231
- 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")
309
+
232
310
 
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))}
233
314
 
234
- def unchanged_waves(repo: Path, recovery: dict) -> set[int]:
235
- """Reuse evidence only when its intent, wave contract, and task bytes match."""
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."""
236
318
  state = runtime()
237
319
  project = repo / ".project"
238
320
  old_intent = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/intent/INTENT.md")
239
321
  if state._read_real_file(project / "intent/INTENT.md", "INTENT.md") != old_intent:
240
- return set()
322
+ return dict.fromkeys(waves, "INTENT.md changed")
241
323
  old_plan = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/plan/PLAN.md") or ""
242
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")
243
328
 
244
329
  def sections(text):
245
330
  return {match[1]: match[2] for match in re.finditer(
246
331
  r"(?ms)^## ([^\n]+)\n(.*?)(?=^## |\Z)", text,
247
332
  )}
248
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
+
249
341
  old_sections, new_sections = sections(old_plan), sections(new_plan)
250
- # Non-wave settings govern every wave, including coverage and surfaces.
251
- if ({k: v for k, v in old_sections.items() if not k.startswith("Wave ")}
252
- != {k: v for k, v in new_sections.items() if not k.startswith("Wave ")}):
253
- 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")
254
346
  old_tasks = checkpoint_runtime()._task_contracts_at(repo, recovery["base"], ".project/tasks")
255
347
  current = settled_tasks(repo)
256
- unchanged = set()
257
- for title, body in new_sections.items():
258
- match = re.match(r"Wave ([1-9]\d*)\b", title)
259
- 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"
260
353
  continue
261
- wave = match[1]
262
354
  before = {Path(path).name: text for path, text in old_tasks.values()
263
- if task_fields(text, path).get("wave") == wave}
264
- after = {name: text for name, (text, fields) in current.items() if fields.get("wave") == wave}
265
- if before and before == after:
266
- unchanged.add(int(wave))
267
- 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
268
360
 
269
361
 
270
- 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."""
271
364
  recovery = context(repo)
272
365
  if not recovery or not recovery["active"]:
273
- return
274
- unchanged = unchanged_waves(repo, recovery)
275
- 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():
276
371
  match = re.match(r"wave-([1-9]\d*)[.]", path.name)
277
- if match and int(match[1]) in unchanged:
372
+ if match and int(match[1]) not in reasons:
278
373
  destination = repo / ".project/review" / path.name
279
374
  content = path.read_text(encoding="utf-8")
280
375
  if not destination.exists() or runtime()._read_real_file(destination, destination.name) != content:
281
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)]}
282
379
 
283
380
 
284
381
  def inventory_checkpoint(repo: Path, head: str) -> bool: