@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
@@ -17,10 +17,12 @@ import os
17
17
  import re
18
18
  import shutil
19
19
  import sys
20
+ from datetime import date
20
21
 
21
22
  # Runtime helpers must not modify their immutable installation.
22
23
  sys.dont_write_bytecode = True
23
24
  import tempfile
25
+ import traceback
24
26
  from pathlib import Path, PurePosixPath
25
27
  from typing import Optional, Sequence
26
28
 
@@ -52,7 +54,7 @@ def _load_pipeline_modules():
52
54
  try:
53
55
  from scripts import archive_milestone, isolation, pipeline_git, pipeline_state, state_checkpoint
54
56
  return archive_milestone, isolation, pipeline_git, pipeline_state, state_checkpoint
55
- except ModuleNotFoundError as error:
57
+ except ImportError as error: # a foreign `scripts` package
56
58
  if error.name not in {
57
59
  "scripts",
58
60
  "scripts.archive_milestone",
@@ -62,6 +64,13 @@ def _load_pipeline_modules():
62
64
  "scripts.state_checkpoint",
63
65
  }:
64
66
  raise
67
+ # A plain ImportError raised while a `scripts` module loaded is not a
68
+ # foreign package. A missing module falls back, as before.
69
+ if not isinstance(error, ModuleNotFoundError) and any(
70
+ frame.f_globals.get("__name__", "").startswith("scripts.")
71
+ for frame, _ in traceback.walk_tb(error.__traceback__)
72
+ ):
73
+ raise
65
74
  shared = Path(__file__).resolve().parents[2] / "gsd-path" / "scripts"
66
75
  sys.path.insert(0, str(shared))
67
76
  import archive_milestone
@@ -106,6 +115,16 @@ KINDS = ("checkpoint", "task", "member-task", "uncommitted-archive", "lookahead"
106
115
  PROMOTION_PREFIX = "router: promote lookahead milestone "
107
116
  ABANDON_PREFIX = "build: abandon milestone "
108
117
  INTEGRATE_PREFIX = "integrate: "
118
+ SHIP_LOG_ONLY_LINE = re.compile(
119
+ r"- (?P<date>\d{4}-\d{2}-\d{2}) — ship — (?P<event>[^\r\n]+)\n\Z"
120
+ )
121
+ SHIPMENT_RECORD_LINE = re.compile(
122
+ r"- (?P<date>\d{4}-\d{2}-\d{2}) — shipped — (?P<event>[^\r\n]+)\n\Z"
123
+ )
124
+ SHIPMENT_EVENTS = {
125
+ "archive preflight passed; shipment recorded",
126
+ "archive preflight passed; shipment recorded; program complete",
127
+ }
109
128
 
110
129
 
111
130
  class UndoError(RuntimeError):
@@ -375,6 +394,80 @@ def pending_transaction(repo: Path) -> Optional[dict[str, object]]:
375
394
  raise UndoError(str(error)) from error
376
395
 
377
396
 
397
+ def _valid_state_event_line(line: str, phase: str) -> bool:
398
+ pattern = SHIP_LOG_ONLY_LINE if phase == "ship" else SHIPMENT_RECORD_LINE
399
+ match = pattern.fullmatch(line)
400
+ if match is None:
401
+ return False
402
+ try:
403
+ event_date = date.fromisoformat(match.group("date"))
404
+ except ValueError:
405
+ return False
406
+ event = match.group("event")
407
+ return event_date.isoformat() == match.group("date") and event == " ".join(event.split())
408
+
409
+
410
+ def _owned_ship_log_events(
411
+ prepared_state: str,
412
+ current_state: str,
413
+ phase: object,
414
+ status: object,
415
+ ) -> Optional[str]:
416
+ """Return only ship log lines appended around a helper-owned archive event.
417
+
418
+ The prepared text must remain byte-for-byte unchanged. For shipped/done,
419
+ record-shipment changes only phase and status before appending its one
420
+ generated shipment event, so validate that exact prefix and event shape.
421
+ """
422
+ if (phase, status) == ("ship", "active"):
423
+ if current_state == prepared_state:
424
+ return ""
425
+ if not current_state.startswith(prepared_state):
426
+ return None
427
+ suffix = current_state[len(prepared_state):]
428
+ separator = "" if prepared_state.endswith("\n") else "\n"
429
+ if separator:
430
+ if not suffix.startswith(separator):
431
+ return None
432
+ suffix = suffix[len(separator):]
433
+ lines = suffix.splitlines(keepends=True)
434
+ if not lines or "".join(lines) != suffix:
435
+ return None
436
+ if not all(_valid_state_event_line(line, "ship") for line in lines):
437
+ return None
438
+ return separator + suffix
439
+
440
+ if (phase, status) != ("shipped", "done"):
441
+ return None
442
+ try:
443
+ shipped_prefix = pipeline_state._set_frontmatter(
444
+ prepared_state,
445
+ {"phase": "shipped", "status": "done"},
446
+ )
447
+ except PipelineStateError:
448
+ return None
449
+ if not current_state.startswith(shipped_prefix):
450
+ return None
451
+ suffix = current_state[len(shipped_prefix):]
452
+ separator = "" if shipped_prefix.endswith("\n") else "\n"
453
+ if separator:
454
+ if not suffix.startswith(separator):
455
+ return None
456
+ suffix = suffix[len(separator):]
457
+ lines = suffix.splitlines(keepends=True)
458
+ if not lines or "".join(lines) != suffix:
459
+ return None
460
+ shipment_event = lines[-1]
461
+ if not _valid_state_event_line(shipment_event, "shipped"):
462
+ return None
463
+ if SHIPMENT_RECORD_LINE.fullmatch(shipment_event).group("event") not in SHIPMENT_EVENTS:
464
+ return None
465
+ ship_events = lines[:-1]
466
+ if not all(_valid_state_event_line(line, "ship") for line in ship_events):
467
+ return None
468
+ return separator + "".join(ship_events) if ship_events else ""
469
+
470
+
378
471
  def _archive_metadata_error(
379
472
  repo: Path,
380
473
  state_fields: dict[str, object],
@@ -420,8 +513,13 @@ def _archive_metadata_error(
420
513
  if final_review.exists() or gap_reviews:
421
514
  reviewed_head, _ = archive_milestone.parse_final_review(archive_path)
422
515
  archive_milestone.validate_gap_reviews(archive_path, reviewed_head)
423
- if reviewed_head != _optional_rev(repo, "HEAD"):
424
- return "archive undo final review does not match current HEAD"
516
+ if not archive_milestone.reviewed_head_covers(
517
+ repo, reviewed_head, _optional_rev(repo, "HEAD")
518
+ ):
519
+ return (
520
+ "archive undo final review does not match current HEAD; "
521
+ f"{archive_milestone._common.RUNTIME_PIN_ALLOWANCE}"
522
+ )
425
523
  expected_inventory.add("review/FINAL.md")
426
524
  expected_inventory.update(
427
525
  path.relative_to(archive_path).as_posix() for path in gap_reviews
@@ -476,10 +574,14 @@ def _archive_metadata_error(
476
574
  "archive",
477
575
  configured,
478
576
  )
479
- current_state = (repo / ".project" / "STATE.md").read_text(encoding="utf-8")
577
+ current_state = (repo / ".project" / "STATE.md").read_bytes().decode("utf-8")
480
578
  phase = state_fields.get("phase")
481
579
  status = state_fields.get("status")
580
+ ship_events = _owned_ship_log_events(prepared_state, current_state, phase, status)
482
581
  if (phase, status) == ("ship", "active"):
582
+ if ship_events is None:
583
+ return "archive undo found unowned STATE.md changes"
584
+ prepared_state += ship_events
483
585
  if current_state != prepared_state:
484
586
  return "archive undo found unowned STATE.md changes"
485
587
  if ".project/ROADMAP.md" in dirty:
@@ -487,14 +589,13 @@ def _archive_metadata_error(
487
589
  return None
488
590
  if (phase, status) != ("shipped", "done"):
489
591
  return "archive undo requires ship/active or shipped/done"
592
+ if ship_events is None:
593
+ return "archive undo found unowned shipment STATE.md changes"
594
+ prepared_state += ship_events
490
595
 
491
596
  prepared = _state_from_text(prepared_state, "prepared archive STATE.md")
492
- events = (
493
- "archive preflight passed; shipment recorded",
494
- "archive preflight passed; shipment recorded; program complete",
495
- )
496
597
  rendered_states = []
497
- for event in events:
598
+ for event in SHIPMENT_EVENTS:
498
599
  try:
499
600
  _, _, rendered = _render_transition(
500
601
  prepared,
@@ -679,14 +780,15 @@ def _member_undo_parent(repo: Path, member: str, landing: str) -> str:
679
780
  bound = entry["branch"]
680
781
  if _run_git(checkout, "rev-parse", f"refs/heads/{bound}").stdout.strip() != landing:
681
782
  raise UndoError(f"member {member} {bound} moved past this landing")
682
- _require_member_unpublished(checkout, member, landing, bound)
783
+ _require_member_unpublished(repo, checkout, member, landing, bound)
683
784
  return _run_git(checkout, "rev-parse", f"{landing}^").stdout.strip()
684
785
 
685
786
 
686
- def _require_member_unpublished(checkout: Path, member: str, landing: str, bound: str) -> None:
687
- for ref, where in (("refs/remotes/origin/main", "origin/main"), (f"refs/remotes/origin/{bound}", f"origin/{bound}")):
787
+ def _require_member_unpublished(repo: Path, checkout: Path, member: str, landing: str, bound: str) -> None:
788
+ for branch in (isolation.member_default_branch(repo, member), bound):
789
+ ref = f"refs/remotes/origin/{branch}"
688
790
  if _run_git(checkout, "merge-base", "--is-ancestor", landing, ref, check=False).returncode == 0:
689
- raise UndoError(f"member {member} landing is already on {where}")
791
+ raise UndoError(f"member {member} landing is already on origin/{branch}")
690
792
 
691
793
 
692
794
  def _require_no_member_landing_journal(repo: Path) -> None:
@@ -701,7 +803,7 @@ def _undo_member_landing(repo: Path, transaction: dict[str, object]) -> None:
701
803
  checkout, _, entry = isolation._member_context(repo, member)
702
804
  tip = _run_git(checkout, "rev-parse", f"refs/heads/{entry['branch']}").stdout.strip()
703
805
  if tip == transaction["member_parent"]:
704
- _require_member_unpublished(checkout, member, str(transaction["landing"]), entry["branch"])
806
+ _require_member_unpublished(repo, checkout, member, str(transaction["landing"]), entry["branch"])
705
807
  return
706
808
  if tip != transaction["landing"]:
707
809
  raise UndoError(f"member {member} bound branch moved during undo")
@@ -768,7 +870,7 @@ def classify_undo(repo: Path) -> dict[str, object]:
768
870
  [f"an in-progress helper journal exists ({names}); resume that transaction first"]
769
871
  )
770
872
  if git.get("ancestor_of_origin_main"):
771
- return _blocked(["HEAD is an ancestor of origin/main; undo would rewrite shipped history"])
873
+ return _blocked(["HEAD is an ancestor of the origin default branch; undo would rewrite shipped history"])
772
874
 
773
875
  product = _product_dirty(dirty)
774
876
  if product:
@@ -988,7 +1090,7 @@ def _require_unpublished_reset(repo: Path, expected_branch: str) -> None:
988
1090
  if git.get("published"):
989
1091
  raise UndoError("HEAD became published on its recorded remote branch")
990
1092
  if git.get("ancestor_of_origin_main"):
991
- raise UndoError("HEAD became published through origin/main")
1093
+ raise UndoError("HEAD became published through the origin default branch")
992
1094
 
993
1095
 
994
1096
  def _remove_untracked_tree(repo: Path, relative: str) -> None:
@@ -198,10 +198,10 @@ def select_fetched_base(
198
198
  use_lookahead: bool,
199
199
  ) -> dict:
200
200
  root = repo.resolve()
201
- if not re.fullmatch(r"origin/[A-Za-z0-9][A-Za-z0-9._/-]*", remote_default):
201
+ if not remote_default.startswith("origin/") or not _common.valid_default_branch(
202
+ remote_default.removeprefix("origin/")
203
+ ):
202
204
  raise LookaheadError("remote default must be an origin branch")
203
- if ".." in remote_default or "//" in remote_default:
204
- raise LookaheadError("remote default is invalid")
205
205
  resolved = subprocess.run(
206
206
  ["git", "-C", str(root), "rev-parse", "--verify", f"{remote_default}^{{commit}}"],
207
207
  capture_output=True,
@@ -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:
@@ -24,9 +24,19 @@ RUNTIME_FILES = (
24
24
  "state_checkpoint.py", "state_promote.py", "discussion_validate.py", "integration.py",
25
25
  "build_state.py", "lean_verification.py", "check_docs_audit.py",
26
26
  "path_config.py", "model_policy.py", "loop_run.py", "members.py",
27
+ "review_findings.py", "task_context.py",
27
28
  )
28
29
  GUARDS = ("guard_hook.py", "git_guard.py")
29
30
 
31
+ # Optional owner-recorded provenance on .gsd-path/runtime.json declarations,
32
+ # e.g. {"source": "<npm version or package id>", "patch_ref": "<commit sha
33
+ # or patch list>", "note": "<owner label>"}. Provenance is display/audit
34
+ # metadata ONLY: it never joins the hashed manifest input (manifest_digest
35
+ # covers exactly {"version", "files"}), never acts as an alternate accepted
36
+ # digest, and never bypasses or weakens validate_runtime. A declaration
37
+ # without provenance is stock and stays exactly as valid as one with it.
38
+ PROVENANCE_KEYS = ("source", "patch_ref", "note")
39
+
30
40
  # Access denied and sharing violation: on Windows a scanner or indexer that
31
41
  # briefly opens a freshly written file blocks renaming the directory holding it.
32
42
  _WINDOWS_BUSY_ERRORS = (5, 32)
@@ -220,7 +230,7 @@ def recover_migration(project):
220
230
  journal.unlink()
221
231
 
222
232
 
223
- def operate(source, project, action, *, dry_run=False):
233
+ def operate(source, project, action, *, dry_run=False, provenance=None):
224
234
  if project.is_symlink() or (project / ".gsd-path").is_symlink():
225
235
  raise ValueError("unsafe project runtime directory")
226
236
  if action == "restore":
@@ -228,8 +238,17 @@ def operate(source, project, action, *, dry_run=False):
228
238
  publish(source, expected=pin, dry_run=dry_run, repair=True)
229
239
  return pin
230
240
  if action == "upgrade":
231
- status_runtime.declaration(project)
241
+ previous = status_runtime.declaration(project)
232
242
  pin = publish(source, dry_run=dry_run)
243
+ # Explicit provenance replaces the recorded object. Without new flags,
244
+ # an existing entry carries forward only while the pinned digest is
245
+ # unchanged — a different source drops stale hotfix metadata instead
246
+ # of stamping it onto bytes it no longer describes. Metadata only —
247
+ # it never changes the pinned digest.
248
+ if provenance is None and previous.get("digest") == pin["digest"]:
249
+ provenance = previous.get("provenance")
250
+ if provenance:
251
+ pin = {**pin, "provenance": provenance}
233
252
  if not dry_run:
234
253
  launcher = project / ".gsd-path/status_runtime.py"
235
254
  # Exact bytes: read_text() would turn CRLF into LF, leaving the