@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
package/RUNTIME.md CHANGED
@@ -23,7 +23,7 @@ inputs, receipt handling, and entry conditions are owned by the
23
23
  | `gate-plan` | Git HEAD; optional lookahead project directory | Pending discussion, intent coverage, brief paths, and panel configuration checked in order. |
24
24
  | `approve-plan` | Owner approval and `--expected-head <reviewed full SHA>` | Plan gates followed by the existing journaled approval checkpoint. |
25
25
  | `lint-round` | Git HEAD; optional `--project-dir .project/next` | Task brief checks against HEAD, then plan handoff checks. |
26
- | `prepare-task` | `--expected-head <clean base> --task-id <id> --round-size <ready task count>` | Canonical task isolation; serial work also gets a verification sidecar before the primary becomes dirty. |
26
+ | `prepare-task` | `--expected-head <clean base> --task-id <id> --round-size <ready task count>` | Canonical task isolation; serial work also gets a verification sidecar before the primary becomes dirty. A verification sidecar that an earlier `prepare-task` left for the task is retired first only when it is proven untouched: its branch did not move from its creation base on the bound branch, and it has no tracked change and no untracked or gitignored file. Any other sidecar stays, and the command stops with its path and what was found. |
27
27
  | `build-evidence` | `--expected-head <full SHA>` | Canonical landing proof written to `<project-dir>/build/evidence.json` (default: `.project/build/evidence.json`). |
28
28
 
29
29
  Plan approval recovery remains `pipeline_state.py resume-checkpoint`. Pre-Git
@@ -68,7 +68,7 @@ skeptic attempts under `skeptics/`.
68
68
  | `skeptics --repo <root> --wave <N> --cycle <C> --child-command '<cmd>' [--wait <s>] [--child-timeout <s>]` | Step 7 skeptic branch: one read-only skeptic per `skeptic_groups` locator in its own sidecar at the recorded review base, briefed with the criterion, every observation, and the cited tasks verbatim; validate with `review_findings.py`, collect, retire, then rerun the helper. `none` without skeptic groups; `escalate` for structural blockers or the cycle cap. The skeptic files stay uncommitted for the fix-task or ruling checkpoint. |
69
69
  | `fix-tasks --repo <root> --wave <N> --cycle <C>` | Step 7 batching: one fix task per `review_findings.py collect` batch with the failed criteria and observations verbatim, lint-checked; `escalate` for structural blockers, skeptic groups, cycle cap, or all-refuted. |
70
70
  | `complete --repo <root>` | Completion: clean primary at `build/active` (or a crash-left `build/done`), no unfinished wave, the archive-time review cycle check (`discussion_validate.review_cycle_counts`: contiguous cycles, passing last cycle, panel and skeptic artifacts present); `workflow_run.py build-evidence` at HEAD; `pipeline_state.py transition` to `ship/active` with event `build done; final review pending`; checkpoint `build: complete milestone`. |
71
- | `finish --repo <root> --task-id <id>` | Recover first; return a proven landing without repeating Verify, or verify, land, record, and retire one returned task. Without a dispatch record, or when the newest record is settled and its base differs from the task's current in-progress base (a native retry after a driver block), read the primary task's in-progress state; if it lacks a recorded base, use the parallel task isolate's state only if that worktree is authorized. A blocked record at the same base still refuses the retry. A deactivated isolate stays blocked. |
71
+ | `finish --repo <root> --task-id <id>` | Recover first; return a proven landing without repeating Verify, or verify, land, record, and retire one returned task. Without a dispatch record, or when the newest record is settled and its base differs from the task's current in-progress base (a native retry after a driver block), read the primary task's in-progress state; if it lacks a recorded base, use the parallel task isolate's state only if that worktree is authorized. A blocked record at the same base still refuses the retry, except a host-dispatched record (no child command) while the task is still in progress at that base: `finish` may run again after a failed Verify. A deactivated isolate stays blocked. |
72
72
  | `answer --repo <root> --task-id <id> --answer '<text>'` | Append `Orchestrator answer:` to the isolate's task Log; the next `round` redispatches that isolate. |
73
73
  | `status --repo <root>` | Every task's newest dispatch record. |
74
74
 
@@ -95,6 +95,13 @@ For `round`, `review`, `panel`, and `skeptics`, when the child process is gone a
95
95
  unset, the driver rereads that attempt's state and exit receipt before
