@opengsd/gsd-path 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (476) hide show
  1. package/CHANGELOG.md +460 -341
  2. package/DOCS.md +16 -5
  3. package/FULL.md +67 -6
  4. package/HOOKS.md +158 -10
  5. package/MIGRATE.md +12 -0
  6. package/QUICK.md +4 -2
  7. package/README.md +136 -116
  8. package/RUNTIME.md +24 -6
  9. package/UPDATE.md +39 -3
  10. package/WORKFLOW.md +80 -4
  11. package/package.json +5 -2
  12. package/platforms/shared-agents/dispatch.md +13 -0
  13. package/scripts/_common.py +256 -2
  14. package/scripts/archive_milestone.py +14 -13
  15. package/scripts/build_recovery.py +155 -58
  16. package/scripts/build_state.py +274 -8
  17. package/scripts/check_docs_audit.py +77 -4
  18. package/scripts/check_handoffs.py +635 -57
  19. package/scripts/check_task_briefs.py +293 -32
  20. package/scripts/detect_project.py +260 -20
  21. package/scripts/discussion_records.py +9 -1
  22. package/scripts/discussion_validate.py +93 -35
  23. package/scripts/dispatch_driver.py +549 -53
  24. package/scripts/git_guard.py +27 -8
  25. package/scripts/guard_hook.py +2630 -210
  26. package/scripts/install.mjs +99 -11
  27. package/scripts/install.py +231 -46
  28. package/scripts/integration.py +244 -65
  29. package/scripts/isolation.py +539 -70
  30. package/scripts/loop_run.py +16 -4
  31. package/scripts/members.py +77 -32
  32. package/scripts/migrate_core.py +2 -0
  33. package/scripts/model_policy.py +5 -1
  34. package/scripts/path_config.py +3 -3
  35. package/scripts/pipeline_diagnose.py +17 -2
  36. package/scripts/pipeline_git.py +131 -42
  37. package/scripts/pipeline_state.py +139 -39
  38. package/scripts/pipeline_undo.py +118 -16
  39. package/scripts/promote_lookahead.py +3 -3
  40. package/scripts/review_findings.py +180 -44
  41. package/scripts/roadmap.py +9 -1
  42. package/scripts/runtime_store.py +21 -2
  43. package/scripts/skill-resources.json +9 -2
  44. package/scripts/state_checkpoint.py +39 -36
  45. package/scripts/state_promote.py +15 -4
  46. package/scripts/status_runtime.py +22 -4
  47. package/scripts/task_context.py +14 -3
  48. package/scripts/wizard.mjs +1 -0
  49. package/scripts/workflow_run.py +14 -4
  50. package/scripts/worktree_paths.py +54 -2
  51. package/skills/gsd-path/DEFINE.md +34 -5
  52. package/skills/gsd-path/PLAN.md +14 -3
  53. package/skills/gsd-path/RESEARCH.md +11 -0
  54. package/skills/gsd-path/ROADMAP.md +16 -6
  55. package/skills/gsd-path/SHIP.md +32 -5
  56. package/skills/gsd-path/SKILL.md +52 -24
  57. package/skills/gsd-path/references/build-native.md +112 -10
  58. package/skills/gsd-path/references/build-recovery.md +13 -0
  59. package/skills/gsd-path/references/coder.md +7 -3
  60. package/skills/gsd-path/references/dispatch.md +13 -0
  61. package/skills/gsd-path/references/docs-auditor.md +5 -0
  62. package/skills/gsd-path/references/operating-rules.md +11 -3
  63. package/skills/gsd-path/references/planner.md +26 -4
  64. package/skills/gsd-path/references/reviewer-final.md +20 -2
  65. package/skills/gsd-path/references/reviewer-wave.md +56 -12
  66. package/skills/gsd-path/references/reviewer.md +3 -1
  67. package/skills/gsd-path/references/runtime-selection.md +9 -0
  68. package/skills/gsd-path/references/spec-probes.md +150 -0
  69. package/skills/gsd-path/scripts/_common.py +256 -2
  70. package/skills/gsd-path/scripts/archive_milestone.py +14 -13
  71. package/skills/gsd-path/scripts/build_recovery.py +155 -58
  72. package/skills/gsd-path/scripts/build_state.py +274 -8
  73. package/skills/gsd-path/scripts/check_docs_audit.py +77 -4
  74. package/skills/gsd-path/scripts/check_handoffs.py +635 -57
  75. package/skills/gsd-path/scripts/check_task_briefs.py +293 -32
  76. package/skills/gsd-path/scripts/detect_project.py +260 -20
  77. package/skills/gsd-path/scripts/discussion_records.py +9 -1
  78. package/skills/gsd-path/scripts/discussion_validate.py +93 -35
  79. package/skills/gsd-path/scripts/dispatch_driver.py +549 -53
  80. package/skills/gsd-path/scripts/git_guard.py +27 -8
  81. package/skills/gsd-path/scripts/integration.py +244 -65
  82. package/skills/gsd-path/scripts/isolation.py +539 -70
  83. package/skills/gsd-path/scripts/loop_run.py +16 -4
  84. package/skills/gsd-path/scripts/members.py +77 -32
  85. package/skills/gsd-path/scripts/model_policy.py +5 -1
  86. package/skills/gsd-path/scripts/path_config.py +3 -3
  87. package/skills/gsd-path/scripts/pipeline_git.py +131 -42
  88. package/skills/gsd-path/scripts/pipeline_state.py +139 -39
  89. package/skills/gsd-path/scripts/promote_lookahead.py +3 -3
  90. package/skills/gsd-path/scripts/review_findings.py +180 -44
  91. package/skills/gsd-path/scripts/roadmap.py +9 -1
  92. package/skills/gsd-path/scripts/state_checkpoint.py +39 -36
  93. package/skills/gsd-path/scripts/state_promote.py +15 -4
  94. package/skills/gsd-path/scripts/status_runtime.py +22 -4
  95. package/skills/gsd-path/scripts/task_context.py +14 -3
  96. package/skills/gsd-path/scripts/workflow_run.py +14 -4
  97. package/skills/gsd-path/scripts/worktree_paths.py +54 -2
  98. package/skills/gsd-path/templates/docs-audit.md +5 -1
  99. package/skills/gsd-path/templates/final-review.md +11 -2
  100. package/skills/gsd-path/templates/intent.md +30 -0
  101. package/skills/gsd-path/templates/patch-findings.md +3 -3
  102. package/skills/gsd-path/templates/plan.md +17 -2
  103. package/skills/gsd-path/templates/research-handoff.md +14 -3
  104. package/skills/gsd-path/templates/state.md +2 -1
  105. package/skills/gsd-path/templates/task.md +1 -0
  106. package/skills/gsd-path/templates/wave-review.md +28 -9
  107. package/skills/gsd-path-build/references/build-native.md +112 -10
  108. package/skills/gsd-path-build/references/build-recovery.md +13 -0
  109. package/skills/gsd-path-build/references/coder.md +7 -3
  110. package/skills/gsd-path-build/references/dispatch.md +13 -0
  111. package/skills/gsd-path-build/references/operating-rules.md +11 -3
  112. package/skills/gsd-path-build/references/reviewer-final.md +20 -2
  113. package/skills/gsd-path-build/references/reviewer-wave.md +56 -12
  114. package/skills/gsd-path-build/references/reviewer.md +3 -1
  115. package/skills/gsd-path-build/references/runtime-selection.md +9 -0
  116. package/skills/gsd-path-build/references/spec-probes.md +150 -0
  117. package/skills/gsd-path-build/scripts/_common.py +256 -2
  118. package/skills/gsd-path-build/scripts/archive_milestone.py +14 -13
  119. package/skills/gsd-path-build/scripts/build_recovery.py +155 -58
  120. package/skills/gsd-path-build/scripts/build_state.py +274 -8
  121. package/skills/gsd-path-build/scripts/check_handoffs.py +635 -57
  122. package/skills/gsd-path-build/scripts/check_task_briefs.py +293 -32
  123. package/skills/gsd-path-build/scripts/detect_project.py +260 -20
  124. package/skills/gsd-path-build/scripts/discussion_records.py +9 -1
  125. package/skills/gsd-path-build/scripts/discussion_validate.py +93 -35
  126. package/skills/gsd-path-build/scripts/dispatch_driver.py +549 -53
  127. package/skills/gsd-path-build/scripts/integration.py +244 -65
  128. package/skills/gsd-path-build/scripts/isolation.py +539 -70
  129. package/skills/gsd-path-build/scripts/loop_run.py +16 -4
  130. package/skills/gsd-path-build/scripts/members.py +77 -32
  131. package/skills/gsd-path-build/scripts/model_policy.py +5 -1
  132. package/skills/gsd-path-build/scripts/path_config.py +3 -3
  133. package/skills/gsd-path-build/scripts/pipeline_git.py +131 -42
  134. package/skills/gsd-path-build/scripts/pipeline_state.py +139 -39
  135. package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -3
  136. package/skills/gsd-path-build/scripts/review_findings.py +180 -44
  137. package/skills/gsd-path-build/scripts/roadmap.py +9 -1
  138. package/skills/gsd-path-build/scripts/state_checkpoint.py +39 -36
  139. package/skills/gsd-path-build/scripts/state_promote.py +15 -4
  140. package/skills/gsd-path-build/scripts/task_context.py +14 -3
  141. package/skills/gsd-path-build/scripts/workflow_run.py +14 -4
  142. package/skills/gsd-path-build/scripts/worktree_paths.py +54 -2
  143. package/skills/gsd-path-build/templates/task.md +1 -0
  144. package/skills/gsd-path-build/templates/wave-review.md +28 -9
  145. package/skills/gsd-path-decide/references/dispatch.md +13 -0
  146. package/skills/gsd-path-decide/references/operating-rules.md +11 -3
  147. package/skills/gsd-path-decide/references/runtime-selection.md +9 -0
  148. package/skills/gsd-path-decide/scripts/_common.py +256 -2
  149. package/skills/gsd-path-decide/scripts/archive_milestone.py +14 -13
  150. package/skills/gsd-path-decide/scripts/build_recovery.py +155 -58
  151. package/skills/gsd-path-decide/scripts/check_handoffs.py +635 -57
  152. package/skills/gsd-path-decide/scripts/check_task_briefs.py +293 -32
  153. package/skills/gsd-path-decide/scripts/detect_project.py +260 -20
  154. package/skills/gsd-path-decide/scripts/discussion_records.py +9 -1
  155. package/skills/gsd-path-decide/scripts/discussion_validate.py +93 -35
  156. package/skills/gsd-path-decide/scripts/integration.py +244 -65
  157. package/skills/gsd-path-decide/scripts/isolation.py +539 -70
  158. package/skills/gsd-path-decide/scripts/loop_run.py +16 -4
  159. package/skills/gsd-path-decide/scripts/members.py +77 -32
  160. package/skills/gsd-path-decide/scripts/model_policy.py +5 -1
  161. package/skills/gsd-path-decide/scripts/path_config.py +3 -3
  162. package/skills/gsd-path-decide/scripts/pipeline_git.py +131 -42
  163. package/skills/gsd-path-decide/scripts/pipeline_state.py +139 -39
  164. package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -3
  165. package/skills/gsd-path-decide/scripts/roadmap.py +9 -1
  166. package/skills/gsd-path-decide/scripts/state_checkpoint.py +39 -36
  167. package/skills/gsd-path-decide/scripts/state_promote.py +15 -4
  168. package/skills/gsd-path-decide/scripts/worktree_paths.py +54 -2
  169. package/skills/gsd-path-define/SKILL.md +34 -5
  170. package/skills/gsd-path-define/references/build-recovery.md +13 -0
  171. package/skills/gsd-path-define/references/operating-rules.md +11 -3
  172. package/skills/gsd-path-define/references/runtime-selection.md +9 -0
  173. package/skills/gsd-path-define/references/spec-probes.md +150 -0
  174. package/skills/gsd-path-define/scripts/_common.py +256 -2
  175. package/skills/gsd-path-define/scripts/archive_milestone.py +14 -13
  176. package/skills/gsd-path-define/scripts/build_recovery.py +155 -58
  177. package/skills/gsd-path-define/scripts/check_handoffs.py +635 -57
  178. package/skills/gsd-path-define/scripts/check_task_briefs.py +293 -32
  179. package/skills/gsd-path-define/scripts/detect_project.py +260 -20
  180. package/skills/gsd-path-define/scripts/discussion_records.py +9 -1
  181. package/skills/gsd-path-define/scripts/discussion_validate.py +93 -35
  182. package/skills/gsd-path-define/scripts/integration.py +244 -65
  183. package/skills/gsd-path-define/scripts/isolation.py +539 -70
  184. package/skills/gsd-path-define/scripts/loop_run.py +16 -4
  185. package/skills/gsd-path-define/scripts/members.py +77 -32
  186. package/skills/gsd-path-define/scripts/model_policy.py +5 -1
  187. package/skills/gsd-path-define/scripts/path_config.py +3 -3
  188. package/skills/gsd-path-define/scripts/pipeline_git.py +131 -42
  189. package/skills/gsd-path-define/scripts/pipeline_state.py +139 -39
  190. package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -3
  191. package/skills/gsd-path-define/scripts/roadmap.py +9 -1
  192. package/skills/gsd-path-define/scripts/state_checkpoint.py +39 -36
  193. package/skills/gsd-path-define/scripts/state_promote.py +15 -4
  194. package/skills/gsd-path-define/scripts/worktree_paths.py +54 -2
  195. package/skills/gsd-path-define/templates/intent.md +30 -0
  196. package/skills/gsd-path-define/templates/state.md +2 -1
  197. package/skills/gsd-path-discuss/references/operating-rules.md +11 -3
  198. package/skills/gsd-path-discuss/references/runtime-selection.md +9 -0
  199. package/skills/gsd-path-discuss/scripts/_common.py +256 -2
  200. package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
  201. package/skills/gsd-path-discuss/scripts/loop_run.py +16 -4
  202. package/skills/gsd-path-discuss/scripts/model_policy.py +5 -1
  203. package/skills/gsd-path-discuss/scripts/path_config.py +3 -3
  204. package/skills/gsd-path-discuss/scripts/worktree_paths.py +54 -2
  205. package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
  206. package/skills/gsd-path-docs-audit/references/docs-auditor.md +5 -0
  207. package/skills/gsd-path-docs-audit/references/operating-rules.md +11 -3
  208. package/skills/gsd-path-docs-audit/references/runtime-selection.md +9 -0
  209. package/skills/gsd-path-docs-audit/scripts/_common.py +256 -2
  210. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +14 -13
  211. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +155 -58
  212. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +77 -4
  213. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +635 -57
  214. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +293 -32
  215. package/skills/gsd-path-docs-audit/scripts/detect_project.py +260 -20
  216. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +9 -1
  217. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +93 -35
  218. package/skills/gsd-path-docs-audit/scripts/integration.py +244 -65
  219. package/skills/gsd-path-docs-audit/scripts/isolation.py +539 -70
  220. package/skills/gsd-path-docs-audit/scripts/loop_run.py +16 -4
  221. package/skills/gsd-path-docs-audit/scripts/members.py +77 -32
  222. package/skills/gsd-path-docs-audit/scripts/model_policy.py +5 -1
  223. package/skills/gsd-path-docs-audit/scripts/path_config.py +3 -3
  224. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +131 -42
  225. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +139 -39
  226. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -3
  227. package/skills/gsd-path-docs-audit/scripts/roadmap.py +9 -1
  228. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +39 -36
  229. package/skills/gsd-path-docs-audit/scripts/state_promote.py +15 -4
  230. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +54 -2
  231. package/skills/gsd-path-docs-audit/templates/docs-audit.md +5 -1
  232. package/skills/gsd-path-forensics/references/operating-rules.md +11 -3
  233. package/skills/gsd-path-forensics/references/runtime-selection.md +9 -0
  234. package/skills/gsd-path-forensics/scripts/_common.py +256 -2
  235. package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
  236. package/skills/gsd-path-forensics/scripts/loop_run.py +16 -4
  237. package/skills/gsd-path-forensics/scripts/model_policy.py +5 -1
  238. package/skills/gsd-path-forensics/scripts/path_config.py +3 -3
  239. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +17 -2
  240. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +118 -16
  241. package/skills/gsd-path-forensics/scripts/worktree_paths.py +54 -2
  242. package/skills/gsd-path-inspect/references/dispatch.md +13 -0
  243. package/skills/gsd-path-inspect/references/docs-auditor.md +5 -0
  244. package/skills/gsd-path-inspect/references/operating-rules.md +11 -3
  245. package/skills/gsd-path-inspect/references/runtime-selection.md +9 -0
  246. package/skills/gsd-path-inspect/scripts/_common.py +256 -2
  247. package/skills/gsd-path-inspect/scripts/archive_milestone.py +14 -13
  248. package/skills/gsd-path-inspect/scripts/build_recovery.py +155 -58
  249. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +77 -4
  250. package/skills/gsd-path-inspect/scripts/check_handoffs.py +635 -57
  251. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +293 -32
  252. package/skills/gsd-path-inspect/scripts/detect_project.py +260 -20
  253. package/skills/gsd-path-inspect/scripts/discussion_records.py +9 -1
  254. package/skills/gsd-path-inspect/scripts/discussion_validate.py +93 -35
  255. package/skills/gsd-path-inspect/scripts/integration.py +244 -65
  256. package/skills/gsd-path-inspect/scripts/isolation.py +539 -70
  257. package/skills/gsd-path-inspect/scripts/loop_run.py +16 -4
  258. package/skills/gsd-path-inspect/scripts/members.py +77 -32
  259. package/skills/gsd-path-inspect/scripts/model_policy.py +5 -1
  260. package/skills/gsd-path-inspect/scripts/path_config.py +3 -3
  261. package/skills/gsd-path-inspect/scripts/pipeline_git.py +131 -42
  262. package/skills/gsd-path-inspect/scripts/pipeline_state.py +139 -39
  263. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -3
  264. package/skills/gsd-path-inspect/scripts/roadmap.py +9 -1
  265. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +39 -36
  266. package/skills/gsd-path-inspect/scripts/state_promote.py +15 -4
  267. package/skills/gsd-path-inspect/scripts/workflow_run.py +14 -4
  268. package/skills/gsd-path-inspect/scripts/worktree_paths.py +54 -2
  269. package/skills/gsd-path-inspect/templates/docs-audit.md +5 -1
  270. package/skills/gsd-path-inspect/templates/state.md +2 -1
  271. package/skills/gsd-path-loop/references/operating-rules.md +11 -3
  272. package/skills/gsd-path-loop/references/runtime-selection.md +9 -0
  273. package/skills/gsd-path-loop/scripts/_common.py +256 -2
  274. package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
  275. package/skills/gsd-path-loop/scripts/loop_run.py +16 -4
  276. package/skills/gsd-path-loop/scripts/model_policy.py +5 -1
  277. package/skills/gsd-path-loop/scripts/path_config.py +3 -3
  278. package/skills/gsd-path-loop/scripts/worktree_paths.py +54 -2
  279. package/skills/gsd-path-migrate/SKILL.md +8 -0
  280. package/skills/gsd-path-migrate/references/operating-rules.md +11 -3
  281. package/skills/gsd-path-migrate/references/runtime-selection.md +9 -0
  282. package/skills/gsd-path-migrate/scripts/_common.py +256 -2
  283. package/skills/gsd-path-migrate/scripts/discussion_records.py +9 -1
  284. package/skills/gsd-path-migrate/scripts/loop_run.py +16 -4
  285. package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -0
  286. package/skills/gsd-path-migrate/scripts/model_policy.py +5 -1
  287. package/skills/gsd-path-migrate/scripts/path_config.py +3 -3
  288. package/skills/gsd-path-plan/SKILL.md +14 -3
  289. package/skills/gsd-path-plan/references/build-recovery.md +13 -0
  290. package/skills/gsd-path-plan/references/dispatch.md +13 -0
  291. package/skills/gsd-path-plan/references/operating-rules.md +11 -3
  292. package/skills/gsd-path-plan/references/planner.md +26 -4
  293. package/skills/gsd-path-plan/references/runtime-selection.md +9 -0
  294. package/skills/gsd-path-plan/references/spec-probes.md +150 -0
  295. package/skills/gsd-path-plan/scripts/_common.py +256 -2
  296. package/skills/gsd-path-plan/scripts/archive_milestone.py +14 -13
  297. package/skills/gsd-path-plan/scripts/build_recovery.py +155 -58
  298. package/skills/gsd-path-plan/scripts/check_handoffs.py +635 -57
  299. package/skills/gsd-path-plan/scripts/check_task_briefs.py +293 -32
  300. package/skills/gsd-path-plan/scripts/detect_project.py +260 -20
  301. package/skills/gsd-path-plan/scripts/discussion_records.py +9 -1
  302. package/skills/gsd-path-plan/scripts/discussion_validate.py +93 -35
  303. package/skills/gsd-path-plan/scripts/integration.py +244 -65
  304. package/skills/gsd-path-plan/scripts/isolation.py +539 -70
  305. package/skills/gsd-path-plan/scripts/loop_run.py +16 -4
  306. package/skills/gsd-path-plan/scripts/members.py +77 -32
  307. package/skills/gsd-path-plan/scripts/model_policy.py +5 -1
  308. package/skills/gsd-path-plan/scripts/path_config.py +3 -3
  309. package/skills/gsd-path-plan/scripts/pipeline_git.py +131 -42
  310. package/skills/gsd-path-plan/scripts/pipeline_state.py +139 -39
  311. package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -3
  312. package/skills/gsd-path-plan/scripts/roadmap.py +9 -1
  313. package/skills/gsd-path-plan/scripts/state_checkpoint.py +39 -36
  314. package/skills/gsd-path-plan/scripts/state_promote.py +15 -4
  315. package/skills/gsd-path-plan/scripts/workflow_run.py +14 -4
  316. package/skills/gsd-path-plan/scripts/worktree_paths.py +54 -2
  317. package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
  318. package/skills/gsd-path-plan/templates/plan.md +17 -2
  319. package/skills/gsd-path-plan/templates/task.md +1 -0
  320. package/skills/gsd-path-research/SKILL.md +11 -0
  321. package/skills/gsd-path-research/references/dispatch.md +13 -0
  322. package/skills/gsd-path-research/references/operating-rules.md +11 -3
  323. package/skills/gsd-path-research/references/runtime-selection.md +9 -0
  324. package/skills/gsd-path-research/scripts/_common.py +256 -2
  325. package/skills/gsd-path-research/scripts/archive_milestone.py +14 -13
  326. package/skills/gsd-path-research/scripts/build_recovery.py +155 -58
  327. package/skills/gsd-path-research/scripts/check_handoffs.py +635 -57
  328. package/skills/gsd-path-research/scripts/check_task_briefs.py +293 -32
  329. package/skills/gsd-path-research/scripts/detect_project.py +260 -20
  330. package/skills/gsd-path-research/scripts/discussion_records.py +9 -1
  331. package/skills/gsd-path-research/scripts/discussion_validate.py +93 -35
  332. package/skills/gsd-path-research/scripts/integration.py +244 -65
  333. package/skills/gsd-path-research/scripts/isolation.py +539 -70
  334. package/skills/gsd-path-research/scripts/loop_run.py +16 -4
  335. package/skills/gsd-path-research/scripts/members.py +77 -32
  336. package/skills/gsd-path-research/scripts/model_policy.py +5 -1
  337. package/skills/gsd-path-research/scripts/path_config.py +3 -3
  338. package/skills/gsd-path-research/scripts/pipeline_git.py +131 -42
  339. package/skills/gsd-path-research/scripts/pipeline_state.py +139 -39
  340. package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -3
  341. package/skills/gsd-path-research/scripts/roadmap.py +9 -1
  342. package/skills/gsd-path-research/scripts/state_checkpoint.py +39 -36
  343. package/skills/gsd-path-research/scripts/state_promote.py +15 -4
  344. package/skills/gsd-path-research/scripts/worktree_paths.py +54 -2
  345. package/skills/gsd-path-research/templates/research-handoff.md +14 -3
  346. package/skills/gsd-path-roadmap/SKILL.md +16 -6
  347. package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
  348. package/skills/gsd-path-roadmap/references/operating-rules.md +11 -3
  349. package/skills/gsd-path-roadmap/references/runtime-selection.md +9 -0
  350. package/skills/gsd-path-roadmap/scripts/_common.py +256 -2
  351. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +14 -13
  352. package/skills/gsd-path-roadmap/scripts/build_recovery.py +155 -58
  353. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +635 -57
  354. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +293 -32
  355. package/skills/gsd-path-roadmap/scripts/detect_project.py +260 -20
  356. package/skills/gsd-path-roadmap/scripts/discussion_records.py +9 -1
  357. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +93 -35
  358. package/skills/gsd-path-roadmap/scripts/integration.py +244 -65
  359. package/skills/gsd-path-roadmap/scripts/isolation.py +539 -70
  360. package/skills/gsd-path-roadmap/scripts/loop_run.py +16 -4
  361. package/skills/gsd-path-roadmap/scripts/members.py +77 -32
  362. package/skills/gsd-path-roadmap/scripts/model_policy.py +5 -1
  363. package/skills/gsd-path-roadmap/scripts/path_config.py +3 -3
  364. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +131 -42
  365. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +139 -39
  366. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -3
  367. package/skills/gsd-path-roadmap/scripts/roadmap.py +9 -1
  368. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +39 -36
  369. package/skills/gsd-path-roadmap/scripts/state_promote.py +15 -4
  370. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +54 -2
  371. package/skills/gsd-path-ship/SKILL.md +32 -5
  372. package/skills/gsd-path-ship/references/dispatch.md +13 -0
  373. package/skills/gsd-path-ship/references/operating-rules.md +11 -3
  374. package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
  375. package/skills/gsd-path-ship/references/reviewer-wave.md +56 -12
  376. package/skills/gsd-path-ship/references/reviewer.md +3 -1
  377. package/skills/gsd-path-ship/references/runtime-selection.md +9 -0
  378. package/skills/gsd-path-ship/references/spec-probes.md +150 -0
  379. package/skills/gsd-path-ship/scripts/_common.py +256 -2
  380. package/skills/gsd-path-ship/scripts/archive_milestone.py +14 -13
  381. package/skills/gsd-path-ship/scripts/build_recovery.py +155 -58
  382. package/skills/gsd-path-ship/scripts/build_state.py +274 -8
  383. package/skills/gsd-path-ship/scripts/check_handoffs.py +635 -57
  384. package/skills/gsd-path-ship/scripts/check_task_briefs.py +293 -32
  385. package/skills/gsd-path-ship/scripts/detect_project.py +260 -20
  386. package/skills/gsd-path-ship/scripts/discussion_records.py +9 -1
  387. package/skills/gsd-path-ship/scripts/discussion_validate.py +93 -35
  388. package/skills/gsd-path-ship/scripts/integration.py +244 -65
  389. package/skills/gsd-path-ship/scripts/isolation.py +539 -70
  390. package/skills/gsd-path-ship/scripts/loop_run.py +16 -4
  391. package/skills/gsd-path-ship/scripts/members.py +77 -32
  392. package/skills/gsd-path-ship/scripts/model_policy.py +5 -1
  393. package/skills/gsd-path-ship/scripts/path_config.py +3 -3
  394. package/skills/gsd-path-ship/scripts/pipeline_git.py +131 -42
  395. package/skills/gsd-path-ship/scripts/pipeline_state.py +139 -39
  396. package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -3
  397. package/skills/gsd-path-ship/scripts/roadmap.py +9 -1
  398. package/skills/gsd-path-ship/scripts/state_checkpoint.py +39 -36
  399. package/skills/gsd-path-ship/scripts/state_promote.py +15 -4
  400. package/skills/gsd-path-ship/scripts/workflow_run.py +14 -4
  401. package/skills/gsd-path-ship/scripts/worktree_paths.py +54 -2
  402. package/skills/gsd-path-ship/templates/final-review.md +11 -2
  403. package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
  404. package/skills/gsd-path-ship/templates/wave-review.md +28 -9
  405. package/skills/gsd-path-undo/references/operating-rules.md +11 -3
  406. package/skills/gsd-path-undo/references/runtime-selection.md +9 -0
  407. package/skills/gsd-path-undo/scripts/_common.py +256 -2
  408. package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
  409. package/skills/gsd-path-undo/scripts/loop_run.py +16 -4
  410. package/skills/gsd-path-undo/scripts/model_policy.py +5 -1
  411. package/skills/gsd-path-undo/scripts/path_config.py +3 -3
  412. package/skills/gsd-path-undo/scripts/pipeline_undo.py +118 -16
  413. package/skills/gsd-path-undo/scripts/worktree_paths.py +54 -2
  414. package/skills/path/DEFINE.md +34 -5
  415. package/skills/path/PLAN.md +14 -3
  416. package/skills/path/RESEARCH.md +11 -0
  417. package/skills/path/ROADMAP.md +16 -6
  418. package/skills/path/SHIP.md +32 -5
  419. package/skills/path/SKILL.md +52 -24
  420. package/skills/path/references/build-native.md +112 -10
  421. package/skills/path/references/build-recovery.md +13 -0
  422. package/skills/path/references/coder.md +7 -3
  423. package/skills/path/references/dispatch.md +13 -0
  424. package/skills/path/references/docs-auditor.md +5 -0
  425. package/skills/path/references/operating-rules.md +11 -3
  426. package/skills/path/references/planner.md +26 -4
  427. package/skills/path/references/reviewer-final.md +20 -2
  428. package/skills/path/references/reviewer-wave.md +56 -12
  429. package/skills/path/references/reviewer.md +3 -1
  430. package/skills/path/references/runtime-selection.md +9 -0
  431. package/skills/path/references/spec-probes.md +150 -0
  432. package/skills/path/scripts/_common.py +256 -2
  433. package/skills/path/scripts/archive_milestone.py +14 -13
  434. package/skills/path/scripts/build_recovery.py +155 -58
  435. package/skills/path/scripts/build_state.py +274 -8
  436. package/skills/path/scripts/check_docs_audit.py +77 -4
  437. package/skills/path/scripts/check_handoffs.py +635 -57
  438. package/skills/path/scripts/check_task_briefs.py +293 -32
  439. package/skills/path/scripts/detect_project.py +260 -20
  440. package/skills/path/scripts/discussion_records.py +9 -1
  441. package/skills/path/scripts/discussion_validate.py +93 -35
  442. package/skills/path/scripts/dispatch_driver.py +549 -53
  443. package/skills/path/scripts/git_guard.py +27 -8
  444. package/skills/path/scripts/integration.py +244 -65
  445. package/skills/path/scripts/isolation.py +539 -70
  446. package/skills/path/scripts/loop_run.py +16 -4
  447. package/skills/path/scripts/members.py +77 -32
  448. package/skills/path/scripts/model_policy.py +5 -1
  449. package/skills/path/scripts/path_config.py +3 -3
  450. package/skills/path/scripts/pipeline_git.py +131 -42
  451. package/skills/path/scripts/pipeline_state.py +139 -39
  452. package/skills/path/scripts/promote_lookahead.py +3 -3
  453. package/skills/path/scripts/review_findings.py +180 -44
  454. package/skills/path/scripts/roadmap.py +9 -1
  455. package/skills/path/scripts/state_checkpoint.py +39 -36
  456. package/skills/path/scripts/state_promote.py +15 -4
  457. package/skills/path/scripts/status_runtime.py +22 -4
  458. package/skills/path/scripts/task_context.py +14 -3
  459. package/skills/path/scripts/workflow_run.py +14 -4
  460. package/skills/path/scripts/worktree_paths.py +54 -2
  461. package/skills/path/templates/docs-audit.md +5 -1
  462. package/skills/path/templates/final-review.md +11 -2
  463. package/skills/path/templates/intent.md +30 -0
  464. package/skills/path/templates/patch-findings.md +3 -3
  465. package/skills/path/templates/plan.md +17 -2
  466. package/skills/path/templates/research-handoff.md +14 -3
  467. package/skills/path/templates/state.md +2 -1
  468. package/skills/path/templates/task.md +1 -0
  469. package/skills/path/templates/wave-review.md +28 -9
  470. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  471. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  472. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  473. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  474. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  475. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  476. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
