@opengsd/gsd-path 1.3.1 → 1.4.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 (434) hide show
  1. package/AGENTS.md +2 -207
  2. package/CHANGELOG.md +281 -0
  3. package/DOCS.md +13 -7
  4. package/FULL.md +9 -3
  5. package/GUIDE.md +1 -1
  6. package/HOOKS.md +9 -6
  7. package/QUICK.md +4 -1
  8. package/README.md +63 -11
  9. package/RUNTIME.md +1 -1
  10. package/UPDATE.md +57 -8
  11. package/WORKFLOW.md +22 -6
  12. package/package.json +9 -8
  13. package/scripts/_common.py +472 -19
  14. package/scripts/archive_milestone.py +146 -73
  15. package/scripts/bootstrap_repository.py +8 -5
  16. package/scripts/build_recovery.py +4 -1
  17. package/scripts/build_state.py +66 -23
  18. package/scripts/check_docs_audit.py +14 -3
  19. package/scripts/check_handoffs.py +116 -20
  20. package/scripts/check_task_briefs.py +60 -8
  21. package/scripts/check_update.py +2 -2
  22. package/scripts/core_hook_gate.py +10 -1
  23. package/scripts/core_hook_settings.py +11 -4
  24. package/scripts/detect_project.py +345 -15
  25. package/scripts/discussion_records.py +19 -13
  26. package/scripts/discussion_validate.py +66 -21
  27. package/scripts/dispatch_driver.py +111 -26
  28. package/scripts/git_guard.py +116 -29
  29. package/scripts/guard_hook.py +151 -29
  30. package/scripts/install.mjs +54 -38
  31. package/scripts/install.py +562 -41
  32. package/scripts/integration.py +434 -24
  33. package/scripts/isolation.py +1077 -28
  34. package/scripts/lean_verification.py +99 -19
  35. package/scripts/loop_run.py +20 -30
  36. package/scripts/members.py +704 -0
  37. package/scripts/migrate_core.py +2 -2
  38. package/scripts/path_config.py +2 -2
  39. package/scripts/pipeline_diagnose.py +82 -31
  40. package/scripts/pipeline_git.py +61 -22
  41. package/scripts/pipeline_state.py +176 -54
  42. package/scripts/pipeline_undo.py +117 -22
  43. package/scripts/promote_lookahead.py +1 -1
  44. package/scripts/review_panel.py +1 -1
  45. package/scripts/runtime_store.py +35 -9
  46. package/scripts/skill-resources.json +28 -3
  47. package/scripts/state_checkpoint.py +140 -31
  48. package/scripts/state_promote.py +2 -0
  49. package/scripts/status_runtime.py +36 -6
  50. package/scripts/sync_skill_resources.py +8 -7
  51. package/scripts/token_budget.py +12 -8
  52. package/scripts/workflow_run.py +103 -29
  53. package/scripts/worktree_paths.py +18 -8
  54. package/skills/gsd-path/BUILD.md +7 -1
  55. package/skills/gsd-path/DECIDE.md +11 -4
  56. package/skills/gsd-path/DEFINE.md +8 -1
  57. package/skills/gsd-path/DOCS-AUDIT.md +6 -4
  58. package/skills/gsd-path/INSPECT.md +5 -4
  59. package/skills/gsd-path/PLAN.md +7 -1
  60. package/skills/gsd-path/RESEARCH.md +2 -1
  61. package/skills/gsd-path/ROADMAP.md +2 -1
  62. package/skills/gsd-path/SHIP.md +27 -10
  63. package/skills/gsd-path/SKILL.md +45 -1
  64. package/skills/gsd-path/references/build-native.md +47 -14
  65. package/skills/gsd-path/references/codebase-mapper.md +2 -1
  66. package/skills/gsd-path/references/docs-auditor.md +2 -1
  67. package/skills/gsd-path/references/operating-rules.md +214 -0
  68. package/skills/gsd-path/references/planner.md +8 -0
  69. package/skills/gsd-path/references/reviewer-wave.md +3 -1
  70. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  71. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  72. package/skills/gsd-path/scripts/_common.py +472 -19
  73. package/skills/gsd-path/scripts/archive_milestone.py +146 -73
  74. package/skills/gsd-path/scripts/bootstrap_repository.py +8 -5
  75. package/skills/gsd-path/scripts/build_recovery.py +4 -1
  76. package/skills/gsd-path/scripts/build_state.py +66 -23
  77. package/skills/gsd-path/scripts/check_docs_audit.py +14 -3
  78. package/skills/gsd-path/scripts/check_handoffs.py +116 -20
  79. package/skills/gsd-path/scripts/check_task_briefs.py +60 -8
  80. package/skills/gsd-path/scripts/check_update.py +2 -2
  81. package/skills/gsd-path/scripts/detect_project.py +345 -15
  82. package/skills/gsd-path/scripts/discussion_records.py +19 -13
  83. package/skills/gsd-path/scripts/discussion_validate.py +66 -21
  84. package/skills/gsd-path/scripts/dispatch_driver.py +111 -26
  85. package/skills/gsd-path/scripts/git_guard.py +116 -29
  86. package/skills/gsd-path/scripts/integration.py +434 -24
  87. package/skills/gsd-path/scripts/isolation.py +1077 -28
  88. package/skills/gsd-path/scripts/lean_verification.py +99 -19
  89. package/skills/gsd-path/scripts/loop_run.py +20 -30
  90. package/skills/gsd-path/scripts/members.py +704 -0
  91. package/skills/gsd-path/scripts/path_config.py +2 -2
  92. package/skills/gsd-path/scripts/pipeline_git.py +61 -22
  93. package/skills/gsd-path/scripts/pipeline_state.py +176 -54
  94. package/skills/gsd-path/scripts/promote_lookahead.py +1 -1
  95. package/skills/gsd-path/scripts/review_panel.py +1 -1
  96. package/skills/gsd-path/scripts/state_checkpoint.py +140 -31
  97. package/skills/gsd-path/scripts/state_promote.py +2 -0
  98. package/skills/gsd-path/scripts/status_runtime.py +36 -6
  99. package/skills/gsd-path/scripts/token_budget.py +12 -8
  100. package/skills/gsd-path/scripts/workflow_run.py +103 -29
  101. package/skills/gsd-path/scripts/worktree_paths.py +18 -8
  102. package/skills/gsd-path/templates/codebase.md +1 -1
  103. package/skills/gsd-path/templates/docs-audit.md +1 -1
  104. package/skills/gsd-path/templates/final-review.md +5 -2
  105. package/skills/gsd-path/templates/gap-review.md +2 -1
  106. package/skills/gsd-path/templates/task.md +1 -0
  107. package/skills/gsd-path/templates/wave-review.md +3 -1
  108. package/skills/gsd-path-build/SKILL.md +7 -1
  109. package/skills/gsd-path-build/references/build-native.md +47 -14
  110. package/skills/gsd-path-build/references/operating-rules.md +214 -0
  111. package/skills/gsd-path-build/references/reviewer-wave.md +3 -1
  112. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  113. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  114. package/skills/gsd-path-build/scripts/_common.py +472 -19
  115. package/skills/gsd-path-build/scripts/archive_milestone.py +146 -73
  116. package/skills/gsd-path-build/scripts/build_recovery.py +4 -1
  117. package/skills/gsd-path-build/scripts/build_state.py +66 -23
  118. package/skills/gsd-path-build/scripts/check_handoffs.py +116 -20
  119. package/skills/gsd-path-build/scripts/check_task_briefs.py +60 -8
  120. package/skills/gsd-path-build/scripts/detect_project.py +345 -15
  121. package/skills/gsd-path-build/scripts/discussion_records.py +19 -13
  122. package/skills/gsd-path-build/scripts/discussion_validate.py +66 -21
  123. package/skills/gsd-path-build/scripts/dispatch_driver.py +111 -26
  124. package/skills/gsd-path-build/scripts/integration.py +434 -24
  125. package/skills/gsd-path-build/scripts/isolation.py +1077 -28
  126. package/skills/gsd-path-build/scripts/loop_run.py +20 -30
  127. package/skills/gsd-path-build/scripts/members.py +704 -0
  128. package/skills/gsd-path-build/scripts/path_config.py +2 -2
  129. package/skills/gsd-path-build/scripts/pipeline_git.py +61 -22
  130. package/skills/gsd-path-build/scripts/pipeline_state.py +176 -54
  131. package/skills/gsd-path-build/scripts/promote_lookahead.py +1 -1
  132. package/skills/gsd-path-build/scripts/review_panel.py +1 -1
  133. package/skills/gsd-path-build/scripts/state_checkpoint.py +140 -31
  134. package/skills/gsd-path-build/scripts/state_promote.py +2 -0
  135. package/skills/gsd-path-build/scripts/token_budget.py +12 -8
  136. package/skills/gsd-path-build/scripts/workflow_run.py +103 -29
  137. package/skills/gsd-path-build/scripts/worktree_paths.py +18 -8
  138. package/skills/gsd-path-build/templates/task.md +1 -0
  139. package/skills/gsd-path-build/templates/wave-review.md +3 -1
  140. package/skills/gsd-path-decide/SKILL.md +11 -4
  141. package/skills/gsd-path-decide/references/operating-rules.md +214 -0
  142. package/skills/gsd-path-decide/scripts/_common.py +472 -19
  143. package/skills/gsd-path-decide/scripts/archive_milestone.py +146 -73
  144. package/skills/gsd-path-decide/scripts/build_recovery.py +4 -1
  145. package/skills/gsd-path-decide/scripts/check_handoffs.py +116 -20
  146. package/skills/gsd-path-decide/scripts/check_task_briefs.py +60 -8
  147. package/skills/gsd-path-decide/scripts/detect_project.py +345 -15
  148. package/skills/gsd-path-decide/scripts/discussion_records.py +19 -13
  149. package/skills/gsd-path-decide/scripts/discussion_validate.py +66 -21
  150. package/skills/gsd-path-decide/scripts/integration.py +434 -24
  151. package/skills/gsd-path-decide/scripts/isolation.py +1077 -28
  152. package/skills/gsd-path-decide/scripts/loop_run.py +20 -30
  153. package/skills/gsd-path-decide/scripts/members.py +704 -0
  154. package/skills/gsd-path-decide/scripts/path_config.py +2 -2
  155. package/skills/gsd-path-decide/scripts/pipeline_git.py +61 -22
  156. package/skills/gsd-path-decide/scripts/pipeline_state.py +176 -54
  157. package/skills/gsd-path-decide/scripts/promote_lookahead.py +1 -1
  158. package/skills/gsd-path-decide/scripts/review_panel.py +1 -1
  159. package/skills/gsd-path-decide/scripts/state_checkpoint.py +140 -31
  160. package/skills/gsd-path-decide/scripts/state_promote.py +2 -0
  161. package/skills/gsd-path-decide/scripts/worktree_paths.py +18 -8
  162. package/skills/gsd-path-define/SKILL.md +8 -1
  163. package/skills/gsd-path-define/references/operating-rules.md +214 -0
  164. package/skills/gsd-path-define/scripts/_common.py +472 -19
  165. package/skills/gsd-path-define/scripts/archive_milestone.py +146 -73
  166. package/skills/gsd-path-define/scripts/build_recovery.py +4 -1
  167. package/skills/gsd-path-define/scripts/check_handoffs.py +116 -20
  168. package/skills/gsd-path-define/scripts/check_task_briefs.py +60 -8
  169. package/skills/gsd-path-define/scripts/detect_project.py +345 -15
  170. package/skills/gsd-path-define/scripts/discussion_records.py +19 -13
  171. package/skills/gsd-path-define/scripts/discussion_validate.py +66 -21
  172. package/skills/gsd-path-define/scripts/integration.py +434 -24
  173. package/skills/gsd-path-define/scripts/isolation.py +1077 -28
  174. package/skills/gsd-path-define/scripts/loop_run.py +20 -30
  175. package/skills/gsd-path-define/scripts/members.py +704 -0
  176. package/skills/gsd-path-define/scripts/path_config.py +2 -2
  177. package/skills/gsd-path-define/scripts/pipeline_git.py +61 -22
  178. package/skills/gsd-path-define/scripts/pipeline_state.py +176 -54
  179. package/skills/gsd-path-define/scripts/promote_lookahead.py +1 -1
  180. package/skills/gsd-path-define/scripts/review_panel.py +1 -1
  181. package/skills/gsd-path-define/scripts/state_checkpoint.py +140 -31
  182. package/skills/gsd-path-define/scripts/state_promote.py +2 -0
  183. package/skills/gsd-path-define/scripts/worktree_paths.py +18 -8
  184. package/skills/gsd-path-discuss/SKILL.md +2 -1
  185. package/skills/gsd-path-discuss/references/operating-rules.md +214 -0
  186. package/skills/gsd-path-discuss/scripts/_common.py +472 -19
  187. package/skills/gsd-path-discuss/scripts/discussion_records.py +19 -13
  188. package/skills/gsd-path-discuss/scripts/loop_run.py +20 -30
  189. package/skills/gsd-path-discuss/scripts/path_config.py +2 -2
  190. package/skills/gsd-path-discuss/scripts/review_panel.py +1 -1
  191. package/skills/gsd-path-discuss/scripts/worktree_paths.py +18 -8
  192. package/skills/gsd-path-docs-audit/SKILL.md +6 -4
  193. package/skills/gsd-path-docs-audit/references/docs-auditor.md +2 -1
  194. package/skills/gsd-path-docs-audit/references/operating-rules.md +214 -0
  195. package/skills/gsd-path-docs-audit/scripts/_common.py +472 -19
  196. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +146 -73
  197. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +4 -1
  198. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +14 -3
  199. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +116 -20
  200. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +60 -8
  201. package/skills/gsd-path-docs-audit/scripts/detect_project.py +345 -15
  202. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +19 -13
  203. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +66 -21
  204. package/skills/gsd-path-docs-audit/scripts/integration.py +434 -24
  205. package/skills/gsd-path-docs-audit/scripts/isolation.py +1077 -28
  206. package/skills/gsd-path-docs-audit/scripts/loop_run.py +20 -30
  207. package/skills/gsd-path-docs-audit/scripts/members.py +704 -0
  208. package/skills/gsd-path-docs-audit/scripts/path_config.py +2 -2
  209. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +61 -22
  210. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +176 -54
  211. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +1 -1
  212. package/skills/gsd-path-docs-audit/scripts/review_panel.py +1 -1
  213. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +140 -31
  214. package/skills/gsd-path-docs-audit/scripts/state_promote.py +2 -0
  215. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +18 -8
  216. package/skills/gsd-path-docs-audit/templates/docs-audit.md +1 -1
  217. package/skills/gsd-path-forensics/SKILL.md +2 -1
  218. package/skills/gsd-path-forensics/references/operating-rules.md +214 -0
  219. package/skills/gsd-path-forensics/scripts/_common.py +472 -19
  220. package/skills/gsd-path-forensics/scripts/discussion_records.py +19 -13
  221. package/skills/gsd-path-forensics/scripts/loop_run.py +20 -30
  222. package/skills/gsd-path-forensics/scripts/path_config.py +2 -2
  223. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +82 -31
  224. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +117 -22
  225. package/skills/gsd-path-forensics/scripts/review_panel.py +1 -1
  226. package/skills/gsd-path-forensics/scripts/worktree_paths.py +18 -8
  227. package/skills/gsd-path-inspect/SKILL.md +5 -4
  228. package/skills/gsd-path-inspect/references/codebase-mapper.md +2 -1
  229. package/skills/gsd-path-inspect/references/docs-auditor.md +2 -1
  230. package/skills/gsd-path-inspect/references/operating-rules.md +214 -0
  231. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  232. package/skills/gsd-path-inspect/scripts/_common.py +472 -19
  233. package/skills/gsd-path-inspect/scripts/archive_milestone.py +146 -73
  234. package/skills/gsd-path-inspect/scripts/build_recovery.py +4 -1
  235. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +14 -3
  236. package/skills/gsd-path-inspect/scripts/check_handoffs.py +116 -20
  237. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +60 -8
  238. package/skills/gsd-path-inspect/scripts/detect_project.py +345 -15
  239. package/skills/gsd-path-inspect/scripts/discussion_records.py +19 -13
  240. package/skills/gsd-path-inspect/scripts/discussion_validate.py +66 -21
  241. package/skills/gsd-path-inspect/scripts/integration.py +434 -24
  242. package/skills/gsd-path-inspect/scripts/isolation.py +1077 -28
  243. package/skills/gsd-path-inspect/scripts/loop_run.py +20 -30
  244. package/skills/gsd-path-inspect/scripts/members.py +704 -0
  245. package/skills/gsd-path-inspect/scripts/path_config.py +2 -2
  246. package/skills/gsd-path-inspect/scripts/pipeline_git.py +61 -22
  247. package/skills/gsd-path-inspect/scripts/pipeline_state.py +176 -54
  248. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +1 -1
  249. package/skills/gsd-path-inspect/scripts/review_panel.py +1 -1
  250. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +140 -31
  251. package/skills/gsd-path-inspect/scripts/state_promote.py +2 -0
  252. package/skills/gsd-path-inspect/scripts/workflow_run.py +103 -29
  253. package/skills/gsd-path-inspect/scripts/worktree_paths.py +18 -8
  254. package/skills/gsd-path-inspect/templates/codebase.md +1 -1
  255. package/skills/gsd-path-inspect/templates/docs-audit.md +1 -1
  256. package/skills/gsd-path-loop/SKILL.md +2 -1
  257. package/skills/gsd-path-loop/references/operating-rules.md +214 -0
  258. package/skills/gsd-path-loop/scripts/_common.py +472 -19
  259. package/skills/gsd-path-loop/scripts/discussion_records.py +19 -13
  260. package/skills/gsd-path-loop/scripts/loop_run.py +20 -30
  261. package/skills/gsd-path-loop/scripts/path_config.py +2 -2
  262. package/skills/gsd-path-loop/scripts/review_panel.py +1 -1
  263. package/skills/gsd-path-loop/scripts/worktree_paths.py +18 -8
  264. package/skills/gsd-path-migrate/SKILL.md +2 -1
  265. package/skills/gsd-path-migrate/references/operating-rules.md +214 -0
  266. package/skills/gsd-path-migrate/scripts/_common.py +472 -19
  267. package/skills/gsd-path-migrate/scripts/core_hook_gate.py +10 -1
  268. package/skills/gsd-path-migrate/scripts/core_hook_settings.py +11 -4
  269. package/skills/gsd-path-migrate/scripts/discussion_records.py +19 -13
  270. package/skills/gsd-path-migrate/scripts/loop_run.py +20 -30
  271. package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -2
  272. package/skills/gsd-path-migrate/scripts/path_config.py +2 -2
  273. package/skills/gsd-path-migrate/scripts/review_panel.py +1 -1
  274. package/skills/gsd-path-plan/SKILL.md +7 -1
  275. package/skills/gsd-path-plan/references/operating-rules.md +214 -0
  276. package/skills/gsd-path-plan/references/planner.md +8 -0
  277. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  278. package/skills/gsd-path-plan/scripts/_common.py +472 -19
  279. package/skills/gsd-path-plan/scripts/archive_milestone.py +146 -73
  280. package/skills/gsd-path-plan/scripts/build_recovery.py +4 -1
  281. package/skills/gsd-path-plan/scripts/check_handoffs.py +116 -20
  282. package/skills/gsd-path-plan/scripts/check_task_briefs.py +60 -8
  283. package/skills/gsd-path-plan/scripts/detect_project.py +345 -15
  284. package/skills/gsd-path-plan/scripts/discussion_records.py +19 -13
  285. package/skills/gsd-path-plan/scripts/discussion_validate.py +66 -21
  286. package/skills/gsd-path-plan/scripts/integration.py +434 -24
  287. package/skills/gsd-path-plan/scripts/isolation.py +1077 -28
  288. package/skills/gsd-path-plan/scripts/loop_run.py +20 -30
  289. package/skills/gsd-path-plan/scripts/members.py +704 -0
  290. package/skills/gsd-path-plan/scripts/path_config.py +2 -2
  291. package/skills/gsd-path-plan/scripts/pipeline_git.py +61 -22
  292. package/skills/gsd-path-plan/scripts/pipeline_state.py +176 -54
  293. package/skills/gsd-path-plan/scripts/promote_lookahead.py +1 -1
  294. package/skills/gsd-path-plan/scripts/review_panel.py +1 -1
  295. package/skills/gsd-path-plan/scripts/state_checkpoint.py +140 -31
  296. package/skills/gsd-path-plan/scripts/state_promote.py +2 -0
  297. package/skills/gsd-path-plan/scripts/workflow_run.py +103 -29
  298. package/skills/gsd-path-plan/scripts/worktree_paths.py +18 -8
  299. package/skills/gsd-path-plan/templates/task.md +1 -0
  300. package/skills/gsd-path-research/SKILL.md +2 -1
  301. package/skills/gsd-path-research/references/operating-rules.md +214 -0
  302. package/skills/gsd-path-research/scripts/_common.py +472 -19
  303. package/skills/gsd-path-research/scripts/archive_milestone.py +146 -73
  304. package/skills/gsd-path-research/scripts/build_recovery.py +4 -1
  305. package/skills/gsd-path-research/scripts/check_handoffs.py +116 -20
  306. package/skills/gsd-path-research/scripts/check_task_briefs.py +60 -8
  307. package/skills/gsd-path-research/scripts/detect_project.py +345 -15
  308. package/skills/gsd-path-research/scripts/discussion_records.py +19 -13
  309. package/skills/gsd-path-research/scripts/discussion_validate.py +66 -21
  310. package/skills/gsd-path-research/scripts/integration.py +434 -24
  311. package/skills/gsd-path-research/scripts/isolation.py +1077 -28
  312. package/skills/gsd-path-research/scripts/loop_run.py +20 -30
  313. package/skills/gsd-path-research/scripts/members.py +704 -0
  314. package/skills/gsd-path-research/scripts/path_config.py +2 -2
  315. package/skills/gsd-path-research/scripts/pipeline_git.py +61 -22
  316. package/skills/gsd-path-research/scripts/pipeline_state.py +176 -54
  317. package/skills/gsd-path-research/scripts/promote_lookahead.py +1 -1
  318. package/skills/gsd-path-research/scripts/review_panel.py +1 -1
  319. package/skills/gsd-path-research/scripts/state_checkpoint.py +140 -31
  320. package/skills/gsd-path-research/scripts/state_promote.py +2 -0
  321. package/skills/gsd-path-research/scripts/worktree_paths.py +18 -8
  322. package/skills/gsd-path-roadmap/SKILL.md +2 -1
  323. package/skills/gsd-path-roadmap/references/operating-rules.md +214 -0
  324. package/skills/gsd-path-roadmap/scripts/_common.py +472 -19
  325. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +146 -73
  326. package/skills/gsd-path-roadmap/scripts/build_recovery.py +4 -1
  327. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +116 -20
  328. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +60 -8
  329. package/skills/gsd-path-roadmap/scripts/detect_project.py +345 -15
  330. package/skills/gsd-path-roadmap/scripts/discussion_records.py +19 -13
  331. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +66 -21
  332. package/skills/gsd-path-roadmap/scripts/integration.py +434 -24
  333. package/skills/gsd-path-roadmap/scripts/isolation.py +1077 -28
  334. package/skills/gsd-path-roadmap/scripts/loop_run.py +20 -30
  335. package/skills/gsd-path-roadmap/scripts/members.py +704 -0
  336. package/skills/gsd-path-roadmap/scripts/path_config.py +2 -2
  337. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +61 -22
  338. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +176 -54
  339. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +1 -1
  340. package/skills/gsd-path-roadmap/scripts/review_panel.py +1 -1
  341. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +140 -31
  342. package/skills/gsd-path-roadmap/scripts/state_promote.py +2 -0
  343. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +18 -8
  344. package/skills/gsd-path-ship/SKILL.md +27 -10
  345. package/skills/gsd-path-ship/references/operating-rules.md +214 -0
  346. package/skills/gsd-path-ship/references/reviewer-wave.md +3 -1
  347. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  348. package/skills/gsd-path-ship/scripts/_common.py +472 -19
  349. package/skills/gsd-path-ship/scripts/archive_milestone.py +146 -73
  350. package/skills/gsd-path-ship/scripts/build_recovery.py +4 -1
  351. package/skills/gsd-path-ship/scripts/build_state.py +66 -23
  352. package/skills/gsd-path-ship/scripts/check_handoffs.py +116 -20
  353. package/skills/gsd-path-ship/scripts/check_task_briefs.py +60 -8
  354. package/skills/gsd-path-ship/scripts/detect_project.py +345 -15
  355. package/skills/gsd-path-ship/scripts/discussion_records.py +19 -13
  356. package/skills/gsd-path-ship/scripts/discussion_validate.py +66 -21
  357. package/skills/gsd-path-ship/scripts/integration.py +434 -24
  358. package/skills/gsd-path-ship/scripts/isolation.py +1077 -28
  359. package/skills/gsd-path-ship/scripts/lean_verification.py +99 -19
  360. package/skills/gsd-path-ship/scripts/loop_run.py +20 -30
  361. package/skills/gsd-path-ship/scripts/members.py +704 -0
  362. package/skills/gsd-path-ship/scripts/path_config.py +2 -2
  363. package/skills/gsd-path-ship/scripts/pipeline_git.py +61 -22
  364. package/skills/gsd-path-ship/scripts/pipeline_state.py +176 -54
  365. package/skills/gsd-path-ship/scripts/promote_lookahead.py +1 -1
  366. package/skills/gsd-path-ship/scripts/review_panel.py +1 -1
  367. package/skills/gsd-path-ship/scripts/state_checkpoint.py +140 -31
  368. package/skills/gsd-path-ship/scripts/state_promote.py +2 -0
  369. package/skills/gsd-path-ship/scripts/workflow_run.py +103 -29
  370. package/skills/gsd-path-ship/scripts/worktree_paths.py +18 -8
  371. package/skills/gsd-path-ship/templates/final-review.md +5 -2
  372. package/skills/gsd-path-ship/templates/gap-review.md +2 -1
  373. package/skills/gsd-path-ship/templates/wave-review.md +3 -1
  374. package/skills/gsd-path-undo/SKILL.md +2 -1
  375. package/skills/gsd-path-undo/references/operating-rules.md +214 -0
  376. package/skills/gsd-path-undo/scripts/_common.py +472 -19
  377. package/skills/gsd-path-undo/scripts/discussion_records.py +19 -13
  378. package/skills/gsd-path-undo/scripts/loop_run.py +20 -30
  379. package/skills/gsd-path-undo/scripts/path_config.py +2 -2
  380. package/skills/gsd-path-undo/scripts/pipeline_undo.py +117 -22
  381. package/skills/gsd-path-undo/scripts/review_panel.py +1 -1
  382. package/skills/gsd-path-undo/scripts/worktree_paths.py +18 -8
  383. package/skills/path/BUILD.md +7 -1
  384. package/skills/path/DECIDE.md +11 -4
  385. package/skills/path/DEFINE.md +8 -1
  386. package/skills/path/DOCS-AUDIT.md +6 -4
  387. package/skills/path/INSPECT.md +5 -4
  388. package/skills/path/PLAN.md +7 -1
  389. package/skills/path/RESEARCH.md +2 -1
  390. package/skills/path/ROADMAP.md +2 -1
  391. package/skills/path/SHIP.md +27 -10
  392. package/skills/path/SKILL.md +45 -1
  393. package/skills/path/references/build-native.md +47 -14
  394. package/skills/path/references/codebase-mapper.md +2 -1
  395. package/skills/path/references/docs-auditor.md +2 -1
  396. package/skills/path/references/operating-rules.md +214 -0
  397. package/skills/path/references/planner.md +8 -0
  398. package/skills/path/references/reviewer-wave.md +3 -1
  399. package/skills/path/scripts/_common.py +472 -19
  400. package/skills/path/scripts/archive_milestone.py +146 -73
  401. package/skills/path/scripts/bootstrap_repository.py +8 -5
  402. package/skills/path/scripts/build_recovery.py +4 -1
  403. package/skills/path/scripts/build_state.py +66 -23
  404. package/skills/path/scripts/check_docs_audit.py +14 -3
  405. package/skills/path/scripts/check_handoffs.py +116 -20
  406. package/skills/path/scripts/check_task_briefs.py +60 -8
  407. package/skills/path/scripts/check_update.py +2 -2
  408. package/skills/path/scripts/detect_project.py +345 -15
  409. package/skills/path/scripts/discussion_records.py +19 -13
  410. package/skills/path/scripts/discussion_validate.py +66 -21
  411. package/skills/path/scripts/dispatch_driver.py +111 -26
  412. package/skills/path/scripts/git_guard.py +116 -29
  413. package/skills/path/scripts/integration.py +434 -24
  414. package/skills/path/scripts/isolation.py +1077 -28
  415. package/skills/path/scripts/lean_verification.py +99 -19
  416. package/skills/path/scripts/loop_run.py +20 -30
  417. package/skills/path/scripts/members.py +704 -0
  418. package/skills/path/scripts/path_config.py +2 -2
  419. package/skills/path/scripts/pipeline_git.py +61 -22
  420. package/skills/path/scripts/pipeline_state.py +176 -54
  421. package/skills/path/scripts/promote_lookahead.py +1 -1
  422. package/skills/path/scripts/review_panel.py +1 -1
  423. package/skills/path/scripts/state_checkpoint.py +140 -31
  424. package/skills/path/scripts/state_promote.py +2 -0
  425. package/skills/path/scripts/status_runtime.py +36 -6
  426. package/skills/path/scripts/token_budget.py +12 -8
  427. package/skills/path/scripts/workflow_run.py +103 -29
  428. package/skills/path/scripts/worktree_paths.py +18 -8
  429. package/skills/path/templates/codebase.md +1 -1
  430. package/skills/path/templates/docs-audit.md +1 -1
  431. package/skills/path/templates/final-review.md +5 -2
  432. package/skills/path/templates/gap-review.md +2 -1
  433. package/skills/path/templates/task.md +1 -0
  434. package/skills/path/templates/wave-review.md +3 -1
@@ -24,13 +24,15 @@ from datetime import date
24
24
  from pathlib import Path, PurePosixPath
25
25
  from typing import Iterator, NamedTuple, Optional, Sequence
26
26
 
27
- try:
28
- from isolation import (
27
+ if __package__:
28
+ from .isolation import (
29
29
  IsolationError,
30
+ prune_host_scratch,
31
+ PROJECT_ENTRIES,
30
32
  checkpoint as isolation_checkpoint,
31
33
  verify_landed_task_files,
32
34
  )
33
- from pipeline_git import (
35
+ from .pipeline_git import (
34
36
  bound_branch_name,
35
37
  default_branch_name,
36
38
  integrate_commit_body,
@@ -43,45 +45,75 @@ try:
43
45
  ship_commit_body,
44
46
  ship_subject,
45
47
  )
46
- from review_panel import KNOWN_FAMILIES, ReviewPanelError, parse_sources
47
- from pipeline_state import (
48
+ from .review_panel import KNOWN_FAMILIES, ReviewPanelError, parse_sources
49
+ from .pipeline_state import (
48
50
  PipelineState,
49
51
  PipelineStateError,
50
52
  load_state,
51
53
  transition_state,
52
54
  )
53
- import _common
54
- except ImportError: # pragma: no cover - package import used by tests
55
- from scripts.isolation import (
56
- IsolationError,
57
- checkpoint as isolation_checkpoint,
58
- verify_landed_task_files,
59
- )
60
- from scripts.pipeline_git import (
61
- bound_branch_name,
62
- default_branch_name,
63
- integrate_commit_body,
64
- integrate_subject,
65
- is_bound_branch,
66
- is_integrate_subject,
67
- is_ship_subject,
68
- milestone_id,
69
- milestone_number,
70
- ship_commit_body,
71
- ship_subject,
72
- )
73
- from scripts.review_panel import KNOWN_FAMILIES, ReviewPanelError, parse_sources
74
- from scripts.pipeline_state import (
75
- PipelineState,
76
- PipelineStateError,
77
- load_state,
78
- transition_state,
79
- )
80
- from scripts import _common
81
-
82
- if sys.platform == "win32":
83
- import msvcrt
55
+ from . import _common
84
56
  else:
57
+ try:
58
+ from isolation import (
59
+ IsolationError,
60
+ prune_host_scratch,
61
+ PROJECT_ENTRIES,
62
+ checkpoint as isolation_checkpoint,
63
+ verify_landed_task_files,
64
+ )
65
+ from pipeline_git import (
66
+ bound_branch_name,
67
+ default_branch_name,
68
+ integrate_commit_body,
69
+ integrate_subject,
70
+ is_bound_branch,
71
+ is_integrate_subject,
72
+ is_ship_subject,
73
+ milestone_id,
74
+ milestone_number,
75
+ ship_commit_body,
76
+ ship_subject,
77
+ )
78
+ from review_panel import KNOWN_FAMILIES, ReviewPanelError, parse_sources
79
+ from pipeline_state import (
80
+ PipelineState,
81
+ PipelineStateError,
82
+ load_state,
83
+ transition_state,
84
+ )
85
+ import _common
86
+ except ImportError: # pragma: no cover - package import used by tests
87
+ from scripts.isolation import (
88
+ IsolationError,
89
+ prune_host_scratch,
90
+ PROJECT_ENTRIES,
91
+ checkpoint as isolation_checkpoint,
92
+ verify_landed_task_files,
93
+ )
94
+ from scripts.pipeline_git import (
95
+ bound_branch_name,
96
+ default_branch_name,
97
+ integrate_commit_body,
98
+ integrate_subject,
99
+ is_bound_branch,
100
+ is_integrate_subject,
101
+ is_ship_subject,
102
+ milestone_id,
103
+ milestone_number,
104
+ ship_commit_body,
105
+ ship_subject,
106
+ )
107
+ from scripts.review_panel import KNOWN_FAMILIES, ReviewPanelError, parse_sources
108
+ from scripts.pipeline_state import (
109
+ PipelineState,
110
+ PipelineStateError,
111
+ load_state,
112
+ transition_state,
113
+ )
114
+ from scripts import _common
115
+
116
+ if sys.platform != "win32":
85
117
  import fcntl
86
118
 
87
119
 
@@ -350,31 +382,25 @@ else: # standalone script or sibling import
350
382
  @contextmanager
351
383
  def discussion_lock(active_root: Path) -> Iterator[None]:
352
384
  if sys.platform == "win32":
353
- lock_value = require_git_success(
354
- run_git(
355
- active_root.parent,
356
- "rev-parse",
357
- "--git-path",
358
- "gsd-path-discussion.lock",
359
- ),
360
- "resolve discussion lock",
385
+ # Windows cannot flock a directory, so lock a file in the git dir,
386
+ # where it never shows up as an untracked project file.
387
+ resolved = run_git(
388
+ active_root.parent,
389
+ "rev-parse",
390
+ "--git-path",
391
+ "gsd-path-discussion.lock",
361
392
  )
362
- lock_path = Path(lock_value)
393
+ if resolved.returncode != 0 or not resolved.stdout.strip():
394
+ # Outside git, which the POSIX directory lock allows: the same
395
+ # named kernel mutex pipeline_state holds for this directory.
396
+ with _common.directory_mutex(active_root):
397
+ yield
398
+ return
399
+ lock_path = Path(resolved.stdout.strip())
363
400
  if not lock_path.is_absolute():
364
401
  lock_path = active_root.parent / lock_path
365
- lock_path.parent.mkdir(parents=True, exist_ok=True)
366
- with lock_path.open("a+b") as handle:
367
- handle.seek(0, os.SEEK_END)
368
- if handle.tell() == 0:
369
- handle.write(b"\0")
370
- handle.flush()
371
- handle.seek(0)
372
- msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
373
- try:
374
- yield
375
- finally:
376
- handle.seek(0)
377
- msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
402
+ with _common.exclusive_lock(lock_path):
403
+ yield
378
404
  return
379
405
 
380
406
  descriptor = os.open(active_root, os.O_RDONLY)
@@ -535,6 +561,8 @@ def archive_sequence_entries(project: Path) -> dict[int, Path]:
535
561
  archive_root = safe_archive_root(project)
536
562
  entries = {}
537
563
  for candidate in archive_root.iterdir():
564
+ if _common.is_ignored_junk(candidate):
565
+ continue
538
566
  match = ARCHIVE_PATTERN.fullmatch(candidate.name)
539
567
  if match is None or not candidate.is_dir():
540
568
  raise ArchiveError(f"archive root has a noncanonical entry: {candidate.name}")
@@ -620,7 +648,7 @@ def pending_ruling_count(audit: Path) -> int:
620
648
 
621
649
 
622
650
  def is_existing_carry_forward(source: Path, archived: Path) -> bool:
623
- entries = list(source.iterdir()) if source.is_dir() else []
651
+ entries = [path for path in source.iterdir() if not _common.is_ignored_junk(path)] if source.is_dir() else []
624
652
  archived_audit = archived / "DOCS-AUDIT.md"
625
653
  if not archived_audit.is_file() or pending_ruling_count(archived_audit) == 0:
626
654
  return False
@@ -681,7 +709,7 @@ def is_real_file(path: Path) -> bool:
681
709
  def canonical_task_files(tasks: Path) -> Sequence[Path]:
682
710
  if tasks.is_symlink() or not tasks.is_dir():
683
711
  return ()
684
- candidates = sorted(tasks.iterdir())
712
+ candidates = sorted(path for path in tasks.iterdir() if not _common.is_ignored_junk(path))
685
713
  if any(
686
714
  not TASK_FILE_PATTERN.fullmatch(path.name) or not is_real_file(path)
687
715
  for path in candidates
@@ -1001,6 +1029,7 @@ def prepare(repo: Path, slug: str) -> dict:
1001
1029
  project = repo.resolve()
1002
1030
  active_root = require_project_layout(project)
1003
1031
  with discussion_lock(active_root):
1032
+ prune_host_scratch(active_root)
1004
1033
  return prepare_locked(project, active_root, slug)
1005
1034
 
1006
1035
 
@@ -1131,13 +1160,7 @@ def require_canonical_abandon_inputs(active_root: Path, archive: Path) -> None:
1131
1160
  def write_abandon_manifest(
1132
1161
  archive: Path, slug: str, ruling: str, abandoned_on: str
1133
1162
  ) -> None:
1134
- contents = []
1135
- for path in archive.rglob("*"):
1136
- if path.is_symlink():
1137
- raise ArchiveError(f"archive contents must not be symlinks: {path}")
1138
- if path.is_file() and path.name != "MANIFEST.md":
1139
- contents.append(path.relative_to(archive).as_posix())
1140
- contents.sort()
1163
+ contents = archive_file_inventory(archive)
1141
1164
  listed_contents = "\n".join(f"- {path}" for path in contents)
1142
1165
  atomic_replace(
1143
1166
  archive / "MANIFEST.md",
@@ -1693,10 +1716,13 @@ def require_clean_older_archives(project: Path, configured: str) -> None:
1693
1716
  current_prefix = f"{configured}/"
1694
1717
  dirty = set()
1695
1718
  ignored_current = set()
1719
+ current_files = {f"{configured}/{name}" for name in archive_file_inventory(project / configured)}
1696
1720
  for record in status.split("\0"):
1697
1721
  has_status = len(record) > 3 and record[2] == " "
1698
1722
  code = record[:2] if has_status else ""
1699
1723
  path = record[3:] if has_status else record
1724
+ if code == "!!" and _common.is_ignored_junk(project / path) and path not in current_files:
1725
+ continue # Ignored OS junk never ships and is not an artifact.
1700
1726
  if code == "!!" and (
1701
1727
  path == ".project/archive/"
1702
1728
  or path == configured
@@ -1745,7 +1771,7 @@ def require_clean_active_root(active_root: Path, archive: Path) -> None:
1745
1771
  archived_audit = archive / "research" / "DOCS-AUDIT.md"
1746
1772
  carried_forward = pending_ruling_count(archived_audit)
1747
1773
  # Persistent project metadata ships but never archives with a milestone.
1748
- allowed = {"STATE.md", "REPOSITORY.md", "archive", "LESSONS.md", "next"}
1774
+ allowed = {"STATE.md", "REPOSITORY.md", "MEMBERS.md", "archive", "LESSONS.md", "next"}
1749
1775
  for name in ("config.json", "model-policy.json"):
1750
1776
  path = active_root / name
1751
1777
  if path.exists() or path.is_symlink():
@@ -1760,7 +1786,9 @@ def require_clean_active_root(active_root: Path, archive: Path) -> None:
1760
1786
  if carried_forward:
1761
1787
  allowed.add("research")
1762
1788
  active_audit = active_research / "DOCS-AUDIT.md"
1763
- entries = sorted(path.name for path in active_research.iterdir()) if active_research.is_dir() else []
1789
+ entries = sorted(
1790
+ path.name for path in active_research.iterdir() if not _common.is_ignored_junk(path)
1791
+ ) if active_research.is_dir() else []
1764
1792
  if (
1765
1793
  active_research.is_symlink()
1766
1794
  or entries != ["DOCS-AUDIT.md"]
@@ -1772,7 +1800,12 @@ def require_clean_active_root(active_root: Path, archive: Path) -> None:
1772
1800
  elif active_research.exists() or active_research.is_symlink():
1773
1801
  raise ArchiveError("active research exists without a pending carry-forward queue")
1774
1802
 
1775
- unexpected = sorted(path.name for path in active_root.iterdir() if path.name not in allowed)
1803
+ # Ignored, untracked non-pipeline entries (.DS_Store) never ship; pipeline entries block even when ignored.
1804
+ visible = _common.git_visible_entries(active_root)
1805
+ unexpected = sorted(
1806
+ path.name for path in active_root.iterdir()
1807
+ if path.name not in allowed and (path.name in PROJECT_ENTRIES or path.name in visible)
1808
+ )
1776
1809
  if unexpected:
1777
1810
  raise ArchiveError(f"unexpected active .project paths remain: {', '.join(unexpected)}")
1778
1811
 
@@ -1848,6 +1881,12 @@ def prepared_transaction(
1848
1881
 
1849
1882
 
1850
1883
  def preflight(repo: Path) -> dict:
1884
+ project = repo.resolve()
1885
+ active_root = require_project_layout(project)
1886
+ configured = strict_state(project)[0].archive
1887
+ current = (archive_from_state(project, configured)
1888
+ if configured and not archive_is_committed(project, configured) else None)
1889
+ prune_host_scratch(active_root, current)
1851
1890
  project, _, state, configured, archive, _, reviewed_head = prepared_transaction(
1852
1891
  repo,
1853
1892
  ("ship", "shipped"),
@@ -1937,6 +1976,28 @@ def validate_shipped_roadmap(text: str, state: PipelineState, archive: str) -> N
1937
1976
  raise ArchiveError("ROADMAP.md must record Status: shipped and the exact Archive")
1938
1977
 
1939
1978
 
1979
+ def member_ship_rows(project: Path, archive: Path, ship_commit: str) -> list:
1980
+ """Ship body member lines, bound to the archived lock order and FINAL.md member heads."""
1981
+ if __package__:
1982
+ from . import check_handoffs
1983
+ from .pipeline_git import ship_member_rows
1984
+ else:
1985
+ import check_handoffs
1986
+ from pipeline_git import ship_member_rows
1987
+ message = require_git_success(run_git(project, "show", "-s", "--format=%B", ship_commit), "inspect ship body")
1988
+ rows = ship_member_rows(message)
1989
+ lock = archive / "build" / "members.json"
1990
+ locked = [entry["name"] for entry in json.loads(lock.read_text(encoding="utf-8"))["members"]] if lock.is_file() else []
1991
+ try:
1992
+ heads = check_handoffs.member_reviewed_heads((archive / "review" / "FINAL.md").read_text(encoding="utf-8"),
1993
+ "FINAL.md")
1994
+ except check_handoffs.HandoffError as error:
1995
+ raise ArchiveError(str(error)) from error
1996
+ if [row["name"] for row in rows] != locked or {row["name"]: row["reviewed_head"] for row in rows} != heads:
1997
+ raise ArchiveError("ship commit member lines must name each locked member at its FINAL.md reviewed HEAD")
1998
+ return rows
1999
+
2000
+
1940
2001
  def validate(repo: Path, *, historical: bool = False) -> dict:
1941
2002
  project, _, state, configured, archive, archived_files, reviewed_head = prepared_transaction(
1942
2003
  repo,
@@ -2030,6 +2091,7 @@ def validate(repo: Path, *, historical: bool = False) -> dict:
2030
2091
  )
2031
2092
  require_committed_carry_forward(project, configured, archive, ship_commit)
2032
2093
 
2094
+ members = member_ship_rows(project, archive, ship_commit)
2033
2095
  ship_parent = require_git_success(run_git(project, "rev-parse", f"{ship_commit}^"), "resolve ship parent")
2034
2096
  if reviewed_head != ship_parent:
2035
2097
  raise ArchiveError(
@@ -2039,7 +2101,7 @@ def validate(repo: Path, *, historical: bool = False) -> dict:
2039
2101
  project,
2040
2102
  ship_commit,
2041
2103
  ship_subject(archive.name),
2042
- ship_commit_body(configured, reviewed_head),
2104
+ ship_commit_body(configured, reviewed_head, members),
2043
2105
  "ship",
2044
2106
  )
2045
2107
 
@@ -2077,7 +2139,10 @@ def validate(repo: Path, *, historical: bool = False) -> dict:
2077
2139
  + f"; run: git -C {project} revert <commits after {ship_commit[:12]} that touch .project>"
2078
2140
  )
2079
2141
 
2080
- return {"archive": configured, "commit": ship_commit}
2142
+ result = {"archive": configured, "commit": ship_commit}
2143
+ if members:
2144
+ result["members"] = members
2145
+ return result
2081
2146
 
2082
2147
 
2083
2148
  def parser() -> argparse.ArgumentParser:
@@ -2119,6 +2184,12 @@ def parser() -> argparse.ArgumentParser:
2119
2184
  )
2120
2185
  refresh_parser.add_argument("--repo", required=True, type=Path)
2121
2186
 
2187
+ close_members_parser = subparsers.add_parser(
2188
+ "close-members",
2189
+ help="after preflight, integrate each locked member in lock order before the ship commit",
2190
+ )
2191
+ close_members_parser.add_argument("--repo", required=True, type=Path)
2192
+
2122
2193
  integrate_parser = subparsers.add_parser(
2123
2194
  "integrate",
2124
2195
  help="resume and publish the shipped milestone integration",
@@ -2139,7 +2210,7 @@ def parser() -> argparse.ArgumentParser:
2139
2210
  def main(argv: Optional[Sequence[str]] = None) -> int:
2140
2211
  arguments = parser().parse_args(argv)
2141
2212
  try:
2142
- if arguments.command in {"validate-integrated", "refresh-origin", "integrate"}:
2213
+ if arguments.command in {"validate-integrated", "refresh-origin", "integrate", "close-members"}:
2143
2214
  if __package__:
2144
2215
  from . import integration
2145
2216
  else:
@@ -2156,6 +2227,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2156
2227
  result = integration.validate_integrated(arguments.repo, arguments.slug)
2157
2228
  elif arguments.command == "refresh-origin":
2158
2229
  result = integration.refresh_origin(arguments.repo)
2230
+ elif arguments.command == "close-members":
2231
+ result = integration.close_members(arguments.repo)
2159
2232
  elif arguments.command == "integrate":
2160
2233
  result = integration.integrate(arguments.repo, arguments.slug)
2161
2234
  else:
@@ -90,6 +90,8 @@ def settled_tasks(repo: Path) -> dict[str, tuple[str, dict]]:
90
90
  raise state.PipelineStateError("build recovery requires real task artifacts")
91
91
  tasks = {}
92
92
  for path in sorted(directory.iterdir()):
93
+ if state._common.is_ignored_junk(path):
94
+ continue
93
95
  if not re.fullmatch(r"T[0-9]{3}-[a-z0-9][a-z0-9-]*\.md", path.name):
94
96
  raise state.PipelineStateError(f"invalid recovery task path: {path.name}")
95
97
  text = state._read_real_file(path, path.name)
@@ -165,7 +167,8 @@ def validate_review_backup(repo: Path, recovery: dict, directory: Path) -> None:
165
167
  if path:
166
168
  expected[str(Path(path).relative_to(".project/review"))] = checkpoint_runtime()._git_text_at(repo, recovery["base"], path)
167
169
  actual = {str(path.relative_to(directory)): path.read_text(encoding="utf-8")
168
- for path in directory.rglob("*") if path.is_file()} if directory.exists() else {}
170
+ for path in directory.rglob("*")
171
+ if path.is_file() and not state._common.is_ignored_junk(path)} if directory.exists() else {}
169
172
  if actual != expected:
170
173
  raise state.PipelineStateError("review backup differs from the recovery base")
171
174
 
@@ -13,12 +13,16 @@ sys.dont_write_bytecode = True
13
13
  from pathlib import Path, PurePosixPath
14
14
  from typing import Dict, List, Optional, Sequence, Set, Tuple
15
15
 
16
- try:
17
- from pipeline_state import PipelineStateError, load_state
18
- import _common
19
- except ImportError: # pragma: no cover - package imports used by tests
20
- from scripts.pipeline_state import PipelineStateError, load_state
21
- from scripts import _common
16
+ if __package__:
17
+ from .pipeline_state import PipelineStateError, load_state
18
+ from . import _common
19
+ else:
20
+ try:
21
+ from pipeline_state import PipelineStateError, load_state
22
+ import _common
23
+ except ImportError: # pragma: no cover - package imports used by tests
24
+ from scripts.pipeline_state import PipelineStateError, load_state
25
+ from scripts import _common
22
26
 
23
27
 
24
28
  PIPELINE = "gsd-path/v2"
@@ -377,7 +381,10 @@ def validate_research(
377
381
  ) -> Dict[str, object]:
378
382
  """Validate the research-to-synthesis hand-off and return its summary."""
379
383
 
380
- _require_state(root, "research", "active", project_dir)
384
+ # Research runs this before closing; decide runs it after its entry transition.
385
+ _require_state_one_of(
386
+ root, {("research", "active"), ("decide", "active")}, project_dir
387
+ )
381
388
  return validate_research_artifacts(root, project_dir)
382
389
 
383
390
 
@@ -548,6 +555,45 @@ def _reviewed_head(text: str, source: str) -> str:
548
555
  return value
549
556
 
550
557
 
558
+ MEMBER_HEAD_PATTERN = re.compile(r"(?m)^Member reviewed HEAD:(.*)$")
559
+
560
+
561
+ def member_reviewed_heads(text: str, source: str) -> Dict[str, str]:
562
+ """`Member reviewed HEAD: <member> <sha>` lines, one per member of a multi-repo milestone."""
563
+ heads: Dict[str, str] = {}
564
+ # Only the header binds heads; command output quoted in later sections cannot add one.
565
+ for value in MEMBER_HEAD_PATTERN.findall(_strip_comments(text).split("\n## ", 1)[0]):
566
+ name, _, sha = value.strip().partition(" ")
567
+ if not name or not SHA_PATTERN.fullmatch(sha.strip()):
568
+ raise HandoffError(
569
+ f"{source} Member reviewed HEAD must be '<member> <full commit SHA>', not {value.strip() or 'empty'}"
570
+ )
571
+ if name in heads:
572
+ raise HandoffError(f"{source} repeats Member reviewed HEAD for {name}")
573
+ heads[name] = sha.strip()
574
+ return heads
575
+
576
+
577
+ def _member_tips(root: Path) -> Dict[str, str]:
578
+ """Each locked member's bound branch tip; empty when the milestone locks no member."""
579
+ try:
580
+ import isolation
581
+ except ImportError: # pragma: no cover - package import used by tests
582
+ from scripts import isolation
583
+ lock = root / ".project/build/members.json"
584
+ if not lock.is_file():
585
+ return {}
586
+ tips: Dict[str, str] = {}
587
+ try:
588
+ for item in json.loads(lock.read_text(encoding="utf-8"))["members"]:
589
+ checkout, _, entry = isolation._member_context(root, item["name"])
590
+ tips[entry["name"]] = isolation.git_output(
591
+ checkout, "rev-parse", "--verify", f"refs/heads/{entry['branch']}^{{commit}}")
592
+ except isolation.IsolationError as error:
593
+ raise HandoffError(str(error)) from error
594
+ return tips
595
+
596
+
551
597
  def _strip_comments(text: str) -> str:
552
598
  return COMMENT_PATTERN.sub("", text)
553
599
 
@@ -750,6 +796,8 @@ def _task_texts(root: Path, project_dir: str) -> Dict[str, str]:
750
796
  raise HandoffError(f"missing {project_dir}/tasks")
751
797
  tasks: Dict[str, str] = {}
752
798
  for path in sorted(tasks_dir.iterdir()):
799
+ if _common.is_ignored_junk(path):
800
+ continue
753
801
  named = TASK_FILE_NAME.fullmatch(path.name)
754
802
  if named is None or not path.is_file() or path.is_symlink():
755
803
  raise HandoffError(f"non-canonical task artifact: {path.name}")
@@ -1008,7 +1056,10 @@ def _validate_task_graph(
1008
1056
  graph: Dict[str, List[str]] = {}
1009
1057
  task_waves: Dict[str, int] = {}
1010
1058
  task_files: Dict[str, List[str]] = {}
1059
+ # `repo:` names the member a task changes; absent means the coordinator.
1060
+ task_repos: Dict[str, str] = {}
1011
1061
  for task_id, text in tasks.items():
1062
+ task_repos[task_id] = str(_strict_frontmatter(text, task_id).get("repo") or "")
1012
1063
  # Patch mode reopens planning beside landed tasks; only new tasks start clean.
1013
1064
  landed = _task_scalar(text, task_id, "status") == "done"
1014
1065
  _require_task_structure(task_id, text, initial=initial and not landed)
@@ -1028,7 +1079,7 @@ def _validate_task_graph(
1028
1079
 
1029
1080
  for index, left in enumerate(task_ids):
1030
1081
  for right in task_ids[index + 1 :]:
1031
- if task_waves[left] != task_waves[right]:
1082
+ if task_waves[left] != task_waves[right] or task_repos[left] != task_repos[right]:
1032
1083
  continue
1033
1084
  overlap = sorted(set(task_files[left]) & set(task_files[right]))
1034
1085
  if overlap:
@@ -1039,6 +1090,7 @@ def _validate_task_graph(
1039
1090
  visiting: Set[str] = set()
1040
1091
  visited: Set[str] = set()
1041
1092
  dependency_files: Dict[str, Set[str]] = {}
1093
+ dependency_pairs: Dict[str, Set[Tuple[str, str]]] = {}
1042
1094
 
1043
1095
  def visit(task_id: str) -> None:
1044
1096
  if task_id in visiting:
@@ -1046,11 +1098,15 @@ def _validate_task_graph(
1046
1098
  if task_id in visited:
1047
1099
  return
1048
1100
  visiting.add(task_id)
1049
- dependency_files[task_id] = set()
1101
+ dependency_pairs[task_id] = set()
1050
1102
  for dependency in graph[task_id]:
1051
1103
  visit(dependency)
1052
- dependency_files[task_id].update(task_files[dependency])
1053
- dependency_files[task_id].update(dependency_files[dependency])
1104
+ dependency_pairs[task_id].update((task_repos[dependency], name) for name in task_files[dependency])
1105
+ dependency_pairs[task_id].update(dependency_pairs[dependency])
1106
+ # A dependency's files are supplied only within the same repo.
1107
+ dependency_files[task_id] = {
1108
+ name for repo, name in dependency_pairs[task_id] if repo == task_repos[task_id]
1109
+ }
1054
1110
  visiting.remove(task_id)
1055
1111
  visited.add(task_id)
1056
1112
 
@@ -1212,7 +1268,7 @@ def _head_file(root: Path, relative: str) -> Optional[str]:
1212
1268
  ["git", "show", f"HEAD:{relative}"],
1213
1269
  cwd=root,
1214
1270
  capture_output=True,
1215
- text=True,
1271
+ encoding="utf-8", errors="replace",
1216
1272
  check=False,
1217
1273
  )
1218
1274
  return result.stdout if result.returncode == 0 else None
@@ -1313,6 +1369,21 @@ def validate_roadmap(
1313
1369
  return {"phase": "roadmap", "milestones": ids, "dependencies": dependencies}
1314
1370
 
1315
1371
 
1372
+ def _integrated_members(root: Path, state: Dict[str, str]) -> Set[str]:
1373
+ """Members this milestone already closed; a new task for one would need a second merge."""
1374
+ match = re.fullmatch(r"gsd-path/M(\d{3,})", state.get("branch") or "")
1375
+ common = subprocess.run(["git", "-C", str(root), "rev-parse", "--path-format=absolute", "--git-common-dir"],
1376
+ capture_output=True, text=True, check=False)
1377
+ if match is None or common.returncode != 0:
1378
+ return set()
1379
+ if __package__:
1380
+ from . import members
1381
+ else:
1382
+ import members
1383
+ rows = members.milestone_member_close(Path(common.stdout.strip()), int(match.group(1)))
1384
+ return {row["name"] for row in rows if row.get("status") == "integrated"}
1385
+
1386
+
1316
1387
  def validate_plan(
1317
1388
  root: Path, project_dir: str = DEFAULT_PROJECT_DIR
1318
1389
  ) -> Dict[str, object]:
@@ -1332,6 +1403,14 @@ def validate_plan(
1332
1403
  # Build readiness owns task lifecycle validation; coverage must survive landings.
1333
1404
  state = _require_pipeline(root, project_dir)
1334
1405
  _validate_task_graph(wave_depths, tasks, initial=state["phase"] == "plan")
1406
+ integrated = _integrated_members(root, state)
1407
+ for task_id, text in tasks.items():
1408
+ repo = str(_strict_frontmatter(text, task_id).get("repo") or "")
1409
+ if repo in integrated and _task_scalar(text, task_id, "status") != "done":
1410
+ raise HandoffError(
1411
+ f"{task_id} changes member {repo}, which is already integrated in this milestone; "
1412
+ "plan that change in a new milestone"
1413
+ )
1335
1414
  assigned: Dict[str, Set[str]] = {task_id: set() for task_id in tasks}
1336
1415
  covered = set()
1337
1416
  for criterion, task_id, acceptance in rows:
@@ -1591,6 +1670,19 @@ def validate_wave_evidence(
1591
1670
  }
1592
1671
 
1593
1672
 
1673
+ def criterion_surfaces(root: Path, project_dir: str = DEFAULT_PROJECT_DIR) -> Dict[str, str]:
1674
+ """Map each surface-owned INTENT SC to the surface PLAN.md's contract lists it under."""
1675
+
1676
+ surface_of: Dict[str, str] = {}
1677
+ surfaces = _surfaces(_read(root, _intent_path(project_dir)), "INTENT.md")
1678
+ if surfaces:
1679
+ plan = _read(root, f"{project_dir}/plan/PLAN.md")
1680
+ for surface, (_task, owned) in _surface_contract(plan, surfaces).items():
1681
+ for criterion in owned:
1682
+ surface_of[criterion] = surface
1683
+ return surface_of
1684
+
1685
+
1594
1686
  def validate_final(
1595
1687
  root: Path, project_dir: str = DEFAULT_PROJECT_DIR, *, final_text: Optional[str] = None
1596
1688
  ) -> Dict[str, object]:
@@ -1599,20 +1691,14 @@ def validate_final(
1599
1691
  _require_state(root, "ship", "active", project_dir)
1600
1692
  intent = _read(root, _intent_path(project_dir))
1601
1693
  criteria = _success_criteria(intent)
1602
- surface_of: Dict[str, str] = {}
1603
- surfaces = _surfaces(intent, "INTENT.md")
1604
- if surfaces:
1605
- plan = _read(root, f"{project_dir}/plan/PLAN.md")
1606
- for surface, (_task, owned) in _surface_contract(plan, surfaces).items():
1607
- for criterion in owned:
1608
- surface_of[criterion] = surface
1694
+ surface_of = criterion_surfaces(root, project_dir)
1609
1695
  relative = f"{project_dir}/review/FINAL.md"
1610
1696
  text = _read(root, relative) if final_text is None else final_text
1611
1697
  reviewed_head = _reviewed_head(text, relative)
1612
1698
  head_result = subprocess.run(
1613
1699
  ("git", "-C", str(root), "rev-parse", "--verify", "HEAD"),
1614
1700
  capture_output=True,
1615
- text=True,
1701
+ encoding="utf-8", errors="replace",
1616
1702
  check=False,
1617
1703
  )
1618
1704
  if head_result.returncode != 0:
@@ -1623,6 +1709,14 @@ def validate_final(
1623
1709
  f"FINAL.md Reviewed HEAD {reviewed_head} is not the current HEAD "
1624
1710
  f"{current_head}; re-run the final review on the current commit"
1625
1711
  )
1712
+ member_heads = member_reviewed_heads(text, relative)
1713
+ tips = _member_tips(root)
1714
+ if member_heads != tips:
1715
+ expected = ", ".join(f"{name} {tip}" for name, tip in tips.items()) or "none"
1716
+ raise HandoffError(
1717
+ f"FINAL.md Member reviewed HEAD lines must name each locked member head ({expected}); "
1718
+ "re-run the final review on the current member heads"
1719
+ )
1626
1720
  overall = _line_value(text, "Overall verdict:")
1627
1721
  if overall not in {"pass", "blocked"}:
1628
1722
  raise HandoffError("FINAL.md Overall verdict is invalid")
@@ -1697,6 +1791,8 @@ def validate_final(
1697
1791
  raise HandoffError(f"{gap_relative} heading does not match its file number")
1698
1792
  if _reviewed_head(gap, gap_relative) != reviewed_head:
1699
1793
  raise HandoffError(f"{gap_relative} Reviewed HEAD differs from FINAL.md")
1794
+ if member_reviewed_heads(gap, gap_relative) != member_heads:
1795
+ raise HandoffError(f"{gap_relative} Member reviewed HEAD lines differ from FINAL.md")
1700
1796
  verdict = _line_value(gap, "Gap verdict:")
1701
1797
  if verdict not in {"pass", "blocked"}:
1702
1798
  raise HandoffError(f"{gap_relative} Gap verdict is invalid")