@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
@@ -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":
@@ -25,22 +25,30 @@ from pathlib import Path, PurePosixPath
25
25
  from typing import Optional, Sequence
26
26
 
27
27
  def _load_pipeline_modules():
28
- try:
29
- import archive_milestone
30
- import isolation
31
- import pipeline_git
32
- import pipeline_state
33
- import state_checkpoint
28
+ if __package__:
29
+ from . import archive_milestone
30
+ from . import isolation
31
+ from . import pipeline_git
32
+ from . import pipeline_state
33
+ from . import state_checkpoint
34
34
  return archive_milestone, isolation, pipeline_git, pipeline_state, state_checkpoint
35
- except ModuleNotFoundError as error:
36
- if error.name not in {
37
- "archive_milestone",
38
- "isolation",
39
- "pipeline_git",
40
- "pipeline_state",
41
- "state_checkpoint",
42
- }:
43
- raise
35
+ else:
36
+ try:
37
+ import archive_milestone
38
+ import isolation
39
+ import pipeline_git
40
+ import pipeline_state
41
+ import state_checkpoint
42
+ return archive_milestone, isolation, pipeline_git, pipeline_state, state_checkpoint
43
+ except ModuleNotFoundError as error:
44
+ if error.name not in {
45
+ "archive_milestone",
46
+ "isolation",
47
+ "pipeline_git",
48
+ "pipeline_state",
49
+ "state_checkpoint",
50
+ }:
51
+ raise
44
52
  try:
45
53
  from scripts import archive_milestone, isolation, pipeline_git, pipeline_state, state_checkpoint
46
54
  return archive_milestone, isolation, pipeline_git, pipeline_state, state_checkpoint
@@ -94,7 +102,7 @@ UNDO_TRANSACTION_SCHEMA = pipeline_state.UNDO_TRANSACTION_SCHEMA
94
102
  UNDO_TRANSACTION_NAME = pipeline_state.UNDO_TRANSACTION_NAME
95
103
  ROADMAP_APPROVAL_SUBJECT = "roadmap: program roadmap approved"
96
104
  TASK_SUBJECT_RE = re.compile(r"^(T\d{3,}): .+")
97
- KINDS = ("checkpoint", "task", "uncommitted-archive", "lookahead")
105
+ KINDS = ("checkpoint", "task", "member-task", "uncommitted-archive", "lookahead")
98
106
  PROMOTION_PREFIX = "router: promote lookahead milestone "
99
107
  ABANDON_PREFIX = "build: abandon milestone "
100
108
  INTEGRATE_PREFIX = "integrate: "
@@ -528,6 +536,7 @@ def _prepare_transaction(
528
536
  branch: str,
529
537
  archive: Optional[str],
530
538
  discussion: Optional[dict[str, bytes]],
539
+ member_fields: Optional[dict[str, str]] = None,
531
540
  ) -> dict[str, object]:
532
541
  transaction = {
533
542
  "schema": UNDO_TRANSACTION_SCHEMA,
@@ -545,6 +554,8 @@ def _prepare_transaction(
545
554
  ),
546
555
  "discussion": _encode_discussion(discussion),
547
556
  }
557
+ if member_fields is not None:
558
+ transaction.update(member_fields)
548
559
  _write_json(_git_path(repo, UNDO_TRANSACTION_NAME), transaction)
549
560
  return transaction
550
561
 