@@ -514,16 +514,28 @@ def run_shell(command: str, timeout: float) -> tuple[Optional[int], str]:
514
514
  """Run one spec command; a hung command counts as failed (exit None).
515
515
 
516
516
  POSIX runs /bin/sh as shell=True did; Windows runs Git for Windows bash, not
517
- cmd.exe. A timeout kills the whole process group, so a grandchild cannot
518
- keep the output pipes open.
517
+ cmd.exe. A timeout kills the whole process group when possible; if output
518
+ pipes stay open, drain them with a short grace timeout and then close them.
519
519
  """
520
520
  argv = _common.bash_argv(command) if os.name == "nt" else ["/bin/sh", "-c", command]
521
521
  process = _common.popen_group(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
522
+ stdout = b""
523
+ stderr = b""
522
524
  try:
523
525
  stdout, stderr = process.communicate(timeout=timeout)
524
- except subprocess.TimeoutExpired:
526
+ except subprocess.TimeoutExpired as expired:
525
527
  _common.kill_tree(process)
526
- stdout, stderr = process.communicate()
528
+ try:
529
+ stdout, stderr = process.communicate(timeout=5)
530
+ except subprocess.TimeoutExpired as drained:
531
+ stdout = drained.stdout or expired.stdout or b""
532
+ stderr = drained.stderr or expired.stderr or b""
533
+ if process.stdout:
534
+ process.stdout.close()
535
+ if process.stderr:
536
+ process.stderr.close()
537
+ process.kill()
538
+ process.wait(timeout=5)
527
539
  output = stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace")
528
540
  return None, f"{output}\ntimed out at the admitted wall-clock deadline"
529
541
  return process.returncode, (stdout + stderr).decode("utf-8", errors="replace")
@@ -35,6 +35,8 @@ MARKER_SCHEMA = "gsd-path/member/v1"
35
35
  MARKER_KEYS = {"schema", "coordinator", "project", "name"}
36
36
  NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
37
37
  FIELDS = ("Checkout", "Remote", "Integration")
38
+ # Optional: `add` records it one time; a row without it means main.
39
+ DEFAULT_BRANCH_FIELD = "Default branch"
38
40
  INTEGRATIONS = ("default", "direct", "pull-request")
39
41
  GITHUB_REMOTE_RE = re.compile(
40
42
  r"(?:https://github\.com/|git@github\.com:|ssh://git@github\.com/)([^/\s]+)/([^/\s]+)"
@@ -221,9 +223,10 @@ def read_members(coordinator: Path) -> list[dict[str, str]]:
221
223
  if not line.strip():
222
224
  continue
223
225
  key, separator, value = line.partition(":")
224
- if not separator or key not in FIELDS or key.lower() in current:
226
+ field = key.lower().replace(" ", "_")
227
+ if not separator or key not in (*FIELDS, DEFAULT_BRANCH_FIELD) or field in current:
225
228
  raise MembersError(f"MEMBERS.md line {number}: unexpected line {line!r}")
226
- current[key.lower()] = value.strip()
229
+ current[field] = value.strip()
227
230
  if not preamble or preamble[0] != "# Members":
228
231
  raise MembersError("MEMBERS.md format error: expected # Members")
229
232
  rest = preamble[1:]
@@ -247,16 +250,37 @@ def read_members(coordinator: Path) -> list[dict[str, str]]:
247
250
  )
248
251
  if not Path(member["checkout"]).is_absolute():
249
252
  raise MembersError(f"MEMBERS.md member {member['name']} Checkout must be absolute")
253
+ if not _common.valid_default_branch(default_branch(member)):
254
+ raise MembersError(
255
+ f"MEMBERS.md member {member['name']} has invalid Default branch: {member['default_branch']}"
256
+ )
250
257
  return members
251
258
 
252
259
 
260
+ def default_branch(member: dict[str, str]) -> str:
261
+ """The default branch a MEMBERS.md row records; a row without the field means main."""
262
+ return member.get("default_branch", "main")
263
+
264
+
265
+ def member_default_branch(coordinator: Path, name: str) -> str:
266
+ """The default branch MEMBERS.md records for the member `name`."""
267
+ for member in read_members(coordinator):
268
+ if member["name"] == name:
269
+ return default_branch(member)
270
+ raise MembersError(f"member {name} is not in MEMBERS.md")
271
+
272
+
253
273
  def check_member(
254
274
  coordinator: Path,
255
275
  coordinator_name: str,
256
276
  checkout: Path,
257
- recorded_remote: Optional[str] = None,
258
- ) -> str:
259
- """Refuse a checkout that cannot join; return its origin URL."""
277
+ recorded: Optional[dict[str, str]] = None,
278
+ refuse_reserved_refs: bool = True,
279
+ ) -> tuple[str, str]:
280
+ """Refuse a checkout that cannot join; return its origin URL and default branch.
281
+
282
+ `recorded` is the member's MEMBERS.md row. Without it (`add`), the default
283
+ branch is the local origin/HEAD, read this one time; an unset ref means main."""
260
284
  if checkout.is_symlink() or not checkout.is_dir():
