@opengsd/gsd-path 1.3.0 → 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 +294 -0
  3. package/DOCS.md +19 -10
  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 +61 -12
  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 +55 -39
  31. package/scripts/install.py +564 -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 +49 -11
  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
@@ -174,13 +174,13 @@ def set_value(repo, scope, key, value, reset=False):
174
174
  if scope == 'project':
175
175
  state, _, _ = sibling('pipeline_state').load_state(repo)
176
176
  branch = subprocess.run(['git', '-C', str(repo), 'branch', '--show-current'],
177
- capture_output=True, text=True)
177
+ capture_output=True, encoding="utf-8", errors="replace")
178
178
  if branch.returncode or not state.branch or branch.stdout.strip() != state.branch:
179
179
  raise ValueError('Run the router to bind the project branch before editing settings.')
180
180
  head = subprocess.run(['git', '-C', str(repo), 'rev-parse', '--verify', 'HEAD'], capture_output=True)
181
181
  if head.returncode == 0:
182
182
  closed = subprocess.run([sys.executable, '-B', str(Path(__file__).with_name('git_guard.py')),
183
- 'closed-milestone'], cwd=repo, capture_output=True, text=True)
183
+ 'closed-milestone'], cwd=repo, capture_output=True, encoding="utf-8", errors="replace")
184
184
  if closed.returncode:
185
185
  raise ValueError(closed.stderr.strip() or 'Cannot verify the milestone branch is open.')
186
186
  if scope == 'project' and key == 'integration':
@@ -23,10 +23,13 @@ sys.dont_write_bytecode = True
23
23
  from pathlib import Path
24
24
  from typing import Optional, Sequence
25
25
 
26
- try:
27
- import _common
28
- except ImportError: # pragma: no cover - package import used by tests
29
- from scripts import _common
26
+ if __package__:
27
+ from . import _common
28
+ else:
29
+ try:
30
+ import _common
31
+ except ImportError: # pragma: no cover - package import used by tests
32
+ from scripts import _common
30
33
 
31
34
 
32
35
  ARCHIVE_NAME_RE = re.compile(r"^(\d{3,})-([a-z0-9][a-z0-9-]*)$")
@@ -123,8 +126,24 @@ def is_integrate_subject(subject: str, archive_name: str, default_branch: str) -
123
126
  }
124
127
 
125
128
 
126
- def ship_commit_body(archive_path: str, reviewed_head: str) -> str:
127
- return f"Archive: {archive_path}\nReviewed-HEAD: {reviewed_head}\n"
129
+ MEMBER_SHIP_ROW = re.compile(
130
+ r"(?m)^Member-Reviewed-HEAD: (?P<name>[^@\s]+)@(?P<reviewed_head>[0-9a-f]{40})\n"
131
+ r"Member: (?P=name) (?P<merge>[0-9a-f]{40}) (?P<tag>\S+)$"
132
+ )
133
+
134
+
135
+ def ship_member_rows(body: str) -> list:
136
+ """Closed-member rows named by a ship body; callers compare the rebuilt body exactly."""
137
+ return [match.groupdict() for match in MEMBER_SHIP_ROW.finditer(body)]
138
+
139
+
140
+ def ship_commit_body(archive_path: str, reviewed_head: str, members: Sequence[dict] = ()) -> str:
141
+ """The ship body; a multi-repo milestone adds each closed member in lock order."""
142
+ return f"Archive: {archive_path}\nReviewed-HEAD: {reviewed_head}\n" + "".join(
143
+ f"Member-Reviewed-HEAD: {row['name']}@{row['reviewed_head']}\n"
144
+ f"Member: {row['name']} {row['merge']} {row['tag']}\n"
145
+ for row in members
146
+ )
128
147
 
129
148
 
130
149
  def integrate_commit_body(
@@ -200,7 +219,7 @@ def _run_git(
200
219
  result = subprocess.run(
201
220
  ["git", "-C", str(repo), *args],
202
221
  capture_output=True,
203
- text=True,
222
+ encoding="utf-8", errors="replace",
204
223
  )
205
224
  if check and result.returncode != 0:
206
225
  detail = result.stderr.strip() or result.stdout.strip()
@@ -319,7 +338,7 @@ def _validated_shipped_state(repo: Path) -> dict[str, object]:
319
338
  str(repo),
320
339
  ],
321
340
  capture_output=True,
322
- text=True,
341
+ encoding="utf-8", errors="replace",
323
342
  )
324
343
  if result.returncode != 0:
325
344
  detail = result.stderr.strip() or result.stdout.strip()
@@ -446,19 +465,16 @@ def _write_bind_next_journal(path: Path, value: dict[str, object]) -> None:
446
465
  raise PipelineGitError(f"bind-next journal temporary path exists: {temporary}")
447
466
  descriptor: Optional[int] = None
448
467
  try:
449
- descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
450
- with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
468
+ descriptor = os.open(
469
+ temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | _common.O_BINARY, 0o600
470
+ )
471
+ with os.fdopen(descriptor, "wb") as handle:
451
472
  descriptor = None
452
- json.dump(value, handle, indent=2, sort_keys=True)
453
- handle.write("\n")
473
+ handle.write((json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8"))
454
474
  handle.flush()
455
475
  os.fsync(handle.fileno())
456
- os.replace(temporary, path)
457
- directory = os.open(path.parent, os.O_RDONLY)
458
- try:
459
- os.fsync(directory)
460
- finally:
461
- os.close(directory)
476
+ _common.replace(temporary, path)
477
+ _common.fsync_directory(path.parent)
462
478
  finally:
463
479
  if descriptor is not None:
464
480
  os.close(descriptor)
@@ -473,10 +489,13 @@ def _initial_binding_snapshot(repo: Path) -> Optional[tuple]:
473
489
  return None
474
490
  if dirty != "?? .project/STATE.md\0":
475
491
  raise PipelineGitError("primary worktree is not clean")
476
- try:
477
- import pipeline_state
478
- except ImportError: # pragma: no cover - package import used by tests
479
- from scripts import pipeline_state
492
+ if __package__:
493
+ from . import pipeline_state
494
+ else:
495
+ try:
496
+ import pipeline_state
497
+ except ImportError: # pragma: no cover - package import used by tests
498
+ from scripts import pipeline_state
480
499
 
481
500
  project = repo / ".project"
482
501
  path = project / "STATE.md"
@@ -635,6 +654,25 @@ def retire_previous_branch(
635
654
  _run_git(repo, "branch", "-d", previous_branch)
636
655
 
637
656
 
657
+ def _retire_members(repo: Path, ship_sha: str) -> None:
658
+ """Retire each member the ship commit names; each step is idempotent for a resumed bind-next."""
659
+ message = _run_git(repo, "show", "-s", "--format=%B", ship_sha).stdout
660
+ rows = ship_member_rows(message)
661
+ if not rows:
662
+ return
663
+ if __package__:
664
+ from . import integration
665
+ else:
666
+ import integration
667
+ archive = re.search(r"(?m)^Archive: (\S+)$", message)
668
+ try:
669
+ for row in rows:
670
+ integration.retire_member(repo, row["name"], archive.group(1), row["reviewed_head"],
671
+ row["merge"], row["tag"])
672
+ except integration.ArchiveError as error:
673
+ raise PipelineGitError(f"member retirement failed: {error}") from error
674
+
675
+
638
676
  def bind_next_milestone_branch(
639
677
  repo: Path,
640
678
  branch: str,
@@ -868,6 +906,7 @@ def bind_next_milestone_branch(
868
906
  allow_remote_absent or journal["stage"] in {"switched", "retired"}
869
907
  ),
870
908
  )
909
+ _retire_members(repo, ship_sha)
871
910
  journal["stage"] = "retired"
872
911
  _write_bind_next_journal(journal_path, journal)
873
912
  return {
@@ -27,25 +27,35 @@ from datetime import date
27
27
  from pathlib import Path, PurePosixPath
28
28
  from typing import Iterator, Mapping, Optional, Sequence
29
29
 
30
- try:
31
- from isolation import (
30
+ if __package__:
31
+ from .isolation import (
32
32
  IsolationError,
33
33
  authorized_task_worktree,
34
34
  checkpoint as isolation_checkpoint,
35
35
  collect_artifact_recoveries,
36
36
  )
37
- import _common
38
- import roadmap
39
- except ModuleNotFoundError as error: # pragma: no cover - package imports used by tests
40
- if error.name not in {"isolation", "_common", "roadmap"}:
41
- raise
42
- from scripts.isolation import (
43
- IsolationError,
44
- authorized_task_worktree,
45
- checkpoint as isolation_checkpoint,
46
- collect_artifact_recoveries,
47
- )
48
- from scripts import _common, roadmap
37
+ from . import _common
38
+ from . import roadmap
39
+ else:
40
+ try:
41
+ from isolation import (
42
+ IsolationError,
43
+ authorized_task_worktree,
44
+ checkpoint as isolation_checkpoint,
45
+ collect_artifact_recoveries,
46
+ )
47
+ import _common
48
+ import roadmap
49
+ except ModuleNotFoundError as error: # pragma: no cover - package imports used by tests
50
+ if error.name not in {"isolation", "_common", "roadmap"}:
51
+ raise
52
+ from scripts.isolation import (
53
+ IsolationError,
54
+ authorized_task_worktree,
55
+ checkpoint as isolation_checkpoint,
56
+ collect_artifact_recoveries,
57
+ )
58
+ from scripts import _common, roadmap
49
59
 
50
60
  try: # pragma: no cover - exercised only on Windows
51
61
  import fcntl
@@ -66,6 +76,8 @@ PHASES = (
66
76
  "shipped",
67
77
  )
68
78
  STATUSES = ("active", "done", "blocked")
79
+ PRE_APPROVAL_KINDS = ("intent", "plan")
80
+ PRE_APPROVAL_MARKER = "pre-approval: "
69
81
  STATE_FIELDS = (
70
82
  "pipeline",
71
83
  "project",
@@ -184,7 +196,7 @@ def _run_git(
184
196
  result = subprocess.run(
185
197
  ["git", "-C", str(repo), *arguments],
186
198
  capture_output=True,
187
- text=True,
199
+ encoding="utf-8", errors="replace",
188
200
  )
189
201
  if check and result.returncode != 0:
190
202
  detail = result.stderr.strip() or result.stdout.strip()
@@ -410,14 +422,17 @@ def undo_transaction(repo: Path) -> Optional[dict[str, object]]:
410
422
  "archive_fingerprint",
411
423
  "discussion",
412
424
  }
425
+ if value.get("kind") == "member-task":
426
+ # A member landing undo also names the member reset it performs first.
427
+ expected |= {"member", "landing", "member_parent"}
413
428
  if set(value) != expected or value.get("schema") != UNDO_TRANSACTION_SCHEMA:
414
429
  raise PipelineStateError("undo transaction has invalid fields")
415
430
  if value.get("repo") != str(resolved):
416
431
  raise PipelineStateError("undo transaction belongs to another worktree")
417
432
  kind = value.get("kind")
418
- if kind not in {"checkpoint", "uncommitted-archive"}:
433
+ if kind not in {"checkpoint", "uncommitted-archive", "member-task"}:
419
434
  raise PipelineStateError("undo transaction has invalid kind")
420
- for field in ("expected_head", "parent"):
435
+ for field in ("expected_head", "parent") + (("landing", "member_parent") if kind == "member-task" else ()):
421
436
  field_value = value.get(field)
422
437
  if not isinstance(field_value, str) or not re.fullmatch(
423
438
  r"[0-9a-f]{40,64}", field_value
@@ -433,10 +448,12 @@ def undo_transaction(repo: Path) -> Optional[dict[str, object]]:
433
448
  raise PipelineStateError("undo transaction has invalid worktree fingerprint")
434
449
  archive = value.get("archive")
435
450
  archive_fingerprint = value.get("archive_fingerprint")
436
- if kind == "checkpoint" and (
451
+ if kind in {"checkpoint", "member-task"} and (
437
452
  archive is not None or archive_fingerprint is not None
438
453
  ):
439
- raise PipelineStateError("checkpoint undo transaction cannot name an archive")
454
+ raise PipelineStateError(f"{kind} undo transaction cannot name an archive")
455
+ if kind == "member-task" and not isinstance(value.get("member"), str):
456
+ raise PipelineStateError("member-task undo transaction has invalid member")
440
457
  if kind == "uncommitted-archive" and (
441
458
  not isinstance(archive, str) or ARCHIVE_RE.fullmatch(archive) is None
442
459
  ):
@@ -563,12 +580,15 @@ def _pending_answers(repo: Path) -> tuple[list[dict[str, str]], Optional[str]]:
563
580
  if not answers.exists() and not answers.is_symlink():
564
581
  return [], None
565
582
  try:
566
- try:
567
- from discussion_records import DiscussionError, pending_records
568
- except ModuleNotFoundError as error: # pragma: no cover - package imports used by tests
569
- if error.name != "discussion_records":
570
- raise
571
- from scripts.discussion_records import DiscussionError, pending_records
583
+ if __package__:
584
+ from .discussion_records import DiscussionError, pending_records
585
+ else:
586
+ try:
587
+ from discussion_records import DiscussionError, pending_records
588
+ except ModuleNotFoundError as error: # pragma: no cover - package imports used by tests
589
+ if error.name != "discussion_records":
590
+ raise
591
+ from scripts.discussion_records import DiscussionError, pending_records
572
592
  except ModuleNotFoundError as error:
573
593
  if error.name not in {"scripts", "scripts.discussion_records"}:
574
594
  raise
@@ -624,6 +644,29 @@ def _completion_status(repo: Path, state: Mapping[str, object], project_dir: str
624
644
  return {"status": "verified", "proof": proof}
625
645
 
626
646
 
647
+ def _member_status(repo: Path, state: Mapping[str, object]) -> Optional[list]:
648
+ """Locked members and their ship close, when this milestone has a member lock."""
649
+ if __package__:
650
+ from scripts import members
651
+ else:
652
+ import members
653
+ archive = state.get("archive")
654
+ locks = [repo / ".project" / "build" / "members.json"]
655
+ if isinstance(archive, str) and archive not in {"", "null"}:
656
+ locks.insert(0, repo / archive.rstrip("/") / "build" / "members.json")
657
+ lock = next((path for path in locks if path.is_file()), None)
658
+ if lock is None:
659
+ return None
660
+ closed = {}
661
+ match = re.fullmatch(r"gsd-path/M(\d{3,})", str(state.get("branch") or ""))
662
+ if match is not None:
663
+ common = Path(_run_git(repo, "rev-parse", "--path-format=absolute", "--git-common-dir").stdout.strip())
664
+ closed = {row["name"]: row.get("status") for row in members.milestone_member_close(common, int(match.group(1)))}
665
+ entries = json.loads(lock.read_text(encoding="utf-8"))["members"]
666
+ return [{"name": entry["name"], "branch": entry["branch"], "close": closed.get(entry["name"])}
667
+ for entry in entries]
668
+
669
+
627
670
  def status_state(repo: Path, project_dir: str = ".project") -> dict[str, object]:
628
671
  """Report owned state, route, and git facts without advancing a phase."""
629
672
  resolved = _repo_root(repo)
@@ -658,7 +701,8 @@ def status_state(repo: Path, project_dir: str = ".project") -> dict[str, object]
658
701
  "collect_artifact": None,
659
702
  }
660
703
  completion = _completion_status(resolved, state, project_dir)
661
- return {
704
+ member_rows = _member_status(resolved, state) if has_git and project_dir == ".project" else None
705
+ result = {
662
706
  "schema": STATUS_SCHEMA,
663
707
  "advance": False,
664
708
  "completion": completion,
@@ -688,6 +732,9 @@ def status_state(repo: Path, project_dir: str = ".project") -> dict[str, object]
688
732
  "next_skill": _next_skill(route if isinstance(route, dict) else {}),
689
733
  "handoff": phase_handoff(state, route, _track_root(resolved, project_dir) / "STATE.md", completion),
690
734
  }
735
+ if member_rows is not None:
736
+ result["members"] = member_rows
737
+ return result
691
738
 
692
739
 
693
740
  def phase_handoff(state: Mapping[str, object], route: Mapping[str, object], path: Path,
@@ -759,7 +806,7 @@ def _valid_patch_findings(repo: Path) -> bool:
759
806
  str(repo),
760
807
  ],
761
808
  capture_output=True,
762
- text=True,
809
+ encoding="utf-8", errors="replace",
763
810
  )
764
811
  return result.returncode == 0
765
812
 
@@ -791,6 +838,8 @@ def _selected_initial_branch(project: Path, state: PipelineState) -> str:
791
838
  if archive.is_symlink() or not archive.is_dir():
792
839
  raise PipelineStateError(f"archive root must be a real directory: {archive}")
793
840
  for child in archive.iterdir():
841
+ if _common.is_ignored_junk(child):
842
+ continue
794
843
  match = re.fullmatch(r"(\d{3,})-[a-z0-9][a-z0-9-]*", child.name)
795
844
  if (
796
845
  child.is_symlink()
@@ -1381,8 +1430,11 @@ def _append_event(
1381
1430
 
1382
1431
  @contextlib.contextmanager
1383
1432
  def _state_lock(project: Path) -> Iterator[None]:
1384
- if fcntl is None: # pragma: no cover - Windows CI is not used
1385
- raise PipelineStateError("state locking is unavailable on this platform")
1433
+ if fcntl is None:
1434
+ # Windows cannot flock a directory.
1435
+ with _common.directory_mutex(project):
1436
+ yield
1437
+ return
1386
1438
  descriptor = os.open(project, os.O_RDONLY)
1387
1439
  try:
1388
1440
  fcntl.flock(descriptor, fcntl.LOCK_EX)
@@ -1408,27 +1460,21 @@ def _atomic_write(path: Path, content: str) -> None:
1408
1460
  and os.path.samestat(path_status, temporary_status)
1409
1461
  ):
1410
1462
  temporary.unlink()
1411
- directory = os.open(path.parent, os.O_RDONLY)
1412
- try:
1413
- os.fsync(directory)
1414
- finally:
1415
- os.close(directory)
1463
+ _common.fsync_directory(path.parent)
1416
1464
  if temporary.exists() or temporary.is_symlink():
1417
1465
  raise PipelineStateError(f"atomic-write temporary path already exists: {temporary}")
1418
1466
  descriptor: Optional[int] = None
1419
1467
  try:
1420
- descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
1421
- with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
1468
+ descriptor = os.open(
1469
+ temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | _common.O_BINARY, 0o666
1470
+ )
1471
+ with os.fdopen(descriptor, "wb") as handle:
1422
1472
  descriptor = None
1423
- handle.write(content)
1473
+ handle.write(content.encode("utf-8"))
1424
1474
  handle.flush()
1425
1475
  os.fsync(handle.fileno())
1426
- os.replace(temporary, path)
1427
- directory = os.open(path.parent, os.O_RDONLY)
1428
- try:
1429
- os.fsync(directory)
1430
- finally:
1431
- os.close(directory)
1476
+ _common.replace(temporary, path)
1477
+ _common.fsync_directory(path.parent)
1432
1478
  finally:
1433
1479
  if descriptor is not None:
1434
1480
  os.close(descriptor)
@@ -1668,12 +1714,6 @@ def transition_state(
1668
1714
  if recovery_context and recovery_context["active"]:
1669
1715
  if (state.milestone, state.branch, state.archive) != (after.milestone, after.branch, after.archive):
1670
1716
  raise PipelineStateError("build recovery must preserve milestone identity")
1671
- if state.phase == "build" and after.phase in {"define", "plan"}:
1672
- recovery = _build_recovery().begin(resolved, state, after, event)
1673
- rendered = _append_event(
1674
- rendered, after.phase,
1675
- _build_recovery().MARKER + json.dumps(recovery, sort_keys=True),
1676
- )
1677
1717
  if after.status != "blocked":
1678
1718
  reason = _pending_discussion_block(
1679
1719
  resolved, after.phase if after.status == "active" else None
@@ -1683,10 +1723,13 @@ def transition_state(
1683
1723
  if state.phase in {"research", "decide"} and (
1684
1724
  after.phase != state.phase or after.status == "done"
1685
1725
  ):
1686
- try:
1687
- import check_handoffs
1688
- except ImportError: # pragma: no cover - package imports used by tests
1689
- from scripts import check_handoffs
1726
+ if __package__:
1727
+ from . import check_handoffs
1728
+ else:
1729
+ try:
1730
+ import check_handoffs
1731
+ except ImportError: # pragma: no cover - package imports used by tests
1732
+ from scripts import check_handoffs
1690
1733
  validator = {
1691
1734
  "research": check_handoffs.validate_research_artifacts,
1692
1735
  "decide": check_handoffs.validate_decide_artifacts,
@@ -1695,6 +1738,15 @@ def transition_state(
1695
1738
  validator(resolved, project_dir)
1696
1739
  except check_handoffs.HandoffError as error:
1697
1740
  raise PipelineStateError(f"{state.phase} handoff failed: {error}") from error
1741
+ if (project_dir == ".project" and (state.phase, state.status) == ("plan", "done")
1742
+ and (after.phase, after.status) == ("build", "active")):
1743
+ _lock_build_members(resolved)
1744
+ if state.phase == "build" and after.phase in {"define", "plan"}:
1745
+ recovery = _build_recovery().begin(resolved, state, after, event)
1746
+ rendered = _append_event(
1747
+ rendered, after.phase,
1748
+ _build_recovery().MARKER + json.dumps(recovery, sort_keys=True),
1749
+ )
1698
1750
  _atomic_write(path, rendered)
1699
1751
  return {
1700
1752
  "schema": STATE_SCHEMA,
@@ -1705,6 +1757,17 @@ def transition_state(
1705
1757
  }
1706
1758
 
1707
1759
 
1760
+ def _lock_build_members(repo: Path) -> None:
1761
+ if __package__:
1762
+ from scripts import members
1763
+ else:
1764
+ import members
1765
+ try:
1766
+ members.lock_build_members(repo)
1767
+ except members.MembersError as error:
1768
+ raise PipelineStateError(f"build start: {error}") from error
1769
+
1770
+
1708
1771
  def _build_recovery():
1709
1772
  if __package__:
1710
1773
  from scripts import build_recovery
@@ -1776,6 +1839,55 @@ def configure_integration(
1776
1839
  }
1777
1840
 
1778
1841
 
1842
+ def _log_events(text: str) -> list[str]:
1843
+ return [line.split(" — ", 2)[-1] for line in text.splitlines()
1844
+ if line.startswith("- ") and " — " in line]
1845
+
1846
+
1847
+ def pre_approve(repo: Path, grant: Optional[str], use: Optional[str]) -> dict[str, object]:
1848
+ """Record an owner's pre-approval grant, or record one granted gate's use."""
1849
+ resolved = _repo_root(repo)
1850
+ project = _track_root(resolved, ".project")
1851
+ with _state_lock(project):
1852
+ state, text, path = load_state(resolved)
1853
+ if grant is not None:
1854
+ kinds = grant.split(",")
1855
+ if not kinds or len(set(kinds)) != len(kinds) or not set(kinds) <= set(PRE_APPROVAL_KINDS):
1856
+ raise PipelineStateError(
1857
+ f"pre-approval kinds must be distinct values from: {', '.join(PRE_APPROVAL_KINDS)}"
1858
+ )
1859
+ if state.milestone is not None:
1860
+ raise PipelineStateError("pre-approval is granted only before milestone intent approval")
1861
+ event = PRE_APPROVAL_MARKER + json.dumps({"kinds": kinds}, sort_keys=True)
1862
+ else:
1863
+ events = _log_events(text)
1864
+ starts = [index for index, event in enumerate(events) if event.startswith(PRE_APPROVAL_MARKER)]
1865
+ if not starts:
1866
+ raise PipelineStateError("no pre-approval grant is recorded")
1867
+ try:
1868
+ recorded = json.loads(events[starts[-1]][len(PRE_APPROVAL_MARKER):])
1869
+ kinds = recorded["kinds"]
1870
+ except (ValueError, TypeError, KeyError) as error:
1871
+ raise PipelineStateError("invalid pre-approval record") from error
1872
+ if not isinstance(kinds, list):
1873
+ raise PipelineStateError("invalid pre-approval record")
1874
+ if (_build_recovery().context(resolved, text) or {}).get("active") or (project / "review/PATCH-FINDINGS.md").exists():
1875
+ raise PipelineStateError("pre-approval never covers build recovery or patch planning")
1876
+ if use not in kinds:
1877
+ raise PipelineStateError(f"pre-approval does not grant {use}")
1878
+ # The canonical approval event, not this record, consumes the grant.
1879
+ approvals = events[starts[-1] + 1:].count("milestone intent approved")
1880
+ expected = {"intent": ("define", "active", 0), "plan": ("plan", "active", 1)}[use]
1881
+ if (state.phase, state.status, approvals) != expected or (
1882
+ (state.milestone is None) != (use == "intent")
1883
+ ):
1884
+ raise PipelineStateError(f"pre-approval for {use} is used or out of scope")
1885
+ event = f"pre-authorized approval: {use}"
1886
+ rendered = _append_event(text, state.phase, event)
1887
+ _atomic_write(path, rendered)
1888
+ return {"schema": STATE_SCHEMA, "status": "recorded", "event": event, "state": state.json()}
1889
+
1890
+
1779
1891
  def _shipment_roadmap(text: str, state: PipelineState, archive: str) -> str:
1780
1892
  if state.milestone is None or state.branch is None:
1781
1893
  raise PipelineStateError("shipment state must name milestone and branch")
@@ -2016,7 +2128,8 @@ def _commit_message(subject: str, body_fields: Mapping[str, str]) -> str:
2016
2128
  def _validate_next_layout(next_root: Path, state: PipelineState) -> None:
2017
2129
  phase_index = LOOKAHEAD_PHASES.index(state.phase)
2018
2130
  for path in next_root.rglob("*"):
2019
- relative = path.relative_to(next_root)
2131
+ # POSIX form: a WindowsPath never equals the PurePosixPath layout below.
2132
+ relative = PurePosixPath(path.relative_to(next_root).as_posix())
2020
2133
  if path.is_symlink():
2021
2134
  raise PipelineStateError(f"lookahead artifacts must not be symlinks: {path}")
2022
2135
  if path.is_dir():
@@ -2028,6 +2141,8 @@ def _validate_next_layout(next_root: Path, state: PipelineState) -> None:
2028
2141
  continue
2029
2142
  if not path.is_file():
2030
2143
  raise PipelineStateError(f"lookahead has unsafe artifact: {path}")
2144
+ if _common.is_ignored_junk(path):
2145
+ continue
2031
2146
  artifact = relative.as_posix()
2032
2147
  required_phase = LOOKAHEAD_FIXED_ARTIFACTS.get(artifact)
2033
2148
  if required_phase is None and LOOKAHEAD_EVIDENCE_RE.fullmatch(artifact):
@@ -2112,6 +2227,11 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
2112
2227
  configure.add_argument("--project-dir", default=".project")
2113
2228
  configure.add_argument("--scope", required=True, choices=("default", "milestone"))
2114
2229
  configure.add_argument("--mode", required=True, choices=INTEGRATION_MODES)
2230
+ pre = subparsers.add_parser("pre-approve")
2231
+ pre.add_argument("--repo", required=True, type=Path)
2232
+ pre_mode = pre.add_mutually_exclusive_group(required=True)
2233
+ pre_mode.add_argument("--grant")
2234
+ pre_mode.add_argument("--use", choices=PRE_APPROVAL_KINDS)
2115
2235
  shipment = subparsers.add_parser("record-shipment")
2116
2236
  shipment.add_argument("--repo", required=True, type=Path)
2117
2237
  shipment.add_argument("--archive", required=True)
@@ -2200,6 +2320,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2200
2320
  base,
2201
2321
  landing,
2202
2322
  )
2323
+ elif args.command == "pre-approve":
2324
+ result = pre_approve(args.repo, args.grant, args.use)
2203
2325
  elif args.command == "record-shipment":
2204
2326
  result = record_shipment(args.repo, args.archive, args.event)
2205
2327
  elif args.command == "configure-integration":
@@ -205,7 +205,7 @@ def select_fetched_base(
205
205
  resolved = subprocess.run(
206
206
  ["git", "-C", str(root), "rev-parse", "--verify", f"{remote_default}^{{commit}}"],
207
207
  capture_output=True,
208
- text=True,
208
+ encoding="utf-8", errors="replace",
209
209
  check=False,
210
210
  )
211
211
  if resolved.returncode != 0 or resolved.stdout.strip() != base:
@@ -470,7 +470,7 @@ def merge_artifacts(
470
470
  lines.append(artifact["text"].rstrip())
471
471
  lines.append("")
472
472
  output.parent.mkdir(parents=True, exist_ok=True)
473
- output.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
473
+ output.write_bytes(("\n".join(lines).rstrip() + "\n").encode("utf-8"))
474
474
  return {
475
475
  "output": str(output.resolve()),
476
476
  "families": families,