@opengsd/gsd-path 1.3.1 → 1.5.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 (481) hide show
  1. package/AGENTS.md +2 -207
  2. package/CHANGELOG.md +331 -69
  3. package/DOCS.md +25 -9
  4. package/FULL.md +12 -5
  5. package/GUIDE.md +1 -1
  6. package/HOOKS.md +104 -14
  7. package/QUICK.md +5 -2
  8. package/README.md +131 -111
  9. package/RUNTIME.md +2 -2
  10. package/UPDATE.md +61 -11
  11. package/WORKFLOW.md +44 -7
  12. package/package.json +13 -9
  13. package/platforms/shared-agents/dispatch.md +13 -0
  14. package/scripts/_common.py +619 -18
  15. package/scripts/archive_milestone.py +160 -86
  16. package/scripts/bootstrap_repository.py +8 -5
  17. package/scripts/build_recovery.py +100 -32
  18. package/scripts/build_state.py +143 -24
  19. package/scripts/check_docs_audit.py +91 -7
  20. package/scripts/check_handoffs.py +662 -50
  21. package/scripts/check_task_briefs.py +338 -30
  22. package/scripts/check_update.py +2 -2
  23. package/scripts/core_hook_gate.py +10 -1
  24. package/scripts/core_hook_settings.py +11 -4
  25. package/scripts/detect_project.py +587 -32
  26. package/scripts/discussion_records.py +19 -13
  27. package/scripts/discussion_validate.py +152 -50
  28. package/scripts/dispatch_driver.py +501 -46
  29. package/scripts/git_guard.py +118 -31
  30. package/scripts/guard_hook.py +1567 -194
  31. package/scripts/install.mjs +77 -46
  32. package/scripts/install.py +611 -50
  33. package/scripts/integration.py +580 -25
  34. package/scripts/isolation.py +1438 -57
  35. package/scripts/lean_verification.py +99 -19
  36. package/scripts/loop_run.py +31 -29
  37. package/scripts/members.py +706 -0
  38. package/scripts/migrate_core.py +2 -2
  39. package/scripts/path_config.py +5 -5
  40. package/scripts/pipeline_diagnose.py +82 -31
  41. package/scripts/pipeline_git.py +124 -34
  42. package/scripts/pipeline_state.py +287 -87
  43. package/scripts/pipeline_undo.py +218 -30
  44. package/scripts/promote_lookahead.py +1 -1
  45. package/scripts/review_panel.py +1 -1
  46. package/scripts/runtime_store.py +55 -11
  47. package/scripts/skill-resources.json +37 -5
  48. package/scripts/state_checkpoint.py +135 -44
  49. package/scripts/state_promote.py +2 -0
  50. package/scripts/status_runtime.py +58 -10
  51. package/scripts/sync_skill_resources.py +8 -7
  52. package/scripts/task_context.py +12 -3
  53. package/scripts/token_budget.py +12 -8
  54. package/scripts/wizard.mjs +1 -0
  55. package/scripts/workflow_run.py +107 -30
  56. package/scripts/worktree_paths.py +72 -10
  57. package/skills/gsd-path/BUILD.md +7 -1
  58. package/skills/gsd-path/DECIDE.md +11 -4
  59. package/skills/gsd-path/DEFINE.md +36 -4
  60. package/skills/gsd-path/DOCS-AUDIT.md +6 -4
  61. package/skills/gsd-path/INSPECT.md +5 -4
  62. package/skills/gsd-path/PLAN.md +18 -4
  63. package/skills/gsd-path/RESEARCH.md +13 -1
  64. package/skills/gsd-path/ROADMAP.md +2 -1
  65. package/skills/gsd-path/SHIP.md +33 -11
  66. package/skills/gsd-path/SKILL.md +45 -1
  67. package/skills/gsd-path/references/build-native.md +63 -15
  68. package/skills/gsd-path/references/codebase-mapper.md +2 -1
  69. package/skills/gsd-path/references/dispatch.md +13 -0
  70. package/skills/gsd-path/references/docs-auditor.md +7 -1
  71. package/skills/gsd-path/references/operating-rules.md +214 -0
  72. package/skills/gsd-path/references/planner.md +22 -4
  73. package/skills/gsd-path/references/reviewer-final.md +20 -2
  74. package/skills/gsd-path/references/reviewer-wave.md +18 -5
  75. package/skills/gsd-path/references/reviewer.md +3 -1
  76. package/skills/gsd-path/references/spec-probes.md +150 -0
  77. package/skills/gsd-path/scripts/_common.py +619 -18
  78. package/skills/gsd-path/scripts/archive_milestone.py +160 -86
  79. package/skills/gsd-path/scripts/bootstrap_repository.py +8 -5
  80. package/skills/gsd-path/scripts/build_recovery.py +100 -32
  81. package/skills/gsd-path/scripts/build_state.py +143 -24
  82. package/skills/gsd-path/scripts/check_docs_audit.py +91 -7
  83. package/skills/gsd-path/scripts/check_handoffs.py +662 -50
  84. package/skills/gsd-path/scripts/check_task_briefs.py +338 -30
  85. package/skills/gsd-path/scripts/check_update.py +2 -2
  86. package/skills/gsd-path/scripts/detect_project.py +587 -32
  87. package/skills/gsd-path/scripts/discussion_records.py +19 -13
  88. package/skills/gsd-path/scripts/discussion_validate.py +152 -50
  89. package/skills/gsd-path/scripts/dispatch_driver.py +501 -46
  90. package/skills/gsd-path/scripts/git_guard.py +118 -31
  91. package/skills/gsd-path/scripts/integration.py +580 -25
  92. package/skills/gsd-path/scripts/isolation.py +1438 -57
  93. package/skills/gsd-path/scripts/lean_verification.py +99 -19
  94. package/skills/gsd-path/scripts/loop_run.py +31 -29
  95. package/skills/gsd-path/scripts/members.py +706 -0
  96. package/skills/gsd-path/scripts/path_config.py +5 -5
  97. package/skills/gsd-path/scripts/pipeline_git.py +124 -34
  98. package/skills/gsd-path/scripts/pipeline_state.py +287 -87
  99. package/skills/gsd-path/scripts/promote_lookahead.py +1 -1
  100. package/skills/gsd-path/scripts/review_panel.py +1 -1
  101. package/skills/gsd-path/scripts/state_checkpoint.py +135 -44
  102. package/skills/gsd-path/scripts/state_promote.py +2 -0
  103. package/skills/gsd-path/scripts/status_runtime.py +58 -10
  104. package/skills/gsd-path/scripts/task_context.py +12 -3
  105. package/skills/gsd-path/scripts/token_budget.py +12 -8
  106. package/skills/gsd-path/scripts/workflow_run.py +107 -30
  107. package/skills/gsd-path/scripts/worktree_paths.py +72 -10
  108. package/skills/gsd-path/templates/codebase.md +1 -1
  109. package/skills/gsd-path/templates/docs-audit.md +6 -2
  110. package/skills/gsd-path/templates/final-review.md +16 -4
  111. package/skills/gsd-path/templates/gap-review.md +2 -1
  112. package/skills/gsd-path/templates/intent.md +30 -0
  113. package/skills/gsd-path/templates/patch-findings.md +3 -3
  114. package/skills/gsd-path/templates/plan.md +17 -2
  115. package/skills/gsd-path/templates/research-handoff.md +14 -3
  116. package/skills/gsd-path/templates/state.md +1 -1
  117. package/skills/gsd-path/templates/task.md +1 -0
  118. package/skills/gsd-path/templates/wave-review.md +16 -4
  119. package/skills/gsd-path-build/SKILL.md +7 -1
  120. package/skills/gsd-path-build/references/build-native.md +63 -15
  121. package/skills/gsd-path-build/references/dispatch.md +13 -0
  122. package/skills/gsd-path-build/references/operating-rules.md +214 -0
  123. package/skills/gsd-path-build/references/reviewer-final.md +20 -2
  124. package/skills/gsd-path-build/references/reviewer-wave.md +18 -5
  125. package/skills/gsd-path-build/references/reviewer.md +3 -1
  126. package/skills/gsd-path-build/references/spec-probes.md +150 -0
  127. package/skills/gsd-path-build/scripts/_common.py +619 -18
  128. package/skills/gsd-path-build/scripts/archive_milestone.py +160 -86
  129. package/skills/gsd-path-build/scripts/build_recovery.py +100 -32
  130. package/skills/gsd-path-build/scripts/build_state.py +143 -24
  131. package/skills/gsd-path-build/scripts/check_handoffs.py +662 -50
  132. package/skills/gsd-path-build/scripts/check_task_briefs.py +338 -30
  133. package/skills/gsd-path-build/scripts/detect_project.py +587 -32
  134. package/skills/gsd-path-build/scripts/discussion_records.py +19 -13
  135. package/skills/gsd-path-build/scripts/discussion_validate.py +152 -50
  136. package/skills/gsd-path-build/scripts/dispatch_driver.py +501 -46
  137. package/skills/gsd-path-build/scripts/integration.py +580 -25
  138. package/skills/gsd-path-build/scripts/isolation.py +1438 -57
  139. package/skills/gsd-path-build/scripts/loop_run.py +31 -29
  140. package/skills/gsd-path-build/scripts/members.py +706 -0
  141. package/skills/gsd-path-build/scripts/path_config.py +5 -5
  142. package/skills/gsd-path-build/scripts/pipeline_git.py +124 -34
  143. package/skills/gsd-path-build/scripts/pipeline_state.py +287 -87
  144. package/skills/gsd-path-build/scripts/promote_lookahead.py +1 -1
  145. package/skills/gsd-path-build/scripts/review_panel.py +1 -1
  146. package/skills/gsd-path-build/scripts/state_checkpoint.py +135 -44
  147. package/skills/gsd-path-build/scripts/state_promote.py +2 -0
  148. package/skills/gsd-path-build/scripts/task_context.py +12 -3
  149. package/skills/gsd-path-build/scripts/token_budget.py +12 -8
  150. package/skills/gsd-path-build/scripts/workflow_run.py +107 -30
  151. package/skills/gsd-path-build/scripts/worktree_paths.py +72 -10
  152. package/skills/gsd-path-build/templates/task.md +1 -0
  153. package/skills/gsd-path-build/templates/wave-review.md +16 -4
  154. package/skills/gsd-path-decide/SKILL.md +11 -4
  155. package/skills/gsd-path-decide/references/dispatch.md +13 -0
  156. package/skills/gsd-path-decide/references/operating-rules.md +214 -0
  157. package/skills/gsd-path-decide/scripts/_common.py +619 -18
  158. package/skills/gsd-path-decide/scripts/archive_milestone.py +160 -86
  159. package/skills/gsd-path-decide/scripts/build_recovery.py +100 -32
  160. package/skills/gsd-path-decide/scripts/check_handoffs.py +662 -50
  161. package/skills/gsd-path-decide/scripts/check_task_briefs.py +338 -30
  162. package/skills/gsd-path-decide/scripts/detect_project.py +587 -32
  163. package/skills/gsd-path-decide/scripts/discussion_records.py +19 -13
  164. package/skills/gsd-path-decide/scripts/discussion_validate.py +152 -50
  165. package/skills/gsd-path-decide/scripts/integration.py +580 -25
  166. package/skills/gsd-path-decide/scripts/isolation.py +1438 -57
  167. package/skills/gsd-path-decide/scripts/loop_run.py +31 -29
  168. package/skills/gsd-path-decide/scripts/members.py +706 -0
  169. package/skills/gsd-path-decide/scripts/path_config.py +5 -5
  170. package/skills/gsd-path-decide/scripts/pipeline_git.py +124 -34
  171. package/skills/gsd-path-decide/scripts/pipeline_state.py +287 -87
  172. package/skills/gsd-path-decide/scripts/promote_lookahead.py +1 -1
  173. package/skills/gsd-path-decide/scripts/review_panel.py +1 -1
  174. package/skills/gsd-path-decide/scripts/state_checkpoint.py +135 -44
  175. package/skills/gsd-path-decide/scripts/state_promote.py +2 -0
  176. package/skills/gsd-path-decide/scripts/worktree_paths.py +72 -10
  177. package/skills/gsd-path-define/SKILL.md +36 -4
  178. package/skills/gsd-path-define/references/operating-rules.md +214 -0
  179. package/skills/gsd-path-define/references/spec-probes.md +150 -0
  180. package/skills/gsd-path-define/scripts/_common.py +619 -18
  181. package/skills/gsd-path-define/scripts/archive_milestone.py +160 -86
  182. package/skills/gsd-path-define/scripts/build_recovery.py +100 -32
  183. package/skills/gsd-path-define/scripts/check_handoffs.py +662 -50
  184. package/skills/gsd-path-define/scripts/check_task_briefs.py +338 -30
  185. package/skills/gsd-path-define/scripts/detect_project.py +587 -32
  186. package/skills/gsd-path-define/scripts/discussion_records.py +19 -13
  187. package/skills/gsd-path-define/scripts/discussion_validate.py +152 -50
  188. package/skills/gsd-path-define/scripts/integration.py +580 -25
  189. package/skills/gsd-path-define/scripts/isolation.py +1438 -57
  190. package/skills/gsd-path-define/scripts/loop_run.py +31 -29
  191. package/skills/gsd-path-define/scripts/members.py +706 -0
  192. package/skills/gsd-path-define/scripts/path_config.py +5 -5
  193. package/skills/gsd-path-define/scripts/pipeline_git.py +124 -34
  194. package/skills/gsd-path-define/scripts/pipeline_state.py +287 -87
  195. package/skills/gsd-path-define/scripts/promote_lookahead.py +1 -1
  196. package/skills/gsd-path-define/scripts/review_panel.py +1 -1
  197. package/skills/gsd-path-define/scripts/state_checkpoint.py +135 -44
  198. package/skills/gsd-path-define/scripts/state_promote.py +2 -0
  199. package/skills/gsd-path-define/scripts/worktree_paths.py +72 -10
  200. package/skills/gsd-path-define/templates/intent.md +30 -0
  201. package/skills/gsd-path-define/templates/state.md +1 -1
  202. package/skills/gsd-path-discuss/SKILL.md +2 -1
  203. package/skills/gsd-path-discuss/references/operating-rules.md +214 -0
  204. package/skills/gsd-path-discuss/scripts/_common.py +619 -18
  205. package/skills/gsd-path-discuss/scripts/discussion_records.py +19 -13
  206. package/skills/gsd-path-discuss/scripts/loop_run.py +31 -29
  207. package/skills/gsd-path-discuss/scripts/path_config.py +5 -5
  208. package/skills/gsd-path-discuss/scripts/review_panel.py +1 -1
  209. package/skills/gsd-path-discuss/scripts/worktree_paths.py +72 -10
  210. package/skills/gsd-path-docs-audit/SKILL.md +6 -4
  211. package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
  212. package/skills/gsd-path-docs-audit/references/docs-auditor.md +7 -1
  213. package/skills/gsd-path-docs-audit/references/operating-rules.md +214 -0
  214. package/skills/gsd-path-docs-audit/scripts/_common.py +619 -18
  215. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +160 -86
  216. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +100 -32
  217. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +91 -7
  218. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +662 -50
  219. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +338 -30
  220. package/skills/gsd-path-docs-audit/scripts/detect_project.py +587 -32
  221. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +19 -13
  222. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +152 -50
  223. package/skills/gsd-path-docs-audit/scripts/integration.py +580 -25
  224. package/skills/gsd-path-docs-audit/scripts/isolation.py +1438 -57
  225. package/skills/gsd-path-docs-audit/scripts/loop_run.py +31 -29
  226. package/skills/gsd-path-docs-audit/scripts/members.py +706 -0
  227. package/skills/gsd-path-docs-audit/scripts/path_config.py +5 -5
  228. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +124 -34
  229. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +287 -87
  230. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +1 -1
  231. package/skills/gsd-path-docs-audit/scripts/review_panel.py +1 -1
  232. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +135 -44
  233. package/skills/gsd-path-docs-audit/scripts/state_promote.py +2 -0
  234. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +72 -10
  235. package/skills/gsd-path-docs-audit/templates/docs-audit.md +6 -2
  236. package/skills/gsd-path-forensics/SKILL.md +2 -1
  237. package/skills/gsd-path-forensics/references/operating-rules.md +214 -0
  238. package/skills/gsd-path-forensics/scripts/_common.py +619 -18
  239. package/skills/gsd-path-forensics/scripts/discussion_records.py +19 -13
  240. package/skills/gsd-path-forensics/scripts/loop_run.py +31 -29
  241. package/skills/gsd-path-forensics/scripts/path_config.py +5 -5
  242. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +82 -31
  243. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +218 -30
  244. package/skills/gsd-path-forensics/scripts/review_panel.py +1 -1
  245. package/skills/gsd-path-forensics/scripts/worktree_paths.py +72 -10
  246. package/skills/gsd-path-inspect/SKILL.md +5 -4
  247. package/skills/gsd-path-inspect/references/codebase-mapper.md +2 -1
  248. package/skills/gsd-path-inspect/references/dispatch.md +13 -0
  249. package/skills/gsd-path-inspect/references/docs-auditor.md +7 -1
  250. package/skills/gsd-path-inspect/references/operating-rules.md +214 -0
  251. package/skills/gsd-path-inspect/scripts/_common.py +619 -18
  252. package/skills/gsd-path-inspect/scripts/archive_milestone.py +160 -86
  253. package/skills/gsd-path-inspect/scripts/build_recovery.py +100 -32
  254. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +91 -7
  255. package/skills/gsd-path-inspect/scripts/check_handoffs.py +662 -50
  256. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +338 -30
  257. package/skills/gsd-path-inspect/scripts/detect_project.py +587 -32
  258. package/skills/gsd-path-inspect/scripts/discussion_records.py +19 -13
  259. package/skills/gsd-path-inspect/scripts/discussion_validate.py +152 -50
  260. package/skills/gsd-path-inspect/scripts/integration.py +580 -25
  261. package/skills/gsd-path-inspect/scripts/isolation.py +1438 -57
  262. package/skills/gsd-path-inspect/scripts/loop_run.py +31 -29
  263. package/skills/gsd-path-inspect/scripts/members.py +706 -0
  264. package/skills/gsd-path-inspect/scripts/path_config.py +5 -5
  265. package/skills/gsd-path-inspect/scripts/pipeline_git.py +124 -34
  266. package/skills/gsd-path-inspect/scripts/pipeline_state.py +287 -87
  267. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +1 -1
  268. package/skills/gsd-path-inspect/scripts/review_panel.py +1 -1
  269. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +135 -44
  270. package/skills/gsd-path-inspect/scripts/state_promote.py +2 -0
  271. package/skills/gsd-path-inspect/scripts/workflow_run.py +107 -30
  272. package/skills/gsd-path-inspect/scripts/worktree_paths.py +72 -10
  273. package/skills/gsd-path-inspect/templates/codebase.md +1 -1
  274. package/skills/gsd-path-inspect/templates/docs-audit.md +6 -2
  275. package/skills/gsd-path-inspect/templates/state.md +1 -1
  276. package/skills/gsd-path-loop/SKILL.md +2 -1
  277. package/skills/gsd-path-loop/references/operating-rules.md +214 -0
  278. package/skills/gsd-path-loop/scripts/_common.py +619 -18
  279. package/skills/gsd-path-loop/scripts/discussion_records.py +19 -13
  280. package/skills/gsd-path-loop/scripts/loop_run.py +31 -29
  281. package/skills/gsd-path-loop/scripts/path_config.py +5 -5
  282. package/skills/gsd-path-loop/scripts/review_panel.py +1 -1
  283. package/skills/gsd-path-loop/scripts/worktree_paths.py +72 -10
  284. package/skills/gsd-path-migrate/SKILL.md +2 -1
  285. package/skills/gsd-path-migrate/references/operating-rules.md +214 -0
  286. package/skills/gsd-path-migrate/scripts/_common.py +619 -18
  287. package/skills/gsd-path-migrate/scripts/core_hook_gate.py +10 -1
  288. package/skills/gsd-path-migrate/scripts/core_hook_settings.py +11 -4
  289. package/skills/gsd-path-migrate/scripts/discussion_records.py +19 -13
  290. package/skills/gsd-path-migrate/scripts/loop_run.py +31 -29
  291. package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -2
  292. package/skills/gsd-path-migrate/scripts/path_config.py +5 -5
  293. package/skills/gsd-path-migrate/scripts/review_panel.py +1 -1
  294. package/skills/gsd-path-plan/SKILL.md +18 -4
  295. package/skills/gsd-path-plan/references/dispatch.md +13 -0
  296. package/skills/gsd-path-plan/references/operating-rules.md +214 -0
  297. package/skills/gsd-path-plan/references/planner.md +22 -4
  298. package/skills/gsd-path-plan/references/spec-probes.md +150 -0
  299. package/skills/gsd-path-plan/scripts/_common.py +619 -18
  300. package/skills/gsd-path-plan/scripts/archive_milestone.py +160 -86
  301. package/skills/gsd-path-plan/scripts/build_recovery.py +100 -32
  302. package/skills/gsd-path-plan/scripts/check_handoffs.py +662 -50
  303. package/skills/gsd-path-plan/scripts/check_task_briefs.py +338 -30
  304. package/skills/gsd-path-plan/scripts/detect_project.py +587 -32
  305. package/skills/gsd-path-plan/scripts/discussion_records.py +19 -13
  306. package/skills/gsd-path-plan/scripts/discussion_validate.py +152 -50
  307. package/skills/gsd-path-plan/scripts/integration.py +580 -25
  308. package/skills/gsd-path-plan/scripts/isolation.py +1438 -57
  309. package/skills/gsd-path-plan/scripts/loop_run.py +31 -29
  310. package/skills/gsd-path-plan/scripts/members.py +706 -0
  311. package/skills/gsd-path-plan/scripts/path_config.py +5 -5
  312. package/skills/gsd-path-plan/scripts/pipeline_git.py +124 -34
  313. package/skills/gsd-path-plan/scripts/pipeline_state.py +287 -87
  314. package/skills/gsd-path-plan/scripts/promote_lookahead.py +1 -1
  315. package/skills/gsd-path-plan/scripts/review_panel.py +1 -1
  316. package/skills/gsd-path-plan/scripts/state_checkpoint.py +135 -44
  317. package/skills/gsd-path-plan/scripts/state_promote.py +2 -0
  318. package/skills/gsd-path-plan/scripts/workflow_run.py +107 -30
  319. package/skills/gsd-path-plan/scripts/worktree_paths.py +72 -10
  320. package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
  321. package/skills/gsd-path-plan/templates/plan.md +17 -2
  322. package/skills/gsd-path-plan/templates/task.md +1 -0
  323. package/skills/gsd-path-research/SKILL.md +13 -1
  324. package/skills/gsd-path-research/references/dispatch.md +13 -0
  325. package/skills/gsd-path-research/references/operating-rules.md +214 -0
  326. package/skills/gsd-path-research/scripts/_common.py +619 -18
  327. package/skills/gsd-path-research/scripts/archive_milestone.py +160 -86
  328. package/skills/gsd-path-research/scripts/build_recovery.py +100 -32
  329. package/skills/gsd-path-research/scripts/check_handoffs.py +662 -50
  330. package/skills/gsd-path-research/scripts/check_task_briefs.py +338 -30
  331. package/skills/gsd-path-research/scripts/detect_project.py +587 -32
  332. package/skills/gsd-path-research/scripts/discussion_records.py +19 -13
  333. package/skills/gsd-path-research/scripts/discussion_validate.py +152 -50
  334. package/skills/gsd-path-research/scripts/integration.py +580 -25
  335. package/skills/gsd-path-research/scripts/isolation.py +1438 -57
  336. package/skills/gsd-path-research/scripts/loop_run.py +31 -29
  337. package/skills/gsd-path-research/scripts/members.py +706 -0
  338. package/skills/gsd-path-research/scripts/path_config.py +5 -5
  339. package/skills/gsd-path-research/scripts/pipeline_git.py +124 -34
  340. package/skills/gsd-path-research/scripts/pipeline_state.py +287 -87
  341. package/skills/gsd-path-research/scripts/promote_lookahead.py +1 -1
  342. package/skills/gsd-path-research/scripts/review_panel.py +1 -1
  343. package/skills/gsd-path-research/scripts/state_checkpoint.py +135 -44
  344. package/skills/gsd-path-research/scripts/state_promote.py +2 -0
  345. package/skills/gsd-path-research/scripts/worktree_paths.py +72 -10
  346. package/skills/gsd-path-research/templates/research-handoff.md +14 -3
  347. package/skills/gsd-path-roadmap/SKILL.md +2 -1
  348. package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
  349. package/skills/gsd-path-roadmap/references/operating-rules.md +214 -0
  350. package/skills/gsd-path-roadmap/scripts/_common.py +619 -18
  351. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +160 -86
  352. package/skills/gsd-path-roadmap/scripts/build_recovery.py +100 -32
  353. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +662 -50
  354. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +338 -30
  355. package/skills/gsd-path-roadmap/scripts/detect_project.py +587 -32
  356. package/skills/gsd-path-roadmap/scripts/discussion_records.py +19 -13
  357. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +152 -50
  358. package/skills/gsd-path-roadmap/scripts/integration.py +580 -25
  359. package/skills/gsd-path-roadmap/scripts/isolation.py +1438 -57
  360. package/skills/gsd-path-roadmap/scripts/loop_run.py +31 -29
  361. package/skills/gsd-path-roadmap/scripts/members.py +706 -0
  362. package/skills/gsd-path-roadmap/scripts/path_config.py +5 -5
  363. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +124 -34
  364. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +287 -87
  365. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +1 -1
  366. package/skills/gsd-path-roadmap/scripts/review_panel.py +1 -1
  367. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +135 -44
  368. package/skills/gsd-path-roadmap/scripts/state_promote.py +2 -0
  369. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +72 -10
  370. package/skills/gsd-path-ship/SKILL.md +33 -11
  371. package/skills/gsd-path-ship/references/dispatch.md +13 -0
  372. package/skills/gsd-path-ship/references/operating-rules.md +214 -0
  373. package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
  374. package/skills/gsd-path-ship/references/reviewer-wave.md +18 -5
  375. package/skills/gsd-path-ship/references/reviewer.md +3 -1
  376. package/skills/gsd-path-ship/references/spec-probes.md +150 -0
  377. package/skills/gsd-path-ship/scripts/_common.py +619 -18
  378. package/skills/gsd-path-ship/scripts/archive_milestone.py +160 -86
  379. package/skills/gsd-path-ship/scripts/build_recovery.py +100 -32
  380. package/skills/gsd-path-ship/scripts/build_state.py +143 -24
  381. package/skills/gsd-path-ship/scripts/check_handoffs.py +662 -50
  382. package/skills/gsd-path-ship/scripts/check_task_briefs.py +338 -30
  383. package/skills/gsd-path-ship/scripts/detect_project.py +587 -32
  384. package/skills/gsd-path-ship/scripts/discussion_records.py +19 -13
  385. package/skills/gsd-path-ship/scripts/discussion_validate.py +152 -50
  386. package/skills/gsd-path-ship/scripts/integration.py +580 -25
  387. package/skills/gsd-path-ship/scripts/isolation.py +1438 -57
  388. package/skills/gsd-path-ship/scripts/lean_verification.py +99 -19
  389. package/skills/gsd-path-ship/scripts/loop_run.py +31 -29
  390. package/skills/gsd-path-ship/scripts/members.py +706 -0
  391. package/skills/gsd-path-ship/scripts/path_config.py +5 -5
  392. package/skills/gsd-path-ship/scripts/pipeline_git.py +124 -34
  393. package/skills/gsd-path-ship/scripts/pipeline_state.py +287 -87
  394. package/skills/gsd-path-ship/scripts/promote_lookahead.py +1 -1
  395. package/skills/gsd-path-ship/scripts/review_panel.py +1 -1
  396. package/skills/gsd-path-ship/scripts/state_checkpoint.py +135 -44
  397. package/skills/gsd-path-ship/scripts/state_promote.py +2 -0
  398. package/skills/gsd-path-ship/scripts/workflow_run.py +107 -30
  399. package/skills/gsd-path-ship/scripts/worktree_paths.py +72 -10
  400. package/skills/gsd-path-ship/templates/final-review.md +16 -4
  401. package/skills/gsd-path-ship/templates/gap-review.md +2 -1
  402. package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
  403. package/skills/gsd-path-ship/templates/wave-review.md +16 -4
  404. package/skills/gsd-path-undo/SKILL.md +2 -1
  405. package/skills/gsd-path-undo/references/operating-rules.md +214 -0
  406. package/skills/gsd-path-undo/scripts/_common.py +619 -18
  407. package/skills/gsd-path-undo/scripts/discussion_records.py +19 -13
  408. package/skills/gsd-path-undo/scripts/loop_run.py +31 -29
  409. package/skills/gsd-path-undo/scripts/path_config.py +5 -5
  410. package/skills/gsd-path-undo/scripts/pipeline_undo.py +218 -30
  411. package/skills/gsd-path-undo/scripts/review_panel.py +1 -1
  412. package/skills/gsd-path-undo/scripts/worktree_paths.py +72 -10
  413. package/skills/path/BUILD.md +7 -1
  414. package/skills/path/DECIDE.md +11 -4
  415. package/skills/path/DEFINE.md +36 -4
  416. package/skills/path/DOCS-AUDIT.md +6 -4
  417. package/skills/path/INSPECT.md +5 -4
  418. package/skills/path/PLAN.md +18 -4
  419. package/skills/path/RESEARCH.md +13 -1
  420. package/skills/path/ROADMAP.md +2 -1
  421. package/skills/path/SHIP.md +33 -11
  422. package/skills/path/SKILL.md +45 -1
  423. package/skills/path/references/build-native.md +63 -15
  424. package/skills/path/references/codebase-mapper.md +2 -1
  425. package/skills/path/references/dispatch.md +13 -0
  426. package/skills/path/references/docs-auditor.md +7 -1
  427. package/skills/path/references/operating-rules.md +214 -0
  428. package/skills/path/references/planner.md +22 -4
  429. package/skills/path/references/reviewer-final.md +20 -2
  430. package/skills/path/references/reviewer-wave.md +18 -5
  431. package/skills/path/references/reviewer.md +3 -1
  432. package/skills/path/references/spec-probes.md +150 -0
  433. package/skills/path/scripts/_common.py +619 -18
  434. package/skills/path/scripts/archive_milestone.py +160 -86
  435. package/skills/path/scripts/bootstrap_repository.py +8 -5
  436. package/skills/path/scripts/build_recovery.py +100 -32
  437. package/skills/path/scripts/build_state.py +143 -24
  438. package/skills/path/scripts/check_docs_audit.py +91 -7
  439. package/skills/path/scripts/check_handoffs.py +662 -50
  440. package/skills/path/scripts/check_task_briefs.py +338 -30
  441. package/skills/path/scripts/check_update.py +2 -2
  442. package/skills/path/scripts/detect_project.py +587 -32
  443. package/skills/path/scripts/discussion_records.py +19 -13
  444. package/skills/path/scripts/discussion_validate.py +152 -50
  445. package/skills/path/scripts/dispatch_driver.py +501 -46
  446. package/skills/path/scripts/git_guard.py +118 -31
  447. package/skills/path/scripts/integration.py +580 -25
  448. package/skills/path/scripts/isolation.py +1438 -57
  449. package/skills/path/scripts/lean_verification.py +99 -19
  450. package/skills/path/scripts/loop_run.py +31 -29
  451. package/skills/path/scripts/members.py +706 -0
  452. package/skills/path/scripts/path_config.py +5 -5
  453. package/skills/path/scripts/pipeline_git.py +124 -34
  454. package/skills/path/scripts/pipeline_state.py +287 -87
  455. package/skills/path/scripts/promote_lookahead.py +1 -1
  456. package/skills/path/scripts/review_panel.py +1 -1
  457. package/skills/path/scripts/state_checkpoint.py +135 -44
  458. package/skills/path/scripts/state_promote.py +2 -0
  459. package/skills/path/scripts/status_runtime.py +58 -10
  460. package/skills/path/scripts/task_context.py +12 -3
  461. package/skills/path/scripts/token_budget.py +12 -8
  462. package/skills/path/scripts/workflow_run.py +107 -30
  463. package/skills/path/scripts/worktree_paths.py +72 -10
  464. package/skills/path/templates/codebase.md +1 -1
  465. package/skills/path/templates/docs-audit.md +6 -2
  466. package/skills/path/templates/final-review.md +16 -4
  467. package/skills/path/templates/gap-review.md +2 -1
  468. package/skills/path/templates/intent.md +30 -0
  469. package/skills/path/templates/patch-findings.md +3 -3
  470. package/skills/path/templates/plan.md +17 -2
  471. package/skills/path/templates/research-handoff.md +14 -3
  472. package/skills/path/templates/state.md +1 -1
  473. package/skills/path/templates/task.md +1 -0
  474. package/skills/path/templates/wave-review.md +16 -4
  475. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  476. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  477. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  478. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  479. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  480. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  481. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