@@ -641,7 +652,7 @@ def _transaction_recovery_stage(
641
652
  expected_head = str(transaction["expected_head"])
642
653
  parent = str(transaction["parent"])
643
654
  current = _optional_rev(repo, "HEAD")
644
- if transaction["kind"] == "checkpoint":
655
+ if transaction["kind"] in {"checkpoint", "member-task"}:
645
656
  allowed = {expected_head, parent}
646
657
  else:
647
658
  allowed = {expected_head}
@@ -662,8 +673,60 @@ def _transaction_recovery_stage(
662
673
  return "reset"
663
674
 
664
675
 
676
+ def _member_undo_parent(repo: Path, member: str, landing: str) -> str:
677
+ """The member landing's parent, while that landing is the unpublished bound tip."""
678
+ checkout, _, entry = isolation._member_context(repo, member)
679
+ bound = entry["branch"]
680
+ if _run_git(checkout, "rev-parse", f"refs/heads/{bound}").stdout.strip() != landing:
681
+ raise UndoError(f"member {member} {bound} moved past this landing")
682
+ _require_member_unpublished(checkout, member, landing, bound)
683
+ return _run_git(checkout, "rev-parse", f"{landing}^").stdout.strip()
684
+
685
+
686
+ def _require_member_unpublished(checkout: Path, member: str, landing: str, bound: str) -> None:
687
+ for ref, where in (("refs/remotes/origin/main", "origin/main"), (f"refs/remotes/origin/{bound}", f"origin/{bound}")):
688
+ if _run_git(checkout, "merge-base", "--is-ancestor", landing, ref, check=False).returncode == 0:
689
+ raise UndoError(f"member {member} landing is already on {where}")
690
+
691
+
692
+ def _require_no_member_landing_journal(repo: Path) -> None:
693
+ directory = isolation.common_git_dir(repo).joinpath(*isolation.MEMBER_LANDING_DIR)
694
+ if any(directory.glob("*.json")):
695
+ raise UndoError("member landing journal is pending; recover member landing first")
696
+
697
+
698
+ def _undo_member_landing(repo: Path, transaction: dict[str, object]) -> None:
699
+ """Reset the member bound branch first; a resumed undo finds it already reset."""
700
+ member = str(transaction["member"])
701
+ checkout, _, entry = isolation._member_context(repo, member)
702
+ tip = _run_git(checkout, "rev-parse", f"refs/heads/{entry['branch']}").stdout.strip()
703
+ if tip == transaction["member_parent"]:
704
+ _require_member_unpublished(checkout, member, str(transaction["landing"]), entry["branch"])
705
+ return
706
+ if tip != transaction["landing"]:
707
+ raise UndoError(f"member {member} bound branch moved during undo")
708
+ if _member_undo_parent(repo, member, str(transaction["landing"])) != transaction["member_parent"]:
709
+ raise UndoError(f"member {member} landing parent changed during undo")
710
+ bound = Path(isolation.member_bound_checkout(repo, member)["checkout"])
711
+ if _run_git(bound, "status", "--porcelain", "--untracked-files=all").stdout:
712
+ raise UndoError(f"member {member} bound checkout is dirty")
713
+ _run_git(bound, "reset", "--hard", str(transaction["member_parent"]))
714
+
715
+
665
716
  def _finish_transaction(repo: Path, transaction: dict[str, object]) -> None:
717
+ if transaction["kind"] == "member-task":
718
+ with isolation._member_landing_lock(repo):
719
+ _finish_transaction_locked(repo, transaction)
720
+ return
721
+ _finish_transaction_locked(repo, transaction)
722
+
723
+
724
+ def _finish_transaction_locked(repo: Path, transaction: dict[str, object]) -> None:
725
+ if transaction["kind"] == "member-task":
726
+ _require_no_member_landing_journal(repo)
666
727
  stage = _transaction_recovery_stage(repo, transaction)
728
+ if transaction["kind"] == "member-task":
729
+ _undo_member_landing(repo, transaction)
667
730
  if stage == "prepared":
668
731
  _require_unpublished_reset(repo, str(transaction["branch"]))
669
732
  _reset_to(repo, str(transaction["parent"]))
@@ -675,11 +738,7 @@ def _finish_transaction(repo: Path, transaction: dict[str, object]) -> None:
675
738
  _remove_untracked_tree(repo, archive)
676
739
  path = _git_path(repo, UNDO_TRANSACTION_NAME)
677
740
  path.unlink()
678
- descriptor = os.open(path.parent, os.O_RDONLY)
679
- try:
680
- os.fsync(descriptor)
681
- finally:
682
- os.close(descriptor)
741
+ pipeline_state._common.fsync_directory(path.parent)
683
742
 
684
743
 
685
744
  def classify_undo(repo: Path) -> dict[str, object]:
@@ -821,6 +880,28 @@ def classify_undo(repo: Path) -> dict[str, object]:
821
880
  proof_error = _task_proof_error(resolved, head)
822
881
  if proof_error:
823
882
  return _blocked([f"task landing ownership is unproven: {proof_error}"])
883
+ record = isolation.MEMBER_RECORD_RE.search(body)
884
+ if record is not None:
885
+ try:
886
+ _require_no_member_landing_journal(resolved)
887
+ member_parent = _member_undo_parent(resolved, record["member"], record["landing"])
888
+ except (IsolationError, UndoError, PipelineStateError) as error:
889
+ return _blocked([f"member landing undo is unsafe: {error}"])
890
+ return {
891
+ "kind": "member-task",
892
+ "branch": recorded_branch,
893
+ "head": head,
894
+ "parent": parent,
895
+ "subject": subject,
896
+ "member": record["member"],
897
+ "landing": record["landing"],
898
+ "member_parent": member_parent,
899
+ "effects": [
900
+ f"reset member {record['member']} bound branch to {member_parent}",
901
+ f"reset {recorded_branch} to {parent}",
902
+ ],
903
+ "blocked": [],
904
+ }
824
905
  return {
825
906
  "kind": "task",
826
907
  "branch": recorded_branch,
@@ -978,6 +1059,20 @@ def apply_undo(repo: Path, kind: str, expected_head: str) -> dict[str, object]:
978
1059
  discussion,
979
1060
  )
980
1061
  _finish_transaction(resolved, transaction)
1062
+ elif kind == "member-task":
1063
+ with isolation._member_landing_lock(resolved):
1064
+ _require_no_member_landing_journal(resolved)
1065
+ if preview(resolved)["target"] != target:
1066
+ raise UndoError("member task undo target changed after preview")
1067
+ if _worktree_changes(resolved):
1068
+ raise UndoError("worktree changed after member task undo preview")
1069
+ _require_unpublished_reset(resolved, str(target["branch"]))
1070
+ transaction = _prepare_transaction(
1071
+ resolved, kind, expected_head, str(target["parent"]), str(target["branch"]), None, None,
1072
+ {"member": str(target["member"]), "landing": str(target["landing"]),
1073
+ "member_parent": str(target["member_parent"])},
1074
+ )
1075
+ _finish_transaction_locked(resolved, transaction)
981
1076
  elif kind == "task":
982
1077
  parent = target["parent"]
983
1078
  if not parent:
@@ -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,