96
96
  collecting the result or blocking with
97
97
  `child wrapper exited without recording a result`.
98
+ A landing is final when the removal of its isolate fails: the receipt reports
99
+ the task as landed with a `cleanup_warning`, and the next `round` or `finish`
100
+ checks the isolate again. When only a leftover of a landed task's isolate
101
+ remains and it holds nothing that Git does not have, the driver stops with
102
+ `task <id> is landed; remove its leftover isolate: <path>`, changes neither
103
+ STATE nor HEAD, and never deletes the path itself. Every other isolate error
104
+ keeps its recovery block.
98
105
  Review starts from a clean base. A same-cycle retry may dispatch a missing lens
99
106
  at that unchanged base while verified, already-collected review artifacts remain
100
107
  uncommitted. Altered artifacts or unrelated changes block recovery.
@@ -127,7 +134,15 @@ with `another dispatch_driver invocation holds the lock`; wait for the
127
134
  active call to return before retrying. Bookkeeping also blocks if an
128
135
  in-progress primary task has no open dispatch record.
129
136
 
130
- Verify output is saved in `verify.json`. A `verify-record` ledger entry is
137
+ Verify output is saved in `verify.json`. `finish` also runs the Verify of each
138
+ earlier-wave task that shares a file with the task, under the
139
+ [wave loop](skills/gsd-path/references/build-native.md#wave-loop) rule; those
140
+ runs are saved under `regressions` and a failure blocks the landing. A Verify
141
+ that a `supersedes` field retires does not run; it is listed under `retired`. A fix
142
+ task Verify that loses only the copy of a retired Verify has a `retired` entry with
143
+ the key `part` (the superseded task) and the Log entry `orchestrator regression
144
+ Verify of <fix task id>: part of <task id> retired: superseded by <task id>`. A task
145
+ with a `Heavy: yes` regression Verify counts as heavy. A `verify-record` ledger entry is
131
146
  written only when the landing commit's parent equals the recorded task base;
132
147
  a later parallel landing can therefore have no ledger entry.
133
148
 
@@ -174,8 +189,11 @@ through its receipt only when the canonical file matches the saved hash.
174
189
 
175
190
  `fix-tasks` reconciles existing repair tasks and PLAN rows before linting and
176
191
  checkpointing. It creates missing batches in an appended repair wave and
177
- reports new tasks in `created`, reused tasks in `existing`, and batches already
178
- covered by other repair tasks in `carried`. Status `created` includes restored
192
+ reports new tasks in `created`, reused tasks in `existing`, and batches that
193
+ other repair tasks not yet `done` cover in the same repo in `carried`. A member
194
+ batch and its task carry `repo`. A fix task for a finding that repeats after
195
+ its fix landed also depends on that landed fix task, so
196
+ `review_findings.py repair-evidence` can link the two. Status `created` includes restored
179
197
  PLAN rows or pending project bookkeeping; `exists` means nothing was written
180
198
  and no project changes needed a checkpoint. Status `none` means no batches
181
199
  need work; `escalate` leaves the named decisions with build step 7 and the user.
package/UPDATE.md CHANGED
@@ -273,6 +273,41 @@ Unrecognized or foreign `.project/` state is never auto-migrated — the router
273
273
 
274
274
  ---
275
275
 
276
+ ## Uninstall skills
277
+
278
+ ```bash
279
+ npx @opengsd/gsd-path@latest --uninstall --all --dry-run
280
+ npx @opengsd/gsd-path@latest --uninstall --all
281
+ ```
282
+
283
+ From a git clone, use `node scripts/install.mjs` with the same flags.
284
+
285
+ - Select the hosts with host flags or `--all`. A selection is necessary.
286
+ - Add `--local`, from the project directory, for project-local skills.
287
+ - `--dry-run` lists each path and removes nothing. A real run lists each
288
+ removed path.
289
+ - A host that has no GSD Path skills shows `nothing to remove`. This is not an
290
+ error. A second run removes nothing.
291
+
292
+ **Removed:** skill directories that GSD Path owns, and Cursor's
293
+ `agents/gsd-path.md`. A directory is owned when it has a GSD Path name
294
+ (`gsd-path`, `gsd-path-*`, the legacy `ogsd` names, or the `path` alias) and
295
+ contains a `VERSION` file or a `SKILL.md` that names `gsd-path`. The `path`
296
+ alias must also pass the ownership rule in
297
+ [What auto-updates](#what-auto-updates). A directory with a GSD Path name
298
+ that is not owned shows as `kept` and stays in place.
299
+
300
+ **Not removed:** other skills, host settings, `disabled-gsd-skills` backups,
301
+ and all project files: `.project/`, `.gsd-path/`, the `AGENTS.md` block, and
302
+ guard hooks. `--uninstall` with `--project`, `--update`, `--doctor`, or a hook
303
+ or runtime flag stops with an error.
304
+
305
+ Removal makes no backup. Changes that you made in a GSD Path skill directory
306
+ are lost. Codex, Zed, and Muse use one skills root (with `--local`, also
307
+ Antigravity): an uninstall for one of them removes the skills for all of them.
308
+
309
+ ---
310
+
276
311
  ## Troubleshooting
277
312
 
278
313
  | Symptom | Fix |
package/WORKFLOW.md CHANGED
@@ -161,7 +161,8 @@ Owned state routes by STATE.md without rerunning the helper. Reserve `classify`
161
161
  for read-only inspection; do not re-derive a verdict from a directory listing.
162
162
 
163
163
  For a newly initialized existing Git repository, the state router returns
164
- `bind-initial` while `STATE.branch` is null. The router fetches `origin/main`,
164
+ `bind-initial` while `STATE.branch` is null. The router fetches the recorded
165
+ [default branch](#default-branch) (`origin/main` in this document),
165
166
  runs `pipeline_git.py bind-initial` with that exact SHA, and records the
166
167
  returned `gsd-path/M00N` binding through `pipeline_state.py transition` before
167
168
  entering inspect or define. Build consumes this binding and never creates or
@@ -512,7 +513,11 @@ untouched.
512
513
  One exception: a DOCS-AUDIT.md with pending `planned: no` rulings is copied
513
514
  atomically back into a recreated `research/` so the alignment queue survives;
514
515
  the archived original keeps full history. Active and archived research may
515
- coexist only for that byte-identical carry-forward.
516
+ coexist only for that byte-identical carry-forward. When a lookahead track
517
+ brings its own `research/`, `promote-next` removes the carry-forward in the
518
+ promotion commit, but only if the lookahead DOCS-AUDIT.md keeps every
519
+ carry-forward ruling (a pending row may gain a `T###`); otherwise promotion
520
+ stops.
516
521
 
517
522
  Archives become read-only when committed. No phase may modify a committed
518
523
  archive; the next milestone's inspection may read it. A retry with a persisted
@@ -540,6 +545,49 @@ the subject and field body defined by its canonical phase contract; task-land
540
545
  fields live only in the
541
546
  [build contract](skills/gsd-path-build/SKILL.md).
542
547
 
548
+ ### Default branch
549
+
550
+ Path ships onto the default branch of `origin`. The name is not fixed.
551
+ `detect_project.py initialize` reads the local `refs/remotes/origin/HEAD` one
552
+ time and records the name in `STATE.md` as `default_branch`; an unset
553
+ `origin/HEAD` records `main`. Run `git remote set-head origin --auto` before
554
+ the first Path run so that the record is the default branch of the forge. A
555
+ `STATE.md` without the field means `main`, so older projects do not change.
556
+
557
+ This document writes `main` and `origin/main` for the recorded default branch.
558
+ In a project that records `master`, read them as `master` and `origin/master`:
559
+ the integration merge subject ends with `into master`, and the user's merge
560
+ request in `external-landing` mode targets `master`.
561
+
562
+ The record does not change after initialization. Bind, integrate, validate,
563
+ promotion, and the git guard read it. When the forge default no longer agrees
564
+ with the record, integration stops with `remote default must be <recorded>`.
565
+ A lookahead `next/STATE.md` must carry the same `default_branch` as the active
566
+ state, or promotion stops. Names in the reserved `gsd-path/`,
567
+ `gsd-path-integrate/`, and `gsd-path-task/` namespaces, and names with `#` or
568
+ a quote character, cannot be recorded.
569
+
570
+ In a multi-repo project, each repository has its own record. The coordinator
571
+ records its default branch in `STATE.md`, as above. `members.py add` reads the
572
+ local `refs/remotes/origin/HEAD` of the member one time and records the name
573
+ in the member's section of `.project/MEMBERS.md`, as `Default branch: <name>`;
574
+ an unset `origin/HEAD` records `main`. Run `git remote set-head origin --auto`
575
+ in the member before you add it. A section without the field means `main`, so
576
+ an older `MEMBERS.md` does not change. The members of one project can have
577
+ different default branches, and the coordinator default can differ from them.
578
+ The same name rules apply.
579
+
580
+ Build start, the task brief check, undo, the member pre-push guard, and member
581
+ close read the member record. The member bound branch starts at
582
+ `origin/<recorded>`, the member merge subject ends with `into <recorded>`, and
583
+ a member pull request targets `<recorded>`. When the remote default of the
584
+ member no longer agrees with the record, `members.py validate` and member
585
+ close stop with `remote default must be <recorded>`.
586
+
587
+ Not supported: a new repository that Path creates uses `main`. This includes
588
+ `members.py add --create`, which stops when the new repository has another
589
+ default branch.
590
+
543
591
  ### Integration
544
592
 
545
593
  After the postcommit validator passes, ship performs integration — the only
@@ -548,14 +596,15 @@ receives merges or back-merges, and the default checkout is never entered;
548
596
  the local default branch ref may lag origin, which is harmless because
549
597
  binding resolves remote SHAs. `STATE.integration_default` stores the project
550
598
  choice and `STATE.integration` stores the current milestone choice. Both are
551
- `direct` or `pull-request`; they may change only before build, and the current
599
+ `direct`, `pull-request`, or `external-landing`; they may change only before build, and the current
552
600
  milestone resets to the project default at the next handoff. Older v2 state
553
601
  without these fields means `direct`. `STATE.integration_source` is `default` or
554
602
  `milestone` so an explicit override remains distinct when its value happens to
555
603
  match the project default.
556
604
 
557
605
  Ship fetches origin, refreshes `origin/HEAD`, mirrors published milestone tags,
558
- and requires the remote default to be `main`. It then follows the locked mode:
606
+ and requires the remote default to be the recorded
607
+ [default branch](#default-branch). It then follows the locked mode:
559
608
 
560
609
  - `direct` creates a temporary named worktree (`gsd-path-integrate/M00N`) at
561
610
  the fetched remote-default SHA and merges the ship commit with `--no-ff`
@@ -567,10 +616,20 @@ and requires the remote default to be `main`. It then follows the locked mode:
567
616
  - `pull-request` requires `gh` authentication for GitHub.com and a GitHub.com
568
617
  origin. Path publishes the exact ship commit and creates or reuses the one
569
618
  eligible PR to `main` with a GSD Path credit footer. It returns
570
- `awaiting-merge` until a human GitHub user merges it with a merge commit; Path
571
- never enables auto-merge or merges the PR. After validation, Path creates the
572
- annotated milestone tag. The [ship contract](skills/gsd-path-ship/SKILL.md)
619
+ `awaiting-merge` until a human GitHub user merges it with a merge commit or a
620
+ rebase merge; Path never enables auto-merge or merges the PR. After
621
+ validation, Path creates the annotated milestone tag. The [ship contract](skills/gsd-path-ship/SKILL.md)
573
622
  owns the candidate, provenance, topology, publication, and recovery rules.
623
+ - `external-landing` is for an origin that is not GitHub.com, such as GitLab,
624
+ Gitea, or GitHub Enterprise Server, where a merge request is the only route
625
+ to `main`. It needs no forge CLI. Path publishes the bound branch at the
626
+ exact ship commit, does not push `main`, and returns `awaiting-merge` with
627
+ the required merge subject and body. You open the merge request in your
628
+ forge (for example with `glab`), let your pipeline run, and merge it with a
629
+ merge commit that has exactly that message. Do not squash, rebase, or
630
+ fast-forward. Your forge may delete the source branch after the merge. On
631
+ the next ship invocation, Path validates the two-parent merge on `main` and
632
+ creates the annotated milestone tag.
574
633
 
575
634
  Ship leaves the primary worktree and `STATE.branch` on the shipped local
576
635
  `gsd-path/M00N`; the router owns the later branch handoff. NNN always comes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-path",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "Disk-backed AI agent pipeline. Docs: DOCS.md (start here), QUICK.md, FULL.md, UPDATE.md",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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: