@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
@@ -8,20 +8,29 @@ canonical review files, and every skeptic file for this wave. It groups
8
8
  blocking findings by canonical criterion locator (`t001_ac2`, `sc3`,
9
9
  `t001_files`, `t001_interface_contract`), collapses true duplicate
10
10
  observations, selects the groups that need a skeptic, carries refutations
11
- forward across cycles, batches fix-task groups by disjoint file scope, keeps
12
- structural blockers out of skeptic accounting, and reports whether the cycle
13
- cap is reached. It never decides the wave verdict.
11
+ forward across cycles, batches fix-task groups by repo and disjoint file
12
+ scope, keeps structural blockers out of skeptic accounting, and reports
13
+ whether the cycle cap is reached. It never decides the wave verdict.
14
14
  """
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
18
  import argparse
19
+ import functools
19
20
  import json
20
21
  import re
21
22
  import sys
23
+
24
+ # Runtime helpers must not modify their immutable installation.
25
+ sys.dont_write_bytecode = True
22
26
  from pathlib import Path
23
27
  from typing import Dict, List, Optional, Sequence
24
28
 
29
+ try:
30
+ from . import _common
31
+ except ImportError: # installed standalone helper
32
+ import _common
33
+
25
34
  DEFAULT_PROJECT_DIR = ".project"
26
35
  DEFAULT_MAX_REVIEW_CYCLES = 3
27
36
  CONFIG_KEYS = ("max_review_cycles", "wave_budget", "review_panel", "finding_skeptics")
@@ -143,15 +152,25 @@ def _frontmatter_list(frontmatter: str, key: str) -> List[str]:
143
152
 
144
153
 
145
154
  def _numbered_items(section: Optional[str]) -> List[str]:
146
- items: List[str] = []
155
+ """Numbered criteria; each also carries its text without sub-bullet markers (issue #392)."""
147
156
  if section is None:
148
- return items
157
+ return []
158
+ bare = _numbered_texts(_common.without_sub_bullet_markers(section))
159
+ return [_common.Criterion(text, other) for text, other in zip(_numbered_texts(section), bare)]
160
+
161
+
162
+ def _numbered_texts(section: str) -> List[str]:
163
+ items: List[str] = []
164
+ open_item = False
149
165
  for line in _strip_comments(section).splitlines():
150
166
  match = NUMBERED_ITEM.match(line.strip())
151
167
  if match:
152
168
  items.append(_normalize(match.group(2)))
153
- elif items and line.startswith(" ") and line.strip():
169
+ open_item = True
170
+ elif open_item and line[:1].isspace() and line.strip():
154
171
  items[-1] = _normalize(items[-1] + " " + line.strip())
172
+ elif line.strip():
173
+ open_item = False
155
174
  return items
156
175
 
157
176
 
@@ -183,9 +202,11 @@ def load_wave_tasks(project: Path, wave: int) -> Dict[str, dict]:
183
202
  if not task_wave or int(task_wave.group(1)) != wave:
184
203
  continue
185
204
  body = parts[2]
205
+ member = re.search(r"(?m)^repo:[ \t]*(?P<name>[^\s#]+)", frontmatter)
186
206
  tasks[named.group("id")] = {
187
207
  "id": named.group("id"),
188
208
  "path": str(path),
209
+ "repo": member.group("name").strip("\"'") if member else None, # None is the coordinator
189
210
  "files": _frontmatter_list(frontmatter, "files"),
190
211
  "acceptance": _numbered_items(_section(body, "Acceptance criteria")),
191
212
  "interface": [
@@ -270,17 +291,20 @@ def parse_lens(text: str, lens: str, tasks: Dict[str, dict]) -> dict:
270
291
  continue
271
292
  zones[zone].append(line)
272
293
  for observation in _fail_items(zones["criteria"]):
273
- criterion = _criterion_of(observation)
274
- locator = None
275
- if criterion in task["acceptance"]:
276
- locator = f"{task_id.lower()}_ac{task['acceptance'].index(criterion) + 1}"
277
- elif criterion in task["interface"]:
278
- locator = f"{task_id.lower()}_interface_contract"
279
- if locator is None:
294
+ # Issue #392: match as the wave gate does, then report the task's own text.
295
+ criteria = task["acceptance"] + task["interface"]
296
+ quoted = _common.quoted_criterion(observation, criteria)
297
+ if quoted is None:
280
298
  problems.append(
281
- f"{lens} lens {task_id} finding names no task criterion: {criterion}"
299
+ f"{lens} lens {task_id} finding names no task criterion: "
300
+ f"{_criterion_of(observation)}"
282
301
  )
283
302
  continue
303
+ criterion = criteria[quoted[0]]
304
+ if quoted[0] < len(task["acceptance"]):
305
+ locator = f"{task_id.lower()}_ac{quoted[0] + 1}"
306
+ else:
307
+ locator = f"{task_id.lower()}_interface_contract"
284
308
  findings.append(
285
309
  {"locator": locator, "criterion": criterion, "task": task_id,
286
310
  "lens": lens, "observation": observation}
@@ -433,21 +457,27 @@ def _earlier_lens_locators(project: Path, wave: int, cycle: int, depth: str, tas
433
457
 
434
458
 
435
459
  def _fix_batches(groups: Sequence[dict], tasks: Dict[str, dict]) -> List[dict]:
436
- """One batch per disjoint file scope: groups whose task files overlap share a batch."""
460
+ """One batch per repo and disjoint file scope: groups whose task files overlap in one repo share a batch.
461
+
462
+ A member batch names its `repo`; a coordinator batch has no such key.
463
+ """
437
464
 
438
465
  batches: List[dict] = []
439
466
  for group in groups:
440
- files = sorted({path for task_id in group["tasks"] for path in tasks.get(task_id, {}).get("files", [])})
441
- merged = {"locators": [group["locator"]], "files": files}
442
- keep: List[dict] = []
443
- for batch in batches:
444
- if set(batch["files"]) & set(files):
445
- merged["locators"] = batch["locators"] + merged["locators"]
446
- merged["files"] = sorted(set(batch["files"]) | set(merged["files"]))
447
- else:
448
- keep.append(batch)
449
- keep.append(merged)
450
- batches = keep
467
+ owners = [tasks.get(task_id, {}) for task_id in group["tasks"]]
468
+ for repo in sorted({task.get("repo") or "" for task in owners}) or [""]:
469
+ files = sorted({path for task in owners if (task.get("repo") or "") == repo
470
+ for path in task.get("files", [])})
471
+ merged = {"locators": [group["locator"]], "files": files, **({"repo": repo} if repo else {})}
472
+ keep: List[dict] = []
473
+ for batch in batches:
474
+ if batch.get("repo") == merged.get("repo") and _common.overlapping_paths(batch["files"], files):
475
+ merged["locators"] = batch["locators"] + merged["locators"]
476
+ merged["files"] = sorted(set(batch["files"]) | set(merged["files"]))
477
+ else:
478
+ keep.append(batch)
479
+ keep.append(merged)
480
+ batches = keep
451
481
  return batches
452
482
 
453
483
 
@@ -604,7 +634,7 @@ def repair_evidence(repo: Path, project_dir: str, wave: int, cycle: int, task_id
604
634
  )["tasks"][0]
605
635
  if proof["verdict"] != "recovered":
606
636
  raise ReviewFindingsError("repair requires a proven landing")
607
- base, commit = proof["base"], proof["commit"]
637
+ base = proof["base"] # in the coordinator, where the source review is
608
638
  findings = _section(text, "Review findings") or ""
609
639
  headings = list(re.finditer(r"(?m)^### ([a-z0-9_]+)\s*$", findings))
610
640
  locators = [heading.group(1) for heading in headings]
@@ -613,10 +643,28 @@ def repair_evidence(repo: Path, project_dir: str, wave: int, cycle: int, task_id
613
643
  source = compute(repo, project_dir, wave, cycle)
614
644
  if source["structural_blockers"]:
615
645
  raise ReviewFindingsError("source findings have structural blockers")
646
+ member = fields.get("repo") # a finding in two repos has one batch per repo with the same locators
616
647
  batches = [batch for batch in source["fix_batches"]
617
- if set(batch["locators"]) == set(locators)]
648
+ if batch.get("repo") == member and set(batch["locators"]) == set(locators)]
618
649
  if len(batches) != 1:
619
650
  raise ReviewFindingsError("repair must carry one eligible source finding batch")
651
+ # A member task's product is in the member repository: its commits, its bound branch, its Verify rows.
652
+ where, tip = repo, [] # the coordinator product is judged in the working tree
653
+ if member:
654
+ where, _, lock = isolation._member_context(repo, member)
655
+ tip = [f"refs/heads/{lock['branch']}"]
656
+
657
+ def entry(landing: dict, path: Path, paths: List[str]) -> dict:
658
+ """A proven landing as the receipt shows it; a member landing adds its commits in the member."""
659
+ item = {"task": landing["task_id"], "base": landing["base"], "commit": landing["commit"], "files": paths}
660
+ if member:
661
+ item.update(member_base=isolation.task_frontmatter(_read(path, "task"))[0]["member_base"],
662
+ member_commit=landing["landing"])
663
+ return item
664
+
665
+ def at(item: dict, point: str = "commit") -> str:
666
+ return item.get(f"member_{point}", item[point])
667
+
620
668
  groups = [group for group in source["groups"] if group["locator"] in locators]
621
669
  for index, heading in enumerate(headings):
622
670
  end = headings[index + 1].start() if index + 1 < len(headings) else len(findings)
@@ -627,22 +675,101 @@ def repair_evidence(repo: Path, project_dir: str, wave: int, cycle: int, task_id
627
675
  raise ReviewFindingsError("repair does not carry the exact criterion and observations")
628
676
  frontmatter = text.split("---", 2)[1]
629
677
  files = _frontmatter_list(frontmatter, "files")
630
- originals = sorted({task for group in groups for task in group["tasks"]})
678
+ sources = load_wave_tasks(project, wave)
679
+ originals = sorted({task for group in groups for task in group["tasks"] if sources[task]["repo"] == member})
680
+ # A repair names the files it needs. They can be fewer than the batch files, and a plan repair
681
+ # can add a fix site that the source tasks did not declare.
631
682
  if (task_id in originals or not set(originals) <= set(_frontmatter_list(frontmatter, "deps"))
632
- or set(files) != set(batches[0]["files"])):
683
+ or not files):
633
684
  raise ReviewFindingsError("repair dependencies/files do not match its source batch")
634
- sources = load_wave_tasks(project, wave)
685
+ repair = {"task_file": repair_path.relative_to(repo).as_posix(), **entry(proof, repair_path, files),
686
+ **({"member": member} if member else {})}
687
+
688
+ def differ(start: str, end: List[str], paths: List[str]) -> bool:
689
+ return bool(isolation.run_git(where, "diff", "--quiet", start, *end, "--", *paths).returncode)
690
+
691
+ def before(start: str, end: str) -> bool:
692
+ return not isolation.run_git(where, "merge-base", "--is-ancestor", start, end).returncode
693
+
694
+ @functools.lru_cache(maxsize=None)
695
+ def linked() -> tuple:
696
+ """Landed repairs of the same findings that `deps` join to this repair, in landing order.
697
+
698
+ A linked task is done, is in the same repo, carries a finding of this repair in its
699
+ `## Review findings`, and depends on this repair or on a linked task, or the reverse.
700
+ """
701
+ tasks, carried = {task_id: (repair_path, fields)}, {}
702
+ for path in (project / "tasks").glob("T*-*.md"):
703
+ other = _read(path, "task")
704
+ other_fields = isolation.task_frontmatter(other)[0] or {}
705
+ tasks[str(other_fields.get("id"))] = (path, other_fields)
706
+ blocks = re.split(r"(?m)^### ([a-z0-9_]+)\s*$", _section(other, "Review findings") or "")
707
+ carried[str(other_fields.get("id"))] = {
708
+ (locator, _normalize(line)) for locator, block in zip(blocks[1::2], blocks[2::2])
709
+ for line in re.findall(r"(?m)^Criterion:(.*)$", block)}
710
+
711
+ def traced(finding: tuple, seen: frozenset = frozenset()) -> set:
712
+ """The finding as it started: its locator and criterion in the review of the source tasks.
713
+
714
+ A finding on an acceptance criterion of a fix task (`fix-tasks` copies the carried criteria
715
+ there) is the finding with the same criterion that this fix task carries. Two findings with
716
+ the same criterion text and other locators stay two findings.
717
+ """
718
+ owner = re.fullmatch(r"t(\d{3})_ac\d+", finding[0])
719
+ sources = () if not owner else {
720
+ item for item in carried.get(f"T{owner.group(1)}", ()) if item[1] == finding[1]} - seen
721
+ return {origin for item in sources for origin in traced(item, seen | {finding})} or {finding}
722
+
723
+ def origins(other: str) -> set:
724
+ return {origin for finding in carried.get(other, ()) for origin in traced(finding)}
725
+
726
+ repairs = {other: task for other, task in tasks.items() if other == task_id or (
727
+ task[1].get("status") == "done" and task[1].get("repo") == member
728
+ and origins(task_id) & origins(other))}
729
+ joined, more = set(), {task_id}
730
+ while more:
731
+ joined |= more
732
+ more = {other for other, (_, other_fields) in repairs.items() if other not in joined and (
733
+ joined & set(other_fields.get("deps") or [])
734
+ or any(other in (repairs[item][1].get("deps") or []) for item in joined))}
735
+ others = sorted(joined - {task_id})
736
+ proofs = isolation.verify_landed_task_files(
737
+ repo, [repairs[other][0] for other in others], f"{project_dir}/tasks", head)["tasks"] if others else []
738
+ links = []
739
+ for landing in proofs:
740
+ path, other_fields = repairs[landing["task_id"]]
741
+ if landing["verdict"] == "recovered":
742
+ links.append({"task_file": path.relative_to(repo).as_posix(),
743
+ **entry(landing, path, list(other_fields.get("files") or []))})
744
+ return tuple(sorted(links, key=lambda link: int(isolation.git_output(where, "rev-list", "--count", at(link)))))
745
+
746
+ def linked_changes(start: str, end: List[str], paths: List[str]) -> Optional[List[dict]]:
747
+ """The linked repairs whose landings are every change to `paths` from `start` to `end`, else None."""
748
+ if not differ(start, end, paths):
749
+ return []
750
+ links = [link for link in linked()
751
+ if before(start, f"{at(link)}^") and (not end or before(at(link), end[0]))]
752
+ for link in links:
753
+ if differ(start, [f"{at(link)}^"], paths):
754
+ return None
755
+ start = at(link)
756
+ return None if differ(start, end, paths) else links
757
+
635
758
  original_paths = [Path(sources[task]["path"]) for task in originals]
636
759
  original_proofs = isolation.verify_landed_task_files(
637
760
  repo, original_paths, f"{project_dir}/tasks", head
638
761
  )["tasks"]
639
- for original in original_proofs:
640
- original_id = original["task_id"]
641
- if (original["verdict"] != "recovered"
642
- or isolation.run_git(repo, "merge-base", "--is-ancestor", original["commit"], base).returncode
643
- or isolation.run_git(repo, "diff", "--quiet", original["commit"], base,
644
- "--", *sources[original_id]["files"]).returncode):
762
+ landed, earlier = [], set()
763
+ for landing in original_proofs:
764
+ links = None
765
+ if landing["verdict"] == "recovered":
766
+ original = entry(landing, Path(sources[landing["task_id"]]["path"]), sources[landing["task_id"]]["files"])
767
+ if before(at(original), at(repair, "base")):
768
+ links = linked_changes(at(original), [at(repair, "base")], original["files"])
769
+ if links is None:
645
770
  raise ReviewFindingsError("repair base contains unlinked changes to original product")
771
+ landed.append(original)
772
+ earlier.update(link["task"] for link in links)
646
773
  source_paths = list(review_paths(project, wave, cycle, source["depth"]).values())
647
774
  source_paths.extend(Path(group["skeptic"]["path"]) for group in groups if group["skeptic"])
648
775
  for path in set(source_paths):
@@ -650,19 +777,28 @@ def repair_evidence(repo: Path, project_dir: str, wave: int, cycle: int, task_id
650
777
  at_base = isolation.run_git(repo, "show", f"{base}:{relative}")
651
778
  if at_base.returncode or at_base.stdout != _read(path, "source review"):
652
779
  raise ReviewFindingsError("source review must be unchanged and precede the repair")
653
- verification = build_state.verify_lookup(str(repo), _common.task_verify_command(text), commit)
780
+ # A later linked repair supersedes this one: the last one is the product at the tip, and its
781
+ # landing and its Verify are the evidence. The reviewer applies every patch of the chain, so the
782
+ # files of every later linked repair must also change only through the landings of the chain.
783
+ later = linked_changes(at(repair), tip, files)
784
+ chain_files = sorted({*files, *(path for link in later or [] for path in link["files"])})
785
+ last = later[-1] if later else repair
786
+ verification = build_state.verify_lookup(
787
+ str(repo), _common.task_verify_command(_read(repo / last["task_file"], "repair task")), at(last), member)
654
788
  if not verification["reuse"]:
655
789
  raise ReviewFindingsError("repair has no reusable passing Verify at its landing")
656
- if isolation.run_git(repo, "diff", "--quiet", commit, "--", *files).returncode:
790
+ if later is None or (later and linked_changes(at(repair), tip, chain_files) is None):
657
791
  raise ReviewFindingsError("product changed after the repair landing")
792
+ # A parallel landing can have a parent that is newer than the repair base. The reviewer builds the
793
+ # product from the base, so no other landing between the two can change a file of the chain.
794
+ if differ(at(repair, "base"), [f"{at(repair)}^"], chain_files):
795
+ raise ReviewFindingsError("product changed between the repair base and the repair landing")
658
796
  return {
659
797
  "status": "ok", "command": "repair-evidence", "head": head,
660
798
  "source": {"wave": wave, "cycle": cycle}, "groups": groups,
661
- "originals": [{"task": item["task_id"], "base": item["base"],
662
- "commit": item["commit"], "files": sources[item["task_id"]]["files"]}
663
- for item in original_proofs],
664
- "repair": {"task": task_id, "task_file": repair_path.relative_to(repo).as_posix(),
665
- "base": base, "commit": commit, "files": files},
799
+ "originals": landed, "repair": repair,
800
+ **({"earlier_repairs": [link for link in linked() if link["task"] in earlier]} if earlier else {}),
801
+ **({"later_repairs": later, "chain_files": chain_files} if later else {}),
666
802
  "verification": verification,
667
803
  "verdict": "evidence-ready; reviewer judges the original unchanged criteria",
668
804
  }
@@ -252,7 +252,12 @@ def activate_milestone(text: str, milestone: str) -> str:
252
252
  return "".join(lines)
253
253
 
254
254
 
255
- def reslice(before: str, candidate: str, phase: str, selected: str) -> str:
255
+ def _entry_identity(block: dict) -> tuple:
256
+ """Heading, Status, Depends on, Archive and Integrated of one entry."""
257
+ return block["id"], block["title"], {name: value for name, (_, value) in block["fields"].items()}
258
+
259
+
260
+ def reslice(before: str, candidate: str, phase: str, selected: str, amend_active: bool = False) -> str:
256
261
  old_blocks = roadmap_blocks(before)
257
262
  before_lines = before.splitlines(keepends=True)
258
263
  candidate_lines = candidate.splitlines(keepends=True)
@@ -264,6 +269,9 @@ def reslice(before: str, candidate: str, phase: str, selected: str) -> str:
264
269
  new = milestone_block(candidate, slug)
265
270
  if status == "shipped":
266
271
  changed = roadmap_contract(before, slug) != roadmap_contract(candidate, slug)
272
+ elif status == "active" and amend_active:
273
+ # Intent is not approved: the scope may change, the identity may not.
274
+ changed = _entry_identity(block) != _entry_identity(new)
267
275
  else:
268
276
  changed = before_lines[block["start"]:block["end"]] != candidate_lines[new["start"]:new["end"]]
269
277
  if changed:
@@ -18,14 +18,14 @@ from pathlib import Path, PurePosixPath
18
18
  from typing import Mapping, Optional, Sequence
19
19
 
20
20
  if __package__:
21
- from .isolation import IsolationError, checkpoint as isolation_checkpoint, require_commit, require_full_sha, PROJECT_ENTRIES
21
+ from .isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
22
22
  else:
23
23
  try:
24
- from isolation import IsolationError, checkpoint as isolation_checkpoint, require_commit, require_full_sha, PROJECT_ENTRIES
24
+ from isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
25
25
  except ModuleNotFoundError as error: # pragma: no cover - package imports used by tests
26
26
  if error.name != "isolation":
27
27
  raise
28
- from scripts.isolation import IsolationError, checkpoint as isolation_checkpoint, require_commit, require_full_sha, PROJECT_ENTRIES
28
+ from scripts.isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
29
29
 
30
30
 
31
31
  if __package__:
@@ -164,9 +164,16 @@ def _approval_details(
164
164
  raise PipelineStateError(f"unsupported approval kind: {kind}")
165
165
 
166
166
 
167
- def _reslice_roadmap(before: str, candidate: str, state: PipelineState, selected: str) -> str:
167
+ def _reslice_roadmap(
168
+ repo: Path, before: str, candidate: str, state: PipelineState, state_text: str, selected: str,
169
+ ) -> str:
170
+ # Define may amend its active entry until intent approval. A return to
171
+ # define after that approval always leaves an active recovery record.
172
+ amend_active = state.phase == "define" and not (
173
+ pipeline_state._build_recovery().context(repo, state_text) or {}
174
+ ).get("active")
168
175
  try:
169
- return roadmap.reslice(before, candidate, state.phase, selected)
176
+ return roadmap.reslice(before, candidate, state.phase, selected, amend_active)
170
177
  except roadmap.RoadmapError as error:
171
178
  raise PipelineStateError(str(error)) from error
172
179
 
@@ -223,6 +230,10 @@ def _approval_journal(repo: Path, journal: Mapping[str, object]) -> dict[str, ob
223
230
  expected_keys.add("baseline_before")
224
231
  if not isinstance(journal.get("baseline_before"), str):
225
232
  raise PipelineStateError("checkpoint journal has an invalid re-slice baseline")
233
+ if "recovery_reviews" in journal:
234
+ expected_keys.add("recovery_reviews")
235
+ if journal.get("kind") != "plan" or not isinstance(journal["recovery_reviews"], dict):
236
+ raise PipelineStateError("checkpoint journal has an invalid recovery review report")
226
237
  if set(journal) != expected_keys:
227
238
  raise PipelineStateError("checkpoint journal has invalid fields")
228
239
  if journal.get("schema") != CHECKPOINT_SCHEMA or journal.get("repo") != str(repo):
@@ -307,7 +318,8 @@ def _approval_journal(repo: Path, journal: Mapping[str, object]) -> dict[str, ob
307
318
  if journal["roadmap_path"] != ".project/ROADMAP.md":
308
319
  raise PipelineStateError("checkpoint journal has an invalid ROADMAP path")
309
320
  expected_roadmap = _reslice_roadmap(
310
- str(journal["baseline_before"]), str(journal["roadmap_before"]), before, str(selected),
321
+ repo, str(journal["baseline_before"]), str(journal["roadmap_before"]), before,
322
+ str(journal["state_before"]), str(selected),
311
323
  ) if journal["kind"] == "roadmap-reslice" else _activate_roadmap_milestone(
312
324
  str(journal["roadmap_before"]),
313
325
  str(selected),
@@ -423,7 +435,7 @@ def _resume_checkpoint_locked(
423
435
  raise PipelineStateError("approval checkpoint returned the wrong commit")
424
436
  _unlink_checkpoint_journal(journal_path)
425
437
  state = pipeline_state._state_from_text(str(transaction["state_after"]))
426
- return {
438
+ approved: dict[str, object] = {
427
439
  "schema": CHECKPOINT_SCHEMA,
428
440
  "status": "approved",
429
441
  "kind": transaction["kind"],
@@ -432,6 +444,9 @@ def _resume_checkpoint_locked(
432
444
  "paths": committed.get("paths"),
433
445
  "state": state.json(),
434
446
  }
447
+ if "recovery_reviews" in transaction:
448
+ approved["recovery_reviews"] = transaction["recovery_reviews"]
449
+ return approved
435
450
 
436
451
 
437
452
  def _validate_plan_briefs(repo: Path, kind: str, project_dir: str) -> None:
@@ -444,14 +459,14 @@ def _validate_plan_briefs(repo: Path, kind: str, project_dir: str) -> None:
444
459
  # Pre-Git approval defers base-dependent checks to build.
445
460
  return
446
461
  if __package__:
447
- from .check_task_briefs import BriefError, _member_bases, validate_task_briefs
462
+ from .check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
448
463
  else:
449
464
  try:
450
- from check_task_briefs import BriefError, _member_bases, validate_task_briefs
465
+ from check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
451
466
  except ModuleNotFoundError as error: # pragma: no cover - package imports
452
467
  if error.name != "check_task_briefs":
453
468
  raise
454
- from scripts.check_task_briefs import BriefError, _member_bases, validate_task_briefs
469
+ from scripts.check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
455
470
  if __package__:
456
471
  from . import check_handoffs
457
472
  else:
@@ -462,28 +477,10 @@ def _validate_plan_briefs(repo: Path, kind: str, project_dir: str) -> None:
462
477
  raise
463
478
  from scripts import check_handoffs
464
479
  try:
465
- tasks, dependency_files = check_handoffs.plan_brief_inputs(repo, project_dir)
466
- landed_bases = {}
467
480
  member_base = _member_bases(repo)
468
- for task_id, text in tasks.items():
469
- if check_handoffs._task_scalar(text, task_id, "status") != "done":
470
- continue
471
- agent = check_handoffs._task_scalar(text, task_id, "agent")
472
- if agent in {"", "null"}:
473
- raise BriefError(f"{task_id} landed task has no recorded agent")
474
- member = check_handoffs._strict_frontmatter(text, task_id).get("repo")
475
- base_repo = repo
476
- if member is not None:
477
- located = member_base(member) if isinstance(member, str) and member else None
478
- if located is None:
479
- raise BriefError(f"repo: names no member in MEMBERS.md: {member}")
480
- base_repo = located[0]
481
- recorded_base = check_handoffs._task_scalar(text, task_id, "base")
482
- try:
483
- landed_bases[task_id] = require_commit(base_repo, require_full_sha(recorded_base))
484
- except IsolationError as error:
485
- raise BriefError(f"{task_id} landed task has invalid historical base: {error}") from error
486
- dependency_files[task_id] = set()
481
+ tasks, dependency_files, landed_bases = plan_validation_bases(
482
+ repo, project_dir, member_base, initial=True
483
+ )
487
484
  validate_task_briefs(
488
485
  repo, head.stdout.strip(), f"{project_dir}/tasks",
489
486
  dependency_files=dependency_files, landed_bases=landed_bases,
@@ -581,7 +578,10 @@ def checkpoint_approval(
581
578
  f"{expected_head}^{{commit}}",
582
579
  ).stdout.strip()
583
580
  if resolved_head != expected_head:
584
- raise PipelineStateError("expected-head must be an exact full commit SHA")
581
+ raise PipelineStateError(
582
+ "expected-head must be an exact full commit SHA; "
583
+ "resolve a ref or a short SHA with git rev-parse first"
584
+ )
585
585
  if pipeline_state._run_git(resolved, "rev-parse", "HEAD").stdout.strip() != expected_head:
586
586
  raise PipelineStateError("approval must start at expected-head")
587
587
  active_state, _, _ = pipeline_state.load_state(resolved)
@@ -596,8 +596,9 @@ def checkpoint_approval(
596
596
  state, state_before, state_path = pipeline_state.load_state(resolved, project_dir)
597
597
  changes, event, subject, body = _approval_details(kind, state, selected_milestone)
598
598
  _validate_plan_briefs(resolved, kind, project_dir)
599
+ recovery_reviews = None
599
600
  if kind == "plan" and project_dir == ".project":
600
- pipeline_state._build_recovery().restore_unchanged_reviews(resolved)
601
+ recovery_reviews = pipeline_state._build_recovery().restore_unchanged_reviews(resolved)
601
602
  expected = {
602
603
  "phase": state.phase,
603
604
  "status": state.status,
@@ -629,7 +630,7 @@ def checkpoint_approval(
629
630
  if kind == "roadmap-reslice":
630
631
  baseline_before = pipeline_state._read_real_file(project / "ROADMAP.before-reslice.md", "re-slice baseline")
631
632
  roadmap_after = _reslice_roadmap(
632
- baseline_before, roadmap_before, state, selected_milestone,
633
+ resolved, baseline_before, roadmap_before, state, state_before, selected_milestone,
633
634
  ) if kind == "roadmap-reslice" else _activate_roadmap_milestone(
634
635
  roadmap_before,
635
636
  selected_milestone,
@@ -659,6 +660,8 @@ def checkpoint_approval(
659
660
  }
660
661
  if baseline_before is not None:
661
662
  journal["baseline_before"] = baseline_before
663
+ if recovery_reviews is not None:
664
+ journal["recovery_reviews"] = recovery_reviews
662
665
  pipeline_state._write_json(journal_path, journal)
663
666
  return _resume_checkpoint_locked(resolved, project, journal_path, journal)
664
667
 
@@ -740,8 +743,8 @@ def defer_approval(
740
743
  if kind == "roadmap-reslice":
741
744
  baseline = project / "ROADMAP.before-reslice.md"
742
745
  roadmap_after = _reslice_roadmap(
743
- pipeline_state._read_real_file(baseline, "re-slice baseline"),
744
- roadmap_before, state, selected_milestone,
746
+ resolved, pipeline_state._read_real_file(baseline, "re-slice baseline"),
747
+ roadmap_before, state, state_before, selected_milestone,
745
748
  )
746
749
  else:
747
750
  roadmap_after = _activate_roadmap_milestone(roadmap_before, selected_milestone)
@@ -377,17 +377,22 @@ def _prepare_promotion(
377
377
  head = pipeline_state._run_git(repo, "rev-parse", "HEAD").stdout.strip()
378
378
  if head != base:
379
379
  raise PipelineStateError(f"promotion must start at base SHA: {head} != {base}")
380
- remote = pipeline_state._run_git(repo, "rev-parse", "--verify", "origin/main^{commit}").stdout.strip()
380
+ active_state, active_text, _ = pipeline_state.load_state(repo)
381
+ remote_default = f"origin/{active_state.default_branch}"
382
+ remote = pipeline_state._run_git(
383
+ repo, "rev-parse", "--verify", f"{remote_default}^{{commit}}"
384
+ ).stdout.strip()
381
385
  if remote != base:
382
386
  raise PipelineStateError(
383
- f"base SHA is not current origin/main: {base} != {remote}"
387
+ f"base SHA is not current {remote_default}: {base} != {remote}"
384
388
  )
385
389
  if not pipeline_state._is_ancestor(repo, landing, base):
386
- raise PipelineStateError("landing is not an ancestor of the current main base")
390
+ raise PipelineStateError(
391
+ f"landing is not an ancestor of the current {active_state.default_branch} base"
392
+ )
387
393
  if pipeline_state._run_git(repo, "status", "--porcelain", "--untracked-files=all").stdout:
388
394
  raise PipelineStateError("promotion must start from a clean worktree")
389
395
 
390
- active_state, active_text, _ = pipeline_state.load_state(repo)
391
396
  if active_state.phase != "shipped" or active_state.status != "done":
392
397
  raise PipelineStateError("promotion requires active STATE shipped/done")
393
398
  if active_state.milestone is None:
@@ -456,6 +461,12 @@ def _prepare_promotion(
456
461
  raise PipelineStateError(
457
462
  "lookahead integration_default does not match the active project"
458
463
  )
464
+ if next_state.default_branch != active_state.default_branch:
465
+ raise PipelineStateError(
466
+ "lookahead default_branch does not match the active project: copy "
467
+ f"`default_branch: {active_state.default_branch}` from .project/STATE.md "
468
+ "into .project/next/STATE.md"
469
+ )
459
470
  if next_state.milestone != milestone:
460
471
  raise PipelineStateError(
461
472
  f"requested milestone {milestone} != next STATE milestone {next_state.milestone}"
@@ -1,23 +1,27 @@
1
1
  #!/usr/bin/env python3
2
+ # gsd-path project runtime
2
3
  """Render a read-only coder intent view from the approved task and intent."""
3
4
 
4
5
  import argparse
5
6
  import hashlib
6
7
  import re
7
8
  import sys
9
+ sys.dont_write_bytecode = True
8
10
  from pathlib import Path
9
11
 
10
12
  try:
11
13
  from scripts import check_handoffs as contracts
14
+ from scripts import _common
12
15
  except ImportError:
13
16
  import check_handoffs as contracts
17
+ import _common
14
18
 
15
19
 
16
20
  def render(repo: Path, task: Path) -> str:
17
21
  intent_path = repo.resolve() / '.project/intent/INTENT.md'
18
22
  task = task.resolve()
19
- intent = intent_path.read_bytes().decode('utf-8')
20
- task_text = task.read_bytes().decode('utf-8')
23
+ intent = _common.read_user_text(intent_path)
24
+ task_text = _common.read_user_text(task)
21
25
  owned = contracts._owned_criteria(task_text, task.stem)
22
26
  criteria = contracts._success_criteria(intent)
23
27
  unknown = set(owned) - set(criteria)
@@ -40,7 +44,14 @@ def render(repo: Path, task: Path) -> str:
40
44
  if simple:
41
45
  before, after = intent[:section.start('body')], intent[section.end('body'):]
42
46
  # Corrections and task interfaces can explicitly refer to another SC.
43
- required = set(owned) | set(re.findall(r'\bSC\d+\b', before + after + task_text))
47
+ # The spec-probe tables name every SC by design, so they select nothing.
48
+ scanned = re.sub(r'(?ms)^## (?:Edge coverage|Prohibitions)[ \t]*\n.*?(?=^## |\Z)', '',
49
+ before + after)
50
+ required = set(owned) | set(re.findall(r'\bSC\d+\b', scanned + task_text))
51
+ # An owned criterion's edge or prohibition ruled into another SC needs that SC's text.
52
+ for rows in (contracts.edge_coverage(intent, criteria), contracts.prohibitions(intent, criteria)):
53
+ required |= {row['target'] for row in (rows or {}).values()
54
+ if row['target'] and row['criterion'] in owned}
44
55
  selected = ''.join(chunk for item, chunk in zip(items, chunks) if f'SC{item[1]}' in required)
45
56
  projected = before + '\n' + (selected or '- No criteria assigned.\n\n') + after
46
57
  mode = 'owned-criteria'