@@ -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",
@@ -79,7 +91,7 @@ STATE_FIELDS = (
79
91
  "integration_source",
80
92
  )
81
93
  LEGACY_STATE_FIELDS = STATE_FIELDS[:7]
82
- INTEGRATION_MODES = ("direct", "pull-request")
94
+ INTEGRATION_MODES = ("direct", "pull-request", "external-landing")
83
95
  INTEGRATION_SOURCES = ("default", "milestone")
84
96
  NULL = "null"
85
97
  SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
@@ -129,6 +141,8 @@ CROSS_PHASE_TRANSITIONS = {
129
141
  ("decide", "done", "plan", "active"),
130
142
  ("roadmap", "done", "define", "active"),
131
143
  ("plan", "done", "build", "active"),
144
+ ("plan", "active", "define", "active"),
145
+ ("plan", "blocked", "define", "active"),
132
146
  ("build", "blocked", "define", "active"),
133
147
  ("build", "blocked", "plan", "active"),
134
148
  ("build", "active", "ship", "active"),
@@ -181,11 +195,24 @@ def _run_git(
181
195
  *arguments: str,
182
196
  check: bool = True,
183
197
  ) -> subprocess.CompletedProcess[str]:
198
+ command = _common.git_command(repo, *arguments)
184
199
  result = subprocess.run(
185
- ["git", "-C", str(repo), *arguments],
200
+ command,
186
201
  capture_output=True,
187
- text=True,
202
+ encoding="utf-8", errors="replace",
188
203
  )
204
+ if (
205
+ check
206
+ and result.returncode != 0
207
+ and arguments
208
+ and arguments[0] == "commit"
209
+ and _common.git_index_lock_retryable(result.stderr)
210
+ ):
211
+ result = subprocess.run(
212
+ command,
213
+ capture_output=True,
214
+ encoding="utf-8", errors="replace",
215
+ )
189
216
  if check and result.returncode != 0:
190
217
  detail = result.stderr.strip() or result.stdout.strip()
191
218
  raise PipelineStateError(f"git {' '.join(arguments)} failed: {detail}")
@@ -410,14 +437,17 @@ def undo_transaction(repo: Path) -> Optional[dict[str, object]]:
410
437
  "archive_fingerprint",
411
438
  "discussion",
412
439
  }