261
285
  raise MembersError(f"member checkout is not a real directory: {checkout}")
262
286
  checkout = checkout.resolve()
@@ -278,12 +302,21 @@ def check_member(
278
302
  remote = _git(checkout, "config", "--get", "remote.origin.url")
279
303
  if not GITHUB_REMOTE_RE.fullmatch(remote):
280
304
  raise MembersError(f"member requires a GitHub.com origin: {remote}")
281
- if recorded_remote is not None and remote != recorded_remote:
282
- raise MembersError(f"member origin changed: {recorded_remote} -> {remote}")
283
- default = _common.run_git(checkout, "symbolic-ref", "--short", "refs/remotes/origin/HEAD")
284
- if default.returncode != 0 or default.stdout.strip() != "origin/main":
285
- raise MembersError(f"member remote default must be main: {checkout}")
286
- require_origin_main(checkout)
305
+ if recorded is not None and remote != recorded["remote"]:
306
+ raise MembersError(f"member origin changed: {recorded['remote']} -> {remote}")
307
+ live = _common.origin_default_branch(checkout)
308
+ default = live if recorded is None else default_branch(recorded)
309
+ if live != default:
310
+ raise MembersError(
311
+ f"member remote default must be {default}, got {live}: {checkout}; "
312
+ "run `git remote set-head origin --auto` when the local origin/HEAD is stale"
313
+ )
314
+ if not _common.valid_default_branch(default):
315
+ raise MembersError(
316
+ f"member default branch {default!r} cannot be recorded in MEMBERS.md: it is a "
317
+ "GSD Path reserved name or it has a character that the file cannot hold"
318
+ )
319
+ require_origin_branch(checkout, default)
287
320
  member_state = checkout / ".project" / "STATE.md"
288
321
  if member_state.exists() or member_state.is_symlink():
289
322
  if member_state.is_symlink() or not member_state.is_file():
@@ -294,14 +327,15 @@ def check_member(
294
327
  raise MembersError(f"member STATE.md is unreadable: {error}") from error
295
328
  if state.milestone is not None and (state.phase, state.status) != ("shipped", "done"):
296
329
  raise MembersError(f"member has an active milestone: {state.milestone}")
297
- prefixes = member_ref_prefixes(coordinator_name) + tuple(
298
- "refs/remotes/origin/" + branch.format(coordinator_name) for branch in MEMBER_BRANCH_FORMATS
299
- )
300
- refs = _git(checkout, "for-each-ref", "--format=%(refname)").splitlines()
301
- colliding = [ref for ref in refs if ref.startswith(prefixes)]
302
- if colliding:
303
- raise MembersError("member refs collide with coordinator names: " + ", ".join(colliding))
304
- return remote
330
+ if refuse_reserved_refs:
331
+ prefixes = member_ref_prefixes(coordinator_name) + tuple(
332
+ "refs/remotes/origin/" + branch.format(coordinator_name) for branch in MEMBER_BRANCH_FORMATS
333
+ )
334
+ refs = _git(checkout, "for-each-ref", "--format=%(refname)").splitlines()
335
+ colliding = [ref for ref in refs if ref.startswith(prefixes)]
336
+ if colliding:
337
+ raise MembersError("member refs collide with coordinator names: " + ", ".join(colliding))
338
+ return remote, default
305
339
 
306
340
 
307
341
  MEMBER_CLOSE_SCHEMA = "gsd-path/member-close/v1"
@@ -336,12 +370,14 @@ def write_member_close(common: Path, archive_name: str, rows: list[dict]) -> Non
336
370
  _common.atomic_write(path, json.dumps({"schema": MEMBER_CLOSE_SCHEMA, "members": rows}, indent=2) + "\n")
337
371
 
338
372
 
339
- def require_origin_main(checkout: Path) -> None:
373
+ def require_origin_branch(checkout: Path, branch: str) -> str:
374
+ """The commit of the member's fetched default branch `branch`."""
340
375
  baseline = _common.run_git(
341
- checkout, "rev-parse", "--verify", "--quiet", "refs/remotes/origin/main^{commit}"
376
+ checkout, "rev-parse", "--verify", "--quiet", f"refs/remotes/origin/{branch}^{{commit}}"
342
377
  )
343
378
  if baseline.returncode != 0:
344
- raise MembersError("member requires refs/remotes/origin/main; run git fetch origin")
379
+ raise MembersError(f"member requires refs/remotes/origin/{branch}; run git fetch origin")
380
+ return baseline.stdout.strip()
345
381
 
346
382
 
347
383
  def member_ref_prefixes(project: str) -> tuple[str, ...]:
@@ -403,7 +439,8 @@ def _task_members(coordinator: Path) -> set[str]:
403
439
 
404
440
  def lock_build_members(coordinator: Path) -> Optional[list[dict[str, str]]]:
405
441
  """At build start, lock the members tasks name (MEMBERS.md order) and create
406
- each member's bound branch at its origin/main. None when no task names a member."""
442
+ each member's bound branch at its recorded default branch on origin. None when
443
+ no task names a member."""
407
444
  root, state = _coordinator(coordinator)
408
445
  named = _task_members(root)
409
446
  lock_path = root / LOCK_PATH
@@ -442,8 +479,8 @@ def lock_build_members(coordinator: Path) -> Optional[list[dict[str, str]]]:
442
479
  remote = _git(checkout, "config", "--get", "remote.origin.url")
443
480
  if remote != member["remote"]:
444
481
  raise MembersError(f"member origin changed: {member['remote']} -> {remote}")
445
- require_origin_main(checkout)
446
- base = _git(checkout, "rev-parse", "refs/remotes/origin/main^{commit}")
482
+ default = default_branch(member)
483
+ base = require_origin_branch(checkout, default)
447
484
  existing = _common.run_git(checkout, "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}")
448
485
  if existing.returncode == 0:
449
486
  tip = existing.stdout.strip()
@@ -456,7 +493,7 @@ def lock_build_members(coordinator: Path) -> Optional[list[dict[str, str]]]:
456
493
  elif _common.run_git(checkout, "merge-base", "--is-ancestor", tip, base).returncode == 0:
457
494
  base = tip
458
495
  else:
459
- raise MembersError(f"member {member['name']} branch {branch} has commits not on origin/main")
496
+ raise MembersError(f"member {member['name']} branch {branch} has commits not on origin/{default}")
460
497
  else:
461
498
  missing.append((checkout, base))
462
499
  entries.append({"name": member["name"], "branch": branch, "base": base})
@@ -473,6 +510,8 @@ def render(members: Sequence[dict[str, str]]) -> str:
473
510
  f"Checkout: {member['checkout']}\n"
474
511
  f"Remote: {member['remote']}\n"
475
512
  f"Integration: {member['integration']}\n"
513
+ # A row that never had the field keeps none, so old rows are not rewritten.
514
+ + (f"{DEFAULT_BRANCH_FIELD}: {member['default_branch']}\n" if "default_branch" in member else "")
476
515
  for member in members
477
516
  ]
478
517
  return "\n".join([HEADER, *sections])
@@ -486,7 +525,7 @@ def add_member(repo: Path, name: str, checkout: Path, integration: str) -> list[
486
525
  raise MembersError(f"invalid member name: {name!r}")
487
526
  members = read_members(root)
488
527
  resolved = checkout.resolve()
489
- remote = check_member(root, state.project, checkout)
528
+ remote, default = check_member(root, state.project, checkout)
490
529
  common = Path(_git(resolved, "rev-parse", "--path-format=absolute", "--git-common-dir")).resolve()
491
530
  identity = _remote_identity(remote)
492
531
  for member in members:
@@ -497,7 +536,8 @@ def add_member(repo: Path, name: str, checkout: Path, integration: str) -> list[
497
536
  raise MembersError(f"member already recorded: {member['name']}")
498
537
  _refuse_foreign_marker(resolved, root, state.project, name)
499
538
  members.append(
500
- {"name": name, "checkout": str(resolved), "remote": remote, "integration": integration}
539
+ {"name": name, "checkout": str(resolved), "remote": remote, "integration": integration,
540
+ "default_branch": default}
501
541
  )
502
542
  _write_marker(resolved, root, state.project, name)
503
543
  _common.atomic_write(root / ".project" / MEMBERS_FILE, render(members))
@@ -558,8 +598,9 @@ def create_member(repo: Path, name: str, checkout: Path, integration: str, githu
558
598
  _common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
559
599
  listed = [member for member in read_members(root) if member["name"] == name]
560
600
  if listed:
561
- if (len(listed) != 1 or listed[0] != {"name": name, "checkout": str(resolved), "remote": url,
562
- "integration": integration}
601
+ if (len(listed) != 1 or {**listed[0], "default_branch": default_branch(listed[0])} != {
602
+ "name": name, "checkout": str(resolved), "remote": url,
603
+ "integration": integration, "default_branch": "main"}
563
604
  or member_role(resolved) != {"coordinator": root, "project": state.project, "name": name}):
564
605
  raise MembersError(f"member already recorded with a different checkout or marker: {name}")
565
606
  journal.unlink()
@@ -608,6 +649,10 @@ def create_member(repo: Path, name: str, checkout: Path, integration: str, githu
608
649
  cloned = _common.run_git(resolved.parent, "clone", "-q", url, str(resolved))
609
650
  if cloned.returncode != 0:
610
651
  raise MembersError(f"could not clone {url}: {(cloned.stderr or cloned.stdout).strip()}")
652
+ # ponytail: --create stays main-only, because a new repository uses main;
653
+ # `add` joins an existing repository with any default branch.
654
+ if _common.origin_default_branch(resolved) != "main":
655
+ raise MembersError(f"member remote default must be main: {resolved}")
611
656
  recorded["step"] = "join"
612
657
  _common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
613
658
  joined = add_member(root, name, resolved, integration)
@@ -620,7 +665,7 @@ def validate_members(repo: Path) -> list[dict[str, str]]:
620
665
  members = read_members(root)
621
666
  for member in members:
622
667
  checkout = Path(member["checkout"])
623
- check_member(root, state.project, checkout, member["remote"])
668
+ check_member(root, state.project, checkout, member, refuse_reserved_refs=False)
624
669
  try:
625
670
  role = member_role(checkout)
626
671
  except MembersError as error:
@@ -639,7 +684,7 @@ def repair_members(repo: Path) -> list[dict[str, str]]:
639
684
  members = read_members(root)
640
685
  for member in members:
641
686
  checkout = Path(member["checkout"])
642
- check_member(root, state.project, checkout, member["remote"])
687
+ check_member(root, state.project, checkout, member, refuse_reserved_refs=False)
643
688
  _refuse_foreign_marker(checkout.resolve(), root, state.project, member["name"])
644
689
  for member in members:
645
690
  checkout = Path(member["checkout"]).resolve()
@@ -202,7 +202,11 @@ def native_record(args):
202
202
  identity = {'repo': str(args.repo.resolve()), 'project_dir': args.project_dir,
203
203
  'scope': args.scope, 'assignment': args.assignment, 'role': args.role}
204
204
  if data is not None and data.get('identity') != identity:
205
- raise PolicyError('record belongs to another assignment or milestone/track/cycle')
205
+ recorded = data.get('identity')
206
+ recorded = recorded if isinstance(recorded, dict) else {}
207
+ differing = ', '.join(f'{field}: {value}, record has {recorded.get(field)}'
208
+ for field, value in identity.items() if recorded.get(field) != value)
209
+ raise PolicyError(f'record belongs to another assignment or milestone/track/cycle ({differing})')
206
210
  if args.action == 'release':
207
211
  if data is None or not args.terminal_result:
208
212
  raise PolicyError('release requires an existing record and terminal-result evidence')
@@ -13,7 +13,7 @@ sys.dont_write_bytecode = True
13
13
  ROLES = ('coder', 'reviewer', 'review_panel', 'skeptic', 'inspect_codebase',
14
14
  'inspect_docs', 'docs_audit', 'research', 'decide', 'roadmap', 'plan', 'plan_patch')
15
15
  HOSTS = ('codex', 'claude', 'grok', 'opencode', 'copilot', 'qwen', 'antigravity',
16
- 'cursor', 'zed', 'kiro', 'kimi')
16
+ 'cursor', 'zed', 'kiro', 'kimi', 'muse')
17
17
 
18
18
 
19
19
  def sibling(name):
@@ -73,8 +73,8 @@ def validate_value(key, value):
73
73
  if not isinstance(value, str) or not value or value != value.strip():
74
74
  raise ValueError(f'{key} requires a nonempty string')
75
75
  if key == 'integration':
76
- if value not in ('direct', 'pull-request'):
77
- raise ValueError('integration must be direct or pull-request')
76
+ if value not in ('direct', 'pull-request', 'external-landing'):
77
+ raise ValueError('integration must be direct, pull-request, or external-landing')
78
78
  elif key == 'review_panel':
79
79
  if any(char.isspace() for char in value):
80
80
  raise ValueError('use off, detected, or comma-separated families without spaces')
@@ -2,8 +2,9 @@
2
2
  # gsd-path project runtime
3
3
  """Deterministic GSD Path branch names and pipeline commit messages.
4
4
 
5
- Bound work lives on `gsd-path/M00N` for that milestone. The required remote
6
- default `main` stays a separate trunk. Ship merges the bound branch onto main;
5
+ Bound work lives on `gsd-path/M00N` for that milestone. The remote default
6
+ that STATE.md records (`main` when unrecorded) stays a separate trunk. Ship
7
+ merges the bound branch onto it;
7
8
  the next milestone binds a new unused `gsd-path/M00N` and retires the
8
9
  integrated previous branch, locally and on origin.
9
10
  """
@@ -98,7 +99,8 @@ def ship_subject(archive_name: str) -> str:
98
99
 
99
100
 
100
101
  def integrate_subject(archive_name: str, default_branch: str) -> str:
101
- if not default_branch or default_branch.startswith("origin/"):
102
+ # The bare name, under the rule that records it; `origin/trunk` is a legal bare name.
103
+ if not _common.valid_default_branch(default_branch):
102
104
  raise PipelineGitError(f"default branch is invalid: {default_branch}")
103
105
  number = milestone_number(archive_name)
104
106
  return (
@@ -146,6 +148,29 @@ def ship_commit_body(archive_path: str, reviewed_head: str, members: Sequence[di
146
148
  )
147
149
 
148
150
 
151
+ _TRAILER_LINE = re.compile(r"^[A-Za-z-]+:\s")
152
+
153
+
154
+ def ship_body_matches(body: str, expected_body: str) -> bool:
155
+ """True when body equals the contract or adds only optional git trailers after it."""
156
+ normalized = body.rstrip("\n")
157
+ expected = expected_body.rstrip("\n")
158
+ if normalized == expected:
159
+ return True
160
+ prefix = expected + "\n"
161
+ if not normalized.startswith(prefix):
162
+ return False
163
+ remainder = normalized[len(prefix):]
164
+ if not remainder:
165
+ return True
166
+ for line in remainder.splitlines():
167
+ if not line.strip():
168
+ return False
169
+ if not _TRAILER_LINE.match(line):
170
+ return False
171
+ return True
172
+
173
+
149
174
  def integrate_commit_body(
150
175
  archive_path: str,
151
176
  ship_commit: str,
@@ -270,22 +295,50 @@ def _symbolic_branch(repo: Path) -> str:
270
295
  return result.stdout.strip()
271
296
 
272
297
 
273
- def _branch_worktrees(repo: Path) -> dict[str, Path]:
274
- """Return every checked-out local branch from Git's worktree registry."""
275
- output = _run_git(repo, "worktree", "list", "--porcelain", "-z").stdout
298
+ def worktree_list_porcelain(repo: Path) -> tuple[str, bool]:
299
+ """Return worktree list output and whether it is NUL-separated."""
300
+ result = _run_git(repo, "worktree", "list", "--porcelain", "-z", check=False)
301
+ if result.returncode == 0:
302
+ return result.stdout, True
303
+ result = _run_git(repo, "worktree", "list", "--porcelain", check=False)
304
+ if result.returncode != 0:
305
+ message = (result.stderr or result.stdout).strip() or "could not list worktrees"
306
+ raise PipelineGitError(message)
307
+ return result.stdout, False
308
+
309
+
310
+ def branch_worktrees_from_porcelain(output: str, *, nul_separated: bool) -> dict[str, Path]:
276
311
  worktrees: dict[str, Path] = {}
277
- current_path: Optional[Path] = None
278
- for field in output.split("\0"):
279
- if field.startswith("worktree "):
280
- current_path = Path(field.removeprefix("worktree ")).resolve()
281
- elif field.startswith("branch ") and current_path is not None:
282
- ref = field.removeprefix("branch ")
283
- prefix = "refs/heads/"
284
- if ref.startswith(prefix):
285
- worktrees[ref.removeprefix(prefix)] = current_path
312
+ if nul_separated:
313
+ current_path: Optional[Path] = None
314
+ for field in output.split("\0"):
315
+ if not field:
316
+ continue
317
+ if field.startswith("worktree "):
318
+ current_path = Path(field.removeprefix("worktree ")).resolve()
319
+ elif field.startswith("branch ") and current_path is not None:
320
+ ref = field.removeprefix("branch ")
321
+ prefix = "refs/heads/"
322
+ if ref.startswith(prefix):
323
+ worktrees[ref.removeprefix(prefix)] = current_path
324
+ return worktrees
325
+ for block in output.strip().split("\n\n"):
326
+ fields = block.splitlines()
327
+ if not fields or not fields[0].startswith("worktree "):
328
+ continue
329
+ current_path = Path(fields[0].removeprefix("worktree ")).resolve()
330
+ for line in fields[1:]:
331
+ if line.startswith("branch refs/heads/"):
332
+ worktrees[line.removeprefix("branch refs/heads/")] = current_path
286
333
  return worktrees
287
334
 
288
335
 
336
+ def _branch_worktrees(repo: Path) -> dict[str, Path]:
337
+ """Return every checked-out local branch from Git's worktree registry."""
338
+ output, nul_separated = worktree_list_porcelain(repo)
339
+ return branch_worktrees_from_porcelain(output, nul_separated=nul_separated)
340
+
341
+
289
342
  def _remote_ref_sha(repo: Path, ref: str) -> Optional[str]:
290
343
  if not _origin_remote_exists(repo):
291
344
  raise PipelineGitError("origin remote is not configured")
@@ -312,7 +365,7 @@ def _remote_branch_exists(repo: Path, branch: str) -> bool:
312
365
  return _remote_ref_sha(repo, f"refs/heads/{branch}") is not None
313
366
 
314
367
 
315
- def _pull_request_tag_fields(repo: Path, ref: str) -> dict[str, str]:
368
+ def _milestone_tag_fields(repo: Path, ref: str) -> dict[str, str]:
316
369
  tag_type = _run_git(repo, "cat-file", "-t", ref, check=False)
317
370
  if tag_type.returncode != 0 or tag_type.stdout.strip() != "tag":
318
371
  return {}
@@ -322,7 +375,7 @@ def _pull_request_tag_fields(repo: Path, ref: str) -> dict[str, str]:
322
375
  match = re.fullmatch(r"(Mode|Pull-Request|Ship|Landing): (.+)", line)
323
376
  if match:
324
377
  if match.group(1) in fields:
325
- raise PipelineGitError("pull-request milestone tag repeats metadata")
378
+ raise PipelineGitError("milestone tag repeats metadata")
326
379
  fields[match.group(1)] = match.group(2)
327
380
  return fields
328
381
 
@@ -353,7 +406,15 @@ def _validated_shipped_state(repo: Path) -> dict[str, object]:
353
406
  return state
354
407
 
355
408
 
356
- def _require_pull_request_integration_proof(
409
+ # Metadata a published milestone tag must carry, per integration mode, before
410
+ # bind-next accepts a previous bound branch that the forge deleted on merge.
411
+ TAG_PROOF_FIELDS = {
412
+ "pull-request": {"Mode", "Pull-Request", "Ship", "Landing"},
413
+ "external-landing": {"Mode", "Ship", "Landing"},
414
+ }
415
+
416
+
417
+ def _require_tag_integration_proof(
357
418
  repo: Path,
358
419
  ship: str,
359
420
  base: str,
@@ -364,8 +425,11 @@ def _require_pull_request_integration_proof(
364
425
  raise PipelineGitError("bind-next requires shipped/done state")
365
426
  if state.get("branch") != previous_branch:
366
427
  raise PipelineGitError("shipped state does not name the previous branch")
367
- if state.get("integration") != "pull-request":
368
- raise PipelineGitError("shipped integration mode is not pull-request")
428
+ mode = state.get("integration")
429
+ if mode not in TAG_PROOF_FIELDS:
430
+ raise PipelineGitError(
431
+ "shipped integration mode is not pull-request or external-landing"
432
+ )
369
433
  archive = state.get("archive")
370
434
  if not isinstance(archive, str):
371
435
  raise PipelineGitError("shipped state does not name an archive")
@@ -376,24 +440,23 @@ def _require_pull_request_integration_proof(
376
440
  ):
377
441
  raise PipelineGitError("shipped archive does not match the previous branch")
378
442
  ref = f"refs/tags/milestone/{match.group(1)}"
379
- fields = _pull_request_tag_fields(repo, ref)
380
- if set(fields) != {"Mode", "Pull-Request", "Ship", "Landing"}:
381
- raise PipelineGitError("pull-request integration proof is incomplete")
382
- if fields["Mode"] != "pull-request" or fields["Ship"] != ship:
383
- raise PipelineGitError("pull-request integration proof does not match shipped state")
384
- pull_request = fields["Pull-Request"]
385
- if re.fullmatch(
443
+ fields = _milestone_tag_fields(repo, ref)
444
+ if set(fields) != TAG_PROOF_FIELDS[mode]:
445
+ raise PipelineGitError(f"{mode} integration proof is incomplete")
446
+ if fields["Mode"] != mode or fields["Ship"] != ship:
447
+ raise PipelineGitError(f"{mode} integration proof does not match shipped state")
448
+ if mode == "pull-request" and re.fullmatch(
386
449
  r"https://github\.com/[^/]+/[^/]+/pull/[1-9][0-9]*",
387
- pull_request,
450
+ fields["Pull-Request"],
388
451
  ) is None:
389
452
  raise PipelineGitError("pull-request integration proof has an invalid PR URL")
390
453
  landing = fields["Landing"]
391
454
  if re.fullmatch(r"[0-9a-f]{40}", landing) is None:
392
- raise PipelineGitError("pull-request integration proof has an invalid landing")
455
+ raise PipelineGitError(f"{mode} integration proof has an invalid landing")
393
456
  local_object = _run_git(repo, "rev-parse", ref).stdout.strip()
394
457
  local_target = _run_git(repo, "rev-parse", f"{ref}^{{commit}}").stdout.strip()
395
458
  if local_target != landing:
396
- raise PipelineGitError("pull-request integration tag does not point at landing")
459
+ raise PipelineGitError(f"{mode} integration tag does not point at landing")
397
460
  remote = _run_git(
398
461
  repo,
399
462
  "ls-remote",
@@ -405,14 +468,14 @@ def _require_pull_request_integration_proof(
405
468
  check=False,
406
469
  )
407
470
  if remote.returncode != 0:
408
- raise PipelineGitError("pull-request integration tag is not published")
471
+ raise PipelineGitError(f"{mode} integration tag is not published")
409
472
  rows = dict(
410
473
  line.split("\t", 1)[::-1]
411
474
  for line in remote.stdout.splitlines()
412
475
  if "\t" in line
413
476
  )
414
477
  if rows != {ref: local_object, f"{ref}^{{}}": landing}:
415
- raise PipelineGitError("published pull-request integration tag differs")
478
+ raise PipelineGitError(f"published {mode} integration tag differs")
416
479
  parents = _run_git(
417
480
  repo,
418
481
  "rev-list",
@@ -422,11 +485,11 @@ def _require_pull_request_integration_proof(
422
485
  landing,
423
486
  ).stdout.split()
424
487
  if len(parents) != 3 or parents[2] != ship:
425
- raise PipelineGitError("pull-request integration proof has invalid merge topology")
488
+ raise PipelineGitError(f"{mode} integration proof has invalid merge topology")
426
489
  first_parent = _run_git(repo, "rev-list", "--first-parent", base).stdout.splitlines()
427
490
  if landing not in first_parent:
428
491
  raise PipelineGitError(
429
- "pull-request integration landing is not on main first-parent history"
492
+ f"{mode} integration landing is not on the default branch first-parent history"
430
493
  )
431
494
  return landing
432
495
 
@@ -482,6 +545,34 @@ def _write_bind_next_journal(path: Path, value: dict[str, object]) -> None:
482
545
  temporary.unlink()
483
546
 
484
547
 
548
+ def _require_state_remote_default(repo: Path, remote_default: str) -> str:
549
+ """Return the default branch STATE.md records; `remote_default` must name it.
550
+
551
+ No STATE.md yet (bind-initial before initialize) or no field means main.
552
+ An unreadable STATE.md also means main, as before the field existed: the
553
+ callers that need a valid state check it themselves.
554
+ """
555
+ if __package__:
556
+ from . import pipeline_state
557
+ else:
558
+ try:
559
+ import pipeline_state
560
+ except ImportError: # pragma: no cover - package import used by tests
561
+ from scripts import pipeline_state
562
+
563
+ default_name, unreadable = "main", ""
564
+ if (repo / ".project" / "STATE.md").exists():
565
+ try:
566
+ default_name = pipeline_state.load_state(repo)[0].default_branch
567
+ except (OSError, pipeline_state.PipelineStateError) as error:
568
+ unreadable = f" (STATE.md is unreadable: {error})"
569
+ if remote_default != f"origin/{default_name}":
570
+ raise PipelineGitError(
571
+ f"remote default must be origin/{default_name}, got {remote_default}{unreadable}"
572
+ )
573
+ return default_name
574
+
575
+
485
576
  def _initial_binding_snapshot(repo: Path) -> Optional[tuple]:
486
577
  """Admit only clean work or the validated, untracked initializer state."""
487
578
  dirty = _run_git(repo, "status", "--porcelain=v1", "-z", "--untracked-files=all").stdout
@@ -528,8 +619,7 @@ def bind_initial_milestone_branch(
528
619
  resolved = _require_worktree_root(repo)
529
620
  if not is_bound_branch(branch):
530
621
  raise PipelineGitError(f"invalid bound branch: {branch}")
531
- if remote_default != "origin/main":
532
- raise PipelineGitError(f"remote default must be origin/main, got {remote_default}")
622
+ default_name = _require_state_remote_default(resolved, remote_default)
533
623
 
534
624
  default_sha = _run_git(
535
625
  resolved,
@@ -549,7 +639,7 @@ def bind_initial_milestone_branch(
549
639
  raise PipelineGitError(
550
640
  f"validated base is stale: {base_sha} != {remote_default} {default_sha}"
551
641
  )
552
- remote_sha = _remote_ref_sha(resolved, "refs/heads/main")
642
+ remote_sha = _remote_ref_sha(resolved, f"refs/heads/{default_name}")
553
643
  if remote_sha != default_sha:
554
644
  raise PipelineGitError(
555
645
  f"fetched {remote_default} is stale relative to origin"
@@ -691,8 +781,7 @@ def bind_next_milestone_branch(
691
781
  raise PipelineGitError(f"invalid previous bound branch: {previous_branch}")
692
782
  if milestone_number(branch) <= milestone_number(previous_branch):
693
783
  raise PipelineGitError(f"next branch {branch} must follow {previous_branch}")
694
- if remote_default != "origin/main":
695
- raise PipelineGitError(f"remote default must be origin/main, got {remote_default}")
784
+ default_name = _require_state_remote_default(repo, remote_default)
696
785
 
697
786
  default_sha = _run_git(
698
787
  repo,
@@ -712,7 +801,7 @@ def bind_next_milestone_branch(
712
801
  raise PipelineGitError(
713
802
  f"validated base is stale: {base_sha} != {remote_default} {default_sha}"
714
803
  )
715
- live_default_sha = _remote_ref_sha(repo, "refs/heads/main")
804
+ live_default_sha = _remote_ref_sha(repo, f"refs/heads/{default_name}")
716
805
  if live_default_sha != default_sha:
717
806
  raise PipelineGitError(f"fetched {remote_default} is stale relative to origin")
718
807
  ship_sha = _run_git(
@@ -770,7 +859,7 @@ def bind_next_milestone_branch(
770
859
  detail = landing_integrated.stderr.strip() or landing_integrated.stdout.strip()
771
860
  raise PipelineGitError(f"could not verify integrated branch: {detail}")
772
861
  if allow_remote_absent:
773
- proven_landing = _require_pull_request_integration_proof(
862
+ proven_landing = _require_tag_integration_proof(
774
863
  repo,
775
864
  ship_sha,
776
865
  base_sha,
@@ -778,7 +867,7 @@ def bind_next_milestone_branch(
778
867
  )
779
868
  if proven_landing != landing_sha:
780
869
  raise PipelineGitError(
781
- "validated landing does not match pull-request integration proof"
870
+ "validated landing does not match the milestone tag integration proof"
782
871
  )
783
872
 
784
873
  symbolic_branch = _run_git(