440
+ if value.get("kind") == "member-task":
441
+ # A member landing undo also names the member reset it performs first.
442
+ expected |= {"member", "landing", "member_parent"}
413
443
  if set(value) != expected or value.get("schema") != UNDO_TRANSACTION_SCHEMA:
414
444
  raise PipelineStateError("undo transaction has invalid fields")
415
445
  if value.get("repo") != str(resolved):
416
446
  raise PipelineStateError("undo transaction belongs to another worktree")
417
447
  kind = value.get("kind")
418
- if kind not in {"checkpoint", "uncommitted-archive"}:
448
+ if kind not in {"checkpoint", "uncommitted-archive", "member-task"}:
419
449
  raise PipelineStateError("undo transaction has invalid kind")
420
- for field in ("expected_head", "parent"):
450
+ for field in ("expected_head", "parent") + (("landing", "member_parent") if kind == "member-task" else ()):
421
451
  field_value = value.get(field)
422
452
  if not isinstance(field_value, str) or not re.fullmatch(
423
453
  r"[0-9a-f]{40,64}", field_value
@@ -433,10 +463,12 @@ def undo_transaction(repo: Path) -> Optional[dict[str, object]]:
433
463
  raise PipelineStateError("undo transaction has invalid worktree fingerprint")
434
464
  archive = value.get("archive")
435
465
  archive_fingerprint = value.get("archive_fingerprint")
436
- if kind == "checkpoint" and (
466
+ if kind in {"checkpoint", "member-task"} and (
437
467
  archive is not None or archive_fingerprint is not None
438
468
  ):
439
- raise PipelineStateError("checkpoint undo transaction cannot name an archive")
469
+ raise PipelineStateError(f"{kind} undo transaction cannot name an archive")
470
+ if kind == "member-task" and not isinstance(value.get("member"), str):
471
+ raise PipelineStateError("member-task undo transaction has invalid member")
440
472
  if kind == "uncommitted-archive" and (
441
473
  not isinstance(archive, str) or ARCHIVE_RE.fullmatch(archive) is None
442
474
  ):
@@ -563,12 +595,15 @@ def _pending_answers(repo: Path) -> tuple[list[dict[str, str]], Optional[str]]:
563
595
  if not answers.exists() and not answers.is_symlink():
564
596
  return [], None
565
597
  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
598
+ if __package__:
599
+ from .discussion_records import DiscussionError, pending_records
600
+ else:
601
+ try:
602
+ from discussion_records import DiscussionError, pending_records
603
+ except ModuleNotFoundError as error: # pragma: no cover - package imports used by tests
604
+ if error.name != "discussion_records":
605
+ raise
606
+ from scripts.discussion_records import DiscussionError, pending_records
572
607
  except ModuleNotFoundError as error:
573
608
  if error.name not in {"scripts", "scripts.discussion_records"}:
574
609
  raise
@@ -624,6 +659,29 @@ def _completion_status(repo: Path, state: Mapping[str, object], project_dir: str
624
659
  return {"status": "verified", "proof": proof}
625
660
 
626
661
 
662
+ def _member_status(repo: Path, state: Mapping[str, object]) -> Optional[list]:
663
+ """Locked members and their ship close, when this milestone has a member lock."""
664
+ if __package__:
665
+ from scripts import members
666
+ else:
667
+ import members
668
+ archive = state.get("archive")
669
+ locks = [repo / ".project" / "build" / "members.json"]
670
+ if isinstance(archive, str) and archive not in {"", "null"}:
671
+ locks.insert(0, repo / archive.rstrip("/") / "build" / "members.json")
672
+ lock = next((path for path in locks if path.is_file()), None)
673
+ if lock is None:
674
+ return None
675
+ closed = {}
676
+ match = re.fullmatch(r"gsd-path/M(\d{3,})", str(state.get("branch") or ""))
677
+ if match is not None:
678
+ common = Path(_run_git(repo, "rev-parse", "--path-format=absolute", "--git-common-dir").stdout.strip())
679
+ closed = {row["name"]: row.get("status") for row in members.milestone_member_close(common, int(match.group(1)))}
680
+ entries = json.loads(lock.read_text(encoding="utf-8"))["members"]
681
+ return [{"name": entry["name"], "branch": entry["branch"], "close": closed.get(entry["name"])}
682
+ for entry in entries]
683
+
684
+
627
685
  def status_state(repo: Path, project_dir: str = ".project") -> dict[str, object]:
628
686
  """Report owned state, route, and git facts without advancing a phase."""
629
687
  resolved = _repo_root(repo)
@@ -658,7 +716,8 @@ def status_state(repo: Path, project_dir: str = ".project") -> dict[str, object]
658
716
  "collect_artifact": None,
659
717
  }
660
718
  completion = _completion_status(resolved, state, project_dir)
661
- return {
719
+ member_rows = _member_status(resolved, state) if has_git and project_dir == ".project" else None
720
+ result = {
662
721
  "schema": STATUS_SCHEMA,
663
722
  "advance": False,
664
723
  "completion": completion,
@@ -688,6 +747,9 @@ def status_state(repo: Path, project_dir: str = ".project") -> dict[str, object]
688
747
  "next_skill": _next_skill(route if isinstance(route, dict) else {}),
689
748
  "handoff": phase_handoff(state, route, _track_root(resolved, project_dir) / "STATE.md", completion),
690
749
  }
750
+ if member_rows is not None:
751
+ result["members"] = member_rows
752
+ return result
691
753
 
692
754
 
693
755
  def phase_handoff(state: Mapping[str, object], route: Mapping[str, object], path: Path,
@@ -759,7 +821,7 @@ def _valid_patch_findings(repo: Path) -> bool:
759
821
  str(repo),
760
822
  ],
761
823
  capture_output=True,
762
- text=True,
824
+ encoding="utf-8", errors="replace",
763
825
  )
764
826
  return result.returncode == 0
765
827
 
@@ -791,6 +853,8 @@ def _selected_initial_branch(project: Path, state: PipelineState) -> str:
791
853
  if archive.is_symlink() or not archive.is_dir():
792
854
  raise PipelineStateError(f"archive root must be a real directory: {archive}")
793
855
  for child in archive.iterdir():
856
+ if _common.is_ignored_junk(child):
857
+ continue
794
858
  match = re.fullmatch(r"(\d{3,})-[a-z0-9][a-z0-9-]*", child.name)
795
859
  if (
796
860
  child.is_symlink()
@@ -862,36 +926,13 @@ def _legacy_bind_next_landing(repo: Path, state: PipelineState) -> str:
862
926
  return landing
863
927
 
864
928
 
865
- def _bind_next_recovery(
929
+ def _bind_next_journal_recovery(
866
930
  repo: Path,
867
931
  state: PipelineState,
932
+ path: Path,
933
+ transaction: dict[str, object],
934
+ current: Optional[str],
868
935
  ) -> Optional[dict[str, object]]:
869
- journal_root = _git_path(repo, BIND_NEXT_JOURNAL_DIR)
870
- if not journal_root.exists() and not journal_root.is_symlink():
871
- return None
872
- if journal_root.is_symlink() or not journal_root.is_dir():
873
- raise PipelineStateError(f"bind-next journal root must be a real directory: {journal_root}")
874
-
875
- matches: list[tuple[Path, dict[str, object]]] = []
876
- for path in sorted(journal_root.glob("*.json")):
877
- transaction = _read_json(path)
878
- if transaction.get("schema") not in {
879
- LEGACY_BIND_NEXT_JOURNAL_SCHEMA,
880
- BIND_NEXT_JOURNAL_SCHEMA,
881
- }:
882
- raise PipelineStateError(f"bind-next journal has invalid schema: {path}")
883
- if transaction.get("repo") != str(repo):
884
- raise PipelineStateError(f"bind-next journal belongs to another worktree: {path}")
885
- if transaction.get("previous_branch") == state.branch:
886
- matches.append((path, transaction))
887
- if not matches:
888
- return None
889
- if len(matches) != 1:
890
- raise PipelineStateError("multiple bind-next journals claim the current STATE branch")
891
- if state.phase != "shipped" or state.status != "done":
892
- raise PipelineStateError("bind-next journal requires STATE shipped/done")
893
-
894
- path, transaction = matches[0]
895
936
  required = (
896
937
  "branch",
897
938
  "previous_branch",
@@ -973,7 +1014,32 @@ def _bind_next_recovery(
973
1014
  if stage not in {"prepared", "switched", "retired"}:
974
1015
  raise PipelineStateError("bind-next journal has invalid stage")
975
1016
 
976
- current = _current_branch(repo)
1017
+ stale_retired = False
1018
+ if stage == "retired":
1019
+ target_ref = _run_git(
1020
+ repo,
1021
+ "show-ref",
1022
+ "--verify",
1023
+ "--quiet",
1024
+ f"refs/heads/{branch}",
1025
+ check=False,
1026
+ )
1027
+ if target_ref.returncode not in {0, 1}:
1028
+ detail = (target_ref.stderr or target_ref.stdout).strip()
1029
+ raise PipelineStateError(
1030
+ f"could not check bind-next target branch {branch}: {detail}"
1031
+ )
1032
+ target_exists = target_ref.returncode == 0
1033
+ stale_retired = current not in {previous, branch} or not target_exists
1034
+ if stale_retired:
1035
+ # If the completed target is gone while the previous branch is checked
1036
+ # out, still prove that branch has not moved away from the recorded ship.
1037
+ if current == previous:
1038
+ head = _run_git(repo, "rev-parse", "HEAD").stdout.strip()
1039
+ if head != ship:
1040
+ raise PipelineStateError("bind-next previous branch is not at journal ship")
1041
+ return None
1042
+
977
1043
  if current not in {previous, branch}:
978
1044
  raise PipelineStateError("bind-next journal does not own the current branch")
979
1045
  if current == previous and stage != "prepared":
@@ -1008,6 +1074,52 @@ def _bind_next_recovery(
1008
1074
  return result
1009
1075
 
1010
1076
 
1077
+ def _bind_next_recovery(
1078
+ repo: Path,
1079
+ state: PipelineState,
1080
+ ) -> Optional[dict[str, object]]:
1081
+ journal_root = _git_path(repo, BIND_NEXT_JOURNAL_DIR)
1082
+ if not journal_root.exists() and not journal_root.is_symlink():
1083
+ return None
1084
+ if journal_root.is_symlink() or not journal_root.is_dir():
1085
+ raise PipelineStateError(f"bind-next journal root must be a real directory: {journal_root}")
1086
+
1087
+ matches: list[tuple[Path, dict[str, object]]] = []
1088
+ for path in sorted(journal_root.glob("*.json")):
1089
+ transaction = _read_json(path)
1090
+ if transaction.get("schema") not in {
1091
+ LEGACY_BIND_NEXT_JOURNAL_SCHEMA,
1092
+ BIND_NEXT_JOURNAL_SCHEMA,
1093
+ }:
1094
+ raise PipelineStateError(f"bind-next journal has invalid schema: {path}")
1095
+ if transaction.get("repo") != str(repo):
1096
+ raise PipelineStateError(f"bind-next journal belongs to another worktree: {path}")
1097
+ if transaction.get("previous_branch") == state.branch:
1098
+ matches.append((path, transaction))
1099
+ if not matches:
1100
+ return None
1101
+ if state.phase != "shipped" or state.status != "done":
1102
+ if len(matches) != 1:
1103
+ raise PipelineStateError("multiple bind-next journals claim the current STATE branch")
1104
+ raise PipelineStateError("bind-next journal requires STATE shipped/done")
1105
+
1106
+ current = _current_branch(repo)
1107
+ recoveries: list[dict[str, object]] = []
1108
+ for path, transaction in matches:
1109
+ recovery = _bind_next_journal_recovery(
1110
+ repo,
1111
+ state,
1112
+ path,
1113
+ transaction,
1114
+ current,
1115
+ )
1116
+ if recovery is not None:
1117
+ recoveries.append(recovery)
1118
+ if len(recoveries) > 1:
1119
+ raise PipelineStateError("multiple bind-next journals claim the current STATE branch")
1120
+ return recoveries[0] if recoveries else None
1121
+
1122
+
1011
1123
  def _pending_discussion_block(repo: Path, phase: Optional[str]) -> Optional[str]:
1012
1124
  pending, error = _pending_answers(repo)
1013
1125
  if error:
@@ -1028,7 +1140,7 @@ def route_state(repo: Path, project_dir: str = ".project") -> dict[str, object]:
1028
1140
  result["recovery"] = recovery
1029
1141
  if route.get("phase") == "define":
1030
1142
  route["mode"] = "corrections"
1031
- elif route.get("phase") == "plan":
1143
+ elif route.get("phase") == "plan" and recovery.get("source", "build") == "build":
1032
1144
  route["mode"] = "build-repair"
1033
1145
  if route["action"] == "run-phase" and result["state"]["archive"] is None:
1034
1146
  reason = _pending_discussion_block(_repo_root(repo), route.get("phase"))
@@ -1381,8 +1493,11 @@ def _append_event(
1381
1493
 
1382
1494
  @contextlib.contextmanager
1383
1495
  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")
1496
+ if fcntl is None:
1497
+ # Windows cannot flock a directory.
1498
+ with _common.directory_mutex(project):
1499
+ yield
1500
+ return
1386
1501
  descriptor = os.open(project, os.O_RDONLY)
1387
1502
  try:
1388
1503
  fcntl.flock(descriptor, fcntl.LOCK_EX)
@@ -1408,27 +1523,21 @@ def _atomic_write(path: Path, content: str) -> None:
1408
1523
  and os.path.samestat(path_status, temporary_status)
1409
1524
  ):
1410
1525
  temporary.unlink()
1411
- directory = os.open(path.parent, os.O_RDONLY)
1412
- try:
1413
- os.fsync(directory)
1414
- finally:
1415
- os.close(directory)
1526
+ _common.fsync_directory(path.parent)
1416
1527
  if temporary.exists() or temporary.is_symlink():
1417
1528
  raise PipelineStateError(f"atomic-write temporary path already exists: {temporary}")
1418
1529
  descriptor: Optional[int] = None
1419
1530
  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:
1531
+ descriptor = os.open(
1532
+ temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | _common.O_BINARY, 0o666
1533
+ )
1534
+ with os.fdopen(descriptor, "wb") as handle:
1422
1535
  descriptor = None
1423
- handle.write(content)
1536
+ handle.write(content.encode("utf-8"))
1424
1537
  handle.flush()
1425
1538
  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)
1539
+ _common.replace(temporary, path)
1540
+ _common.fsync_directory(path.parent)
1432
1541
  finally:
1433
1542
  if descriptor is not None:
1434
1543
  os.close(descriptor)
@@ -1622,6 +1731,8 @@ def _validate_transition(
1622
1731
  },
1623
1732
  ("define", "done", "plan", "active"): {"planning started"},
1624
1733
  ("decide", "done", "plan", "active"): {"planning started"},
1734
+ ("plan", "active", "define", "active"): {"plan intent corrections requested"},
1735
+ ("plan", "blocked", "define", "active"): {"plan intent corrections requested"},
1625
1736
  ("ship", "blocked", "plan", "active"): {"patch plan reopened"},
1626
1737
  ("ship", "active", "shipped", "done"): {
1627
1738
  "archive preflight passed; shipment recorded",
@@ -1668,26 +1779,26 @@ def transition_state(
1668
1779
  if recovery_context and recovery_context["active"]:
1669
1780
  if (state.milestone, state.branch, state.archive) != (after.milestone, after.branch, after.archive):
1670
1781
  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
1782
  if after.status != "blocked":
1678
1783
  reason = _pending_discussion_block(
1679
1784
  resolved, after.phase if after.status == "active" else None
1680
1785
  )
1681
1786
  if reason:
1682
1787
  raise PipelineStateError(reason)
1683
- if state.phase in {"research", "decide"} and (
1788
+ leaving_define = (state.phase, after.phase, after.status) == ("define", "define", "done")
1789
+ if leaving_define or (state.phase in {"research", "decide"} and (
1684
1790
  after.phase != state.phase or after.status == "done"
1685
- ):
1686
- try:
1687
- import check_handoffs
1688
- except ImportError: # pragma: no cover - package imports used by tests
1689
- from scripts import check_handoffs
1791
+ )):
1792
+ if __package__:
1793
+ from . import check_handoffs
1794
+ else:
1795
+ try:
1796
+ import check_handoffs
1797
+ except ImportError: # pragma: no cover - package imports used by tests
1798
+ from scripts import check_handoffs
1690
1799
  validator = {
1800
+ # Approved intent carries its spec-reach probe tables complete.
1801
+ "define": check_handoffs.validate_intent_probes,
1691
1802
  "research": check_handoffs.validate_research_artifacts,
1692
1803
  "decide": check_handoffs.validate_decide_artifacts,
1693
1804
  }[state.phase]
@@ -1695,6 +1806,25 @@ def transition_state(
1695
1806
  validator(resolved, project_dir)
1696
1807
  except check_handoffs.HandoffError as error:
1697
1808
  raise PipelineStateError(f"{state.phase} handoff failed: {error}") from error
1809
+ if (project_dir == ".project" and (state.phase, state.status) == ("plan", "done")
1810
+ and (after.phase, after.status) == ("build", "active")):
1811
+ _lock_build_members(resolved)
1812
+ if state.phase == "build" and after.phase in {"define", "plan"}:
1813
+ recovery = _build_recovery().begin(resolved, state, after, event)
1814
+ rendered = _append_event(
1815
+ rendered, after.phase,
1816
+ _build_recovery().MARKER + json.dumps(recovery, sort_keys=True),
1817
+ )
1818
+ elif (
1819
+ state.phase == "plan"
1820
+ and state.status in {"active", "blocked"}
1821
+ and (after.phase, after.status) == ("define", "active")
1822
+ ):
1823
+ recovery = _build_recovery().begin(resolved, state, after, event)
1824
+ rendered = _append_event(
1825
+ rendered, after.phase,
1826
+ _build_recovery().MARKER + json.dumps(recovery, sort_keys=True),
1827
+ )
1698
1828
  _atomic_write(path, rendered)
1699
1829
  return {
1700
1830
  "schema": STATE_SCHEMA,
@@ -1705,6 +1835,17 @@ def transition_state(
1705
1835
  }
1706
1836
 
1707
1837
 
1838
+ def _lock_build_members(repo: Path) -> None:
1839
+ if __package__:
1840
+ from scripts import members
1841
+ else:
1842
+ import members
1843
+ try:
1844
+ members.lock_build_members(repo)
1845
+ except members.MembersError as error:
1846
+ raise PipelineStateError(f"build start: {error}") from error
1847
+
1848
+
1708
1849
  def _build_recovery():
1709
1850
  if __package__:
1710
1851
  from scripts import build_recovery
@@ -1776,6 +1917,55 @@ def configure_integration(
1776
1917
  }
1777
1918
 
1778
1919
 
1920
+ def _log_events(text: str) -> list[str]:
1921
+ return [line.split(" — ", 2)[-1] for line in text.splitlines()
1922
+ if line.startswith("- ") and " — " in line]
1923
+
1924
+
1925
+ def pre_approve(repo: Path, grant: Optional[str], use: Optional[str]) -> dict[str, object]:
1926
+ """Record an owner's pre-approval grant, or record one granted gate's use."""
1927
+ resolved = _repo_root(repo)
1928
+ project = _track_root(resolved, ".project")
1929
+ with _state_lock(project):
1930
+ state, text, path = load_state(resolved)
1931
+ if grant is not None:
1932
+ kinds = grant.split(",")
1933
+ if not kinds or len(set(kinds)) != len(kinds) or not set(kinds) <= set(PRE_APPROVAL_KINDS):
1934
+ raise PipelineStateError(
1935
+ f"pre-approval kinds must be distinct values from: {', '.join(PRE_APPROVAL_KINDS)}"
1936
+ )
1937
+ if state.milestone is not None:
1938
+ raise PipelineStateError("pre-approval is granted only before milestone intent approval")
1939
+ event = PRE_APPROVAL_MARKER + json.dumps({"kinds": kinds}, sort_keys=True)
1940
+ else:
1941
+ events = _log_events(text)
1942
+ starts = [index for index, event in enumerate(events) if event.startswith(PRE_APPROVAL_MARKER)]
1943
+ if not starts:
1944
+ raise PipelineStateError("no pre-approval grant is recorded")
1945
+ try:
1946
+ recorded = json.loads(events[starts[-1]][len(PRE_APPROVAL_MARKER):])
1947
+ kinds = recorded["kinds"]
1948
+ except (ValueError, TypeError, KeyError) as error:
1949
+ raise PipelineStateError("invalid pre-approval record") from error
1950
+ if not isinstance(kinds, list):
1951
+ raise PipelineStateError("invalid pre-approval record")
1952
+ if (_build_recovery().context(resolved, text) or {}).get("active") or (project / "review/PATCH-FINDINGS.md").exists():
1953
+ raise PipelineStateError("pre-approval never covers build recovery or patch planning")
1954
+ if use not in kinds:
1955
+ raise PipelineStateError(f"pre-approval does not grant {use}")
1956
+ # The canonical approval event, not this record, consumes the grant.
1957
+ approvals = events[starts[-1] + 1:].count("milestone intent approved")
1958
+ expected = {"intent": ("define", "active", 0), "plan": ("plan", "active", 1)}[use]
1959
+ if (state.phase, state.status, approvals) != expected or (
1960
+ (state.milestone is None) != (use == "intent")
1961
+ ):
1962
+ raise PipelineStateError(f"pre-approval for {use} is used or out of scope")
1963
+ event = f"pre-authorized approval: {use}"
1964
+ rendered = _append_event(text, state.phase, event)
1965
+ _atomic_write(path, rendered)
1966
+ return {"schema": STATE_SCHEMA, "status": "recorded", "event": event, "state": state.json()}
1967
+
1968
+
1779
1969
  def _shipment_roadmap(text: str, state: PipelineState, archive: str) -> str:
1780
1970
  if state.milestone is None or state.branch is None:
1781
1971
  raise PipelineStateError("shipment state must name milestone and branch")
@@ -2016,7 +2206,8 @@ def _commit_message(subject: str, body_fields: Mapping[str, str]) -> str:
2016
2206
  def _validate_next_layout(next_root: Path, state: PipelineState) -> None:
2017
2207
  phase_index = LOOKAHEAD_PHASES.index(state.phase)
2018
2208
  for path in next_root.rglob("*"):
2019
- relative = path.relative_to(next_root)
2209
+ # POSIX form: a WindowsPath never equals the PurePosixPath layout below.
2210
+ relative = PurePosixPath(path.relative_to(next_root).as_posix())
2020
2211
  if path.is_symlink():
2021
2212
  raise PipelineStateError(f"lookahead artifacts must not be symlinks: {path}")
2022
2213
  if path.is_dir():
@@ -2028,6 +2219,8 @@ def _validate_next_layout(next_root: Path, state: PipelineState) -> None:
2028
2219
  continue
2029
2220
  if not path.is_file():
2030
2221
  raise PipelineStateError(f"lookahead has unsafe artifact: {path}")
2222
+ if _common.is_ignored_junk(path):
2223
+ continue
2031
2224
  artifact = relative.as_posix()
2032
2225
  required_phase = LOOKAHEAD_FIXED_ARTIFACTS.get(artifact)
2033
2226
  if required_phase is None and LOOKAHEAD_EVIDENCE_RE.fullmatch(artifact):
@@ -2112,6 +2305,11 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
2112
2305
  configure.add_argument("--project-dir", default=".project")
2113
2306
  configure.add_argument("--scope", required=True, choices=("default", "milestone"))
2114
2307
  configure.add_argument("--mode", required=True, choices=INTEGRATION_MODES)
2308
+ pre = subparsers.add_parser("pre-approve")
2309
+ pre.add_argument("--repo", required=True, type=Path)
2310
+ pre_mode = pre.add_mutually_exclusive_group(required=True)
2311
+ pre_mode.add_argument("--grant")
2312
+ pre_mode.add_argument("--use", choices=PRE_APPROVAL_KINDS)
2115
2313
  shipment = subparsers.add_parser("record-shipment")
2116
2314
  shipment.add_argument("--repo", required=True, type=Path)
2117
2315
  shipment.add_argument("--archive", required=True)
@@ -2200,6 +2398,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2200
2398
  base,
2201
2399
  landing,
2202
2400
  )
2401
+ elif args.command == "pre-approve":
2402
+ result = pre_approve(args.repo, args.grant, args.use)
2203
2403
  elif args.command == "record-shipment":
2204
2404
  result = record_shipment(args.repo, args.archive, args.event)
2205
2405
  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,