@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
@@ -0,0 +1,706 @@
1
+ #!/usr/bin/env python3
2
+ # gsd-path project runtime
3
+ """Record and validate the member repositories of a multi-repo coordinator.
4
+
5
+ The coordinator's `.project/MEMBERS.md` lists members in ship order
6
+ (docs/adr/0002-multi-repo-coordinator.md). `add` writes that file and an
7
+ untracked marker in the member's shared Git directory. The next approval
8
+ checkpoint commits MEMBERS.md with the other `.project` artifacts.
9
+ `member_role` verifies the marker from any worktree of the member.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import os
17
+ import re
18
+ import subprocess
19
+ import sys
20
+
21
+ # Runtime helpers must not modify their immutable installation.
22
+ sys.dont_write_bytecode = True
23
+ from pathlib import Path
24
+ from typing import Optional, Sequence
25
+
26
+ if __package__:
27
+ from . import _common, pipeline_state
28
+ else:
29
+ import _common
30
+ import pipeline_state
31
+
32
+
33
+ MEMBERS_FILE = "MEMBERS.md"
34
+ MARKER_SCHEMA = "gsd-path/member/v1"
35
+ MARKER_KEYS = {"schema", "coordinator", "project", "name"}
36
+ NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
37
+ FIELDS = ("Checkout", "Remote", "Integration")
38
+ INTEGRATIONS = ("default", "direct", "pull-request")
39
+ GITHUB_REMOTE_RE = re.compile(
40
+ r"(?:https://github\.com/|git@github\.com:|ssh://git@github\.com/)([^/\s]+)/([^/\s]+)"
41
+ )
42
+ # Member-side Path refs carry the coordinator name, so they cannot collide with
43
+ # a member's own Path history.
44
+ MEMBER_BRANCH_FORMATS = ("gsd-path/{}-", "gsd-path-task/{}-", "gsd-path-verify/{}-", "gsd-path-integrate/{}-")
45
+ MEMBER_TAG_FORMAT = "refs/tags/milestone/{}-"
46
+ # The coordinator records one authorization per (ref, object) before it
47
+ # publishes a member Path ref; member pre-push accepts only a matching one.
48
+ AUTHORIZATION_PREFIX = "refs/gsd-path/authorizations/"
49
+ LOCK_PATH = ".project/build/members.json"
50
+ LOCK_SCHEMA = "gsd-path/member-lock/v1"
51
+ HEADER = (
52
+ "# Members\n\n"
53
+ "<!-- Written by members.py add. One section per member repository, in\n"
54
+ " ship order. Fixed format; change it only through members.py. -->\n"
55
+ )
56
+
57
+
58
+ class MembersError(RuntimeError):
59
+ pass
60
+
61
+
62
+ def _git(repo: Path, *arguments: str) -> str:
63
+ result = _common.run_git(repo, *arguments)
64
+ if result.returncode != 0:
65
+ detail = (result.stderr or result.stdout).strip() or "git command failed"
66
+ raise MembersError(f"{repo}: git {' '.join(arguments)}: {detail}")
67
+ return result.stdout.strip()
68
+
69
+
70
+ def _coordinator(repo: Path) -> tuple[Path, pipeline_state.PipelineState]:
71
+ try:
72
+ root = pipeline_state._repo_root(Path(repo))
73
+ state, _, _ = pipeline_state.load_state(root)
74
+ except pipeline_state.PipelineStateError as error:
75
+ raise MembersError(f"coordinator: {error}") from error
76
+ if Path(_git(root, "rev-parse", "--show-toplevel")).resolve() != root:
77
+ raise MembersError(f"coordinator is not a Git root: {root}")
78
+ return root, state
79
+
80
+
81
+ def _remote_identity(remote: str) -> Optional[tuple[str, str]]:
82
+ match = GITHUB_REMOTE_RE.fullmatch(remote)
83
+ if match is None:
84
+ return None
85
+ return match.group(1).lower(), match.group(2).removesuffix(".git").lower()
86
+
87
+
88
+ def _common_dir(checkout: Path) -> Path:
89
+ return Path(_git(checkout, "rev-parse", "--path-format=absolute", "--git-common-dir")).resolve()
90
+
91
+
92
+ def _marker_path(checkout: Path) -> Path:
93
+ directory = _common_dir(checkout) / "gsd-path"
94
+ if directory.is_symlink() or (directory.exists() and not directory.is_dir()):
95
+ raise MembersError(f"member marker directory must be real: {directory}")
96
+ path = directory / "member.json"
97
+ if path.is_symlink() or (path.exists() and not path.is_file()):
98
+ raise MembersError(
99
+ f"member marker is not a regular file: {path}; "
100
+ "remove it by hand, then run members.py repair --repo <coordinator>"
101
+ )
102
+ return path
103
+
104
+
105
+ def _read_marker(path: Path) -> Optional[dict[str, str]]:
106
+ if not path.exists() and not path.is_symlink():
107
+ return None
108
+ try:
109
+ if path.is_symlink() or not path.is_file():
110
+ raise MembersError(
111
+ f"member marker is not a regular file: {path}; "
112
+ "remove it by hand, then run members.py repair --repo <coordinator>"
113
+ )
114
+ data = json.loads(path.read_text(encoding="utf-8"))
115
+ if (not isinstance(data, dict) or set(data) != MARKER_KEYS
116
+ or any(not isinstance(data[key], str) or not data[key].strip() for key in MARKER_KEYS)
117
+ or data["schema"] != MARKER_SCHEMA
118
+ or not Path(data["coordinator"]).is_absolute()):
119
+ raise ValueError("unexpected content")
120
+ except (OSError, ValueError) as error:
121
+ raise MembersError(
122
+ f"member marker is unreadable: {path}: {error}; "
123
+ "run members.py repair --repo <coordinator>"
124
+ ) from error
125
+ return data
126
+
127
+
128
+ def _write_marker(checkout: Path, coordinator: Path, project: str, name: str) -> None:
129
+ path = _marker_path(checkout)
130
+ path.parent.mkdir(parents=True, exist_ok=True)
131
+ marker = {"schema": MARKER_SCHEMA, "coordinator": str(coordinator), "project": project, "name": name}
132
+ _common.atomic_write(path, json.dumps(marker, sort_keys=True) + "\n")
133
+
134
+
135
+ def _refuse_foreign_marker(checkout: Path, coordinator: Path, project: str, name: str) -> None:
136
+ path = _marker_path(checkout)
137
+ try:
138
+ marker = _read_marker(path)
139
+ except MembersError:
140
+ return
141
+ if marker is not None and (marker["project"], marker["name"]) != (project, name):
142
+ raise MembersError(f"{checkout} is already a member of {marker['project']}")
143
+ if marker is not None:
144
+ try:
145
+ role = member_role(checkout)
146
+ except MembersError:
147
+ return
148
+ if role is not None and role["coordinator"] != coordinator:
149
+ raise MembersError(f"{checkout} is already a member of {role['coordinator']}")
150
+
151
+
152
+ def marker_coordinator(checkout: Path) -> Optional[Path]:
153
+ """The coordinator a member marker names, unverified; None without a marker."""
154
+ marker = _read_marker(_marker_path(Path(checkout)))
155
+ return None if marker is None else Path(marker["coordinator"])
156
+
157
+
158
+ def member_role(checkout: Path) -> Optional[dict[str, object]]:
159
+ """The verified coordinator of a member checkout, or None outside a member."""
160
+ checkout = Path(checkout)
161
+ marker = _read_marker(_marker_path(checkout))
162
+ if marker is None:
163
+ return None
164
+ stale = MembersError(
165
+ f"member marker for {marker['name']} is stale; "
166
+ "run members.py repair --repo <coordinator>"
167
+ )
168
+ try:
169
+ root, state = _coordinator(Path(marker["coordinator"]))
170
+ listed = read_members(root)
171
+ common = _common_dir(checkout)
172
+ for member in listed:
173
+ if member["name"] == marker["name"] and _common_dir(Path(member["checkout"])) == common:
174
+ if state.project != marker["project"]:
175
+ break
176
+ return {"coordinator": root, "project": state.project, "name": member["name"]}
177
+ except (MembersError, OSError) as error:
178
+ raise stale from error
179
+ raise stale
180
+
181
+
182
+ def detect_member(checkout: Path) -> dict[str, object]:
183
+ """Whether a repo is a member: its coordinator, name, and whether its marker is current."""
184
+ checkout = Path(checkout).resolve()
185
+ try:
186
+ named = marker_coordinator(checkout)
187
+ except MembersError as error:
188
+ return {"member": True, "current": False, "checkout": str(checkout), "reason": str(error)}
189
+ if named is None:
190
+ return {"member": False, "checkout": str(checkout)}
191
+ try:
192
+ role = member_role(checkout)
193
+ except MembersError as error:
194
+ return {"member": True, "current": False, "checkout": str(checkout),
195
+ "coordinator": str(named), "reason": str(error)}
196
+ return {"member": True, "current": True, "checkout": str(checkout), "coordinator": str(role["coordinator"]),
197
+ "project": role["project"], "name": role["name"]}
198
+
199
+
200
+ def read_members(coordinator: Path) -> list[dict[str, str]]:
201
+ """Parse MEMBERS.md; an absent file means a single-repo project."""
202
+ path = coordinator / ".project" / MEMBERS_FILE
203
+ if not path.exists() and not path.is_symlink():
204
+ return []
205
+ if path.is_symlink() or not path.is_file():
206
+ raise MembersError("MEMBERS.md must be a regular file")
207
+ members: list[dict[str, str]] = []
208
+ current: Optional[dict[str, str]] = None
209
+ preamble: list[str] = []
210
+ for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
211
+ if line.startswith("## "):
212
+ name = line[3:].strip()
213
+ if not NAME_RE.fullmatch(name):
214
+ raise MembersError(f"MEMBERS.md line {number}: invalid member name {name!r}")
215
+ current = {"name": name}
216
+ members.append(current)
217
+ continue
218
+ if current is None:
219
+ preamble.append(line)
220
+ continue
221
+ if not line.strip():
222
+ continue
223
+ key, separator, value = line.partition(":")
224
+ if not separator or key not in FIELDS or key.lower() in current:
225
+ raise MembersError(f"MEMBERS.md line {number}: unexpected line {line!r}")
226
+ current[key.lower()] = value.strip()
227
+ if not preamble or preamble[0] != "# Members":
228
+ raise MembersError("MEMBERS.md format error: expected # Members")
229
+ rest = preamble[1:]
230
+ while rest and not rest[0]:
231
+ rest.pop(0)
232
+ comment = HEADER.splitlines()[2:]
233
+ if rest[:len(comment)] == comment:
234
+ rest = rest[len(comment):]
235
+ if any(rest):
236
+ raise MembersError("MEMBERS.md format error: unexpected pre-section content")
237
+ names = [member["name"] for member in members]
238
+ if len(set(names)) != len(names):
239
+ raise MembersError("MEMBERS.md repeats a member name")
240
+ for member in members:
241
+ missing = [field for field in FIELDS if not member.get(field.lower())]
242
+ if missing:
243
+ raise MembersError(f"MEMBERS.md member {member['name']} lacks {', '.join(missing)}")
244
+ if member["integration"] not in INTEGRATIONS:
245
+ raise MembersError(
246
+ f"MEMBERS.md member {member['name']} has invalid Integration: {member['integration']}"
247
+ )
248
+ if not Path(member["checkout"]).is_absolute():
249
+ raise MembersError(f"MEMBERS.md member {member['name']} Checkout must be absolute")
250
+ return members
251
+
252
+
253
+ def check_member(
254
+ coordinator: Path,
255
+ coordinator_name: str,
256
+ checkout: Path,
257
+ recorded_remote: Optional[str] = None,
258
+ refuse_reserved_refs: bool = True,
259
+ ) -> str:
260
+ """Refuse a checkout that cannot join; return its origin URL."""
261
+ if checkout.is_symlink() or not checkout.is_dir():
262
+ raise MembersError(f"member checkout is not a real directory: {checkout}")
263
+ checkout = checkout.resolve()
264
+ if coordinator in checkout.parents or checkout in coordinator.parents:
265
+ raise MembersError(f"member checkout is nested with the coordinator: {checkout}")
266
+ if Path(_git(checkout, "rev-parse", "--show-toplevel")).resolve() != checkout:
267
+ raise MembersError(f"member checkout must be its Git root: {checkout}")
268
+ if _git(checkout, "rev-parse", "--show-superproject-working-tree"):
269
+ raise MembersError(f"member checkout is a submodule: {checkout}")
270
+ common = Path(_git(checkout, "rev-parse", "--path-format=absolute", "--git-common-dir"))
271
+ coordinator_common = Path(
272
+ _git(coordinator, "rev-parse", "--path-format=absolute", "--git-common-dir")
273
+ )
274
+ if common.resolve() == coordinator_common.resolve():
275
+ raise MembersError("the coordinator cannot be its own member")
276
+ if _git(checkout, "status", "--porcelain", "--untracked-files=all"):
277
+ raise MembersError(f"member has uncommitted changes: {checkout}")
278
+ # The configured URL, not `get-url`, which expands local insteadOf rewrites.
279
+ remote = _git(checkout, "config", "--get", "remote.origin.url")
280
+ if not GITHUB_REMOTE_RE.fullmatch(remote):
281
+ raise MembersError(f"member requires a GitHub.com origin: {remote}")
282
+ if recorded_remote is not None and remote != recorded_remote:
283
+ raise MembersError(f"member origin changed: {recorded_remote} -> {remote}")
284
+ default = _common.run_git(checkout, "symbolic-ref", "--short", "refs/remotes/origin/HEAD")
285
+ if default.returncode != 0 or default.stdout.strip() != "origin/main":
286
+ raise MembersError(f"member remote default must be main: {checkout}")
287
+ require_origin_main(checkout)
288
+ member_state = checkout / ".project" / "STATE.md"
289
+ if member_state.exists() or member_state.is_symlink():
290
+ if member_state.is_symlink() or not member_state.is_file():
291
+ raise MembersError("member STATE.md is unreadable")
292
+ try:
293
+ state, _, _ = pipeline_state.load_state(checkout)
294
+ except pipeline_state.PipelineStateError as error:
295
+ raise MembersError(f"member STATE.md is unreadable: {error}") from error
296
+ if state.milestone is not None and (state.phase, state.status) != ("shipped", "done"):
297
+ raise MembersError(f"member has an active milestone: {state.milestone}")
298
+ if refuse_reserved_refs:
299
+ prefixes = member_ref_prefixes(coordinator_name) + tuple(
300
+ "refs/remotes/origin/" + branch.format(coordinator_name) for branch in MEMBER_BRANCH_FORMATS
301
+ )
302
+ refs = _git(checkout, "for-each-ref", "--format=%(refname)").splitlines()
303
+ colliding = [ref for ref in refs if ref.startswith(prefixes)]
304
+ if colliding:
305
+ raise MembersError("member refs collide with coordinator names: " + ", ".join(colliding))
306
+ return remote
307
+
308
+
309
+ MEMBER_CLOSE_SCHEMA = "gsd-path/member-close/v1"
310
+
311
+
312
+ def member_close_path(common: Path, archive_name: str) -> Path:
313
+ """Ship's member-close journal; outside the archive so the archive inventory is unchanged."""
314
+ return common / "gsd-path" / "member-close" / f"{archive_name}.json"
315
+
316
+
317
+ def read_member_close(common: Path, archive_name: str) -> list[dict]:
318
+ path = member_close_path(common, archive_name)
319
+ if not path.is_file():
320
+ return []
321
+ data = json.loads(path.read_text(encoding="utf-8"))
322
+ if not isinstance(data, dict) or data.get("schema") != MEMBER_CLOSE_SCHEMA or not isinstance(data.get("members"), list):
323
+ raise MembersError(f"member-close journal is invalid: {path}")
324
+ return data["members"]
325
+
326
+
327
+ def milestone_member_close(common: Path, milestone: int) -> list[dict]:
328
+ """Member-close rows of milestone M<milestone>, whatever archive name it closed under."""
329
+ rows: list[dict] = []
330
+ for path in sorted((common / "gsd-path" / "member-close").glob(f"{milestone:03d}-*.json")):
331
+ rows.extend(read_member_close(common, path.stem))
332
+ return rows
333
+
334
+
335
+ def write_member_close(common: Path, archive_name: str, rows: list[dict]) -> None:
336
+ path = member_close_path(common, archive_name)
337
+ path.parent.mkdir(parents=True, exist_ok=True)
338
+ _common.atomic_write(path, json.dumps({"schema": MEMBER_CLOSE_SCHEMA, "members": rows}, indent=2) + "\n")
339
+
340
+
341
+ def require_origin_main(checkout: Path) -> None:
342
+ baseline = _common.run_git(
343
+ checkout, "rev-parse", "--verify", "--quiet", "refs/remotes/origin/main^{commit}"
344
+ )
345
+ if baseline.returncode != 0:
346
+ raise MembersError("member requires refs/remotes/origin/main; run git fetch origin")
347
+
348
+
349
+ def member_ref_prefixes(project: str) -> tuple[str, ...]:
350
+ """Ref prefixes a member repo reserves for the coordinator named `project`."""
351
+ return tuple(
352
+ "refs/heads/" + branch.format(project) for branch in MEMBER_BRANCH_FORMATS
353
+ ) + (MEMBER_TAG_FORMAT.format(project),)
354
+
355
+
356
+ def _authorization_ref(project: str, kind: str, ref: str) -> str:
357
+ if not NAME_RE.fullmatch(project) or not ref.startswith(("refs/heads/", "refs/tags/")):
358
+ raise MembersError(f"cannot authorize {ref} for {project}")
359
+ return f"{AUTHORIZATION_PREFIX}{project}/{kind}/{ref}"
360
+
361
+
362
+ def _authorize(checkout: Path, project: str, kind: str, ref: str, sha: str) -> None:
363
+ authorization = _authorization_ref(project, kind, ref)
364
+ _git(checkout, "update-ref", authorization, sha)
365
+
366
+
367
+ def clear_authorization(checkout: Path, project: str, kind: str, ref: str) -> None:
368
+ """Remove a push or delete authorization; S4 retirement clears a milestone's own."""
369
+ _git(checkout, "update-ref", "-d", _authorization_ref(project, kind, ref))
370
+
371
+
372
+ def authorize_push(checkout: Path, project: str, ref: str, sha: str) -> None:
373
+ """Authorize `ref` at exactly `sha` until cleared (a commit or tag object)."""
374
+ _authorize(checkout, project, "push", ref, sha)
375
+
376
+
377
+ def authorize_delete(checkout: Path, project: str, ref: str, expected_remote_sha: str) -> None:
378
+ """Allow deleting remote `ref` only while it still points at `expected_remote_sha`."""
379
+ _authorize(checkout, project, "delete", ref, expected_remote_sha)
380
+
381
+
382
+ def authorized(checkout: Path, project: str, kind: str, ref: str, sha: str) -> bool:
383
+ if not ref.startswith(("refs/heads/", "refs/tags/")):
384
+ return False
385
+ result = _common.run_git(
386
+ checkout, "rev-parse", "--verify", "--quiet", _authorization_ref(project, kind, ref)
387
+ )
388
+ return result.returncode == 0 and result.stdout.strip() == sha
389
+
390
+
391
+ def _task_members(coordinator: Path) -> set[str]:
392
+ try:
393
+ from check_task_briefs import _frontmatter
394
+ except ImportError: # pragma: no cover - package import used by tests
395
+ from scripts.check_task_briefs import _frontmatter
396
+ named = set()
397
+ for task in sorted((coordinator / ".project" / "tasks").glob("*.md")):
398
+ fields, error = _frontmatter(task.read_text(encoding="utf-8"))
399
+ if fields is None:
400
+ raise MembersError(f"{task.name}: {error}")
401
+ if fields.get("repo"):
402
+ named.add(str(fields["repo"]))
403
+ return named
404
+
405
+
406
+ def lock_build_members(coordinator: Path) -> Optional[list[dict[str, str]]]:
407
+ """At build start, lock the members tasks name (MEMBERS.md order) and create
408
+ each member's bound branch at its origin/main. None when no task names a member."""
409
+ root, state = _coordinator(coordinator)
410
+ named = _task_members(root)
411
+ lock_path = root / LOCK_PATH
412
+ build_dir = lock_path.parent
413
+ if build_dir.is_symlink() or (build_dir.exists() and not build_dir.is_dir()):
414
+ raise MembersError(f"member lock directory must be real: {build_dir}")
415
+ if lock_path.is_symlink() or (lock_path.exists() and not lock_path.is_file()):
416
+ raise MembersError(f"member lock must be a regular file: {lock_path}")
417
+ if not named:
418
+ lock_path.unlink(missing_ok=True)
419
+ return None
420
+ listed = read_members(root)
421
+ unknown = sorted(named - {member["name"] for member in listed})
422
+ if unknown:
423
+ raise MembersError("tasks name members missing from MEMBERS.md: " + ", ".join(unknown))
424
+ ignore_error = _common.project_ignore_error(root)
425
+ if ignore_error:
426
+ raise MembersError(ignore_error)
427
+ bound = _common.BOUND_BRANCH_RE.fullmatch(state.branch or "")
428
+ if bound is None:
429
+ raise MembersError(f"coordinator branch is not a bound branch: {state.branch}")
430
+ branch = f"gsd-path/{state.project}-M{bound.group(1)}"
431
+ previous = {}
432
+ if lock_path.is_file():
433
+ previous = {entry["name"]: entry for entry in json.loads(lock_path.read_text(encoding="utf-8"))["members"]}
434
+ entries = []
435
+ missing = []
436
+ for member in listed:
437
+ if member["name"] not in named:
438
+ continue
439
+ checkout = Path(member["checkout"])
440
+ role = member_role(checkout)
441
+ if role is None or role["coordinator"] != root or role["name"] != member["name"]:
442
+ raise MembersError(f"member marker for {member['name']} is missing or stale; "
443
+ f"run members.py repair --repo {root}")
444
+ remote = _git(checkout, "config", "--get", "remote.origin.url")
445
+ if remote != member["remote"]:
446
+ raise MembersError(f"member origin changed: {member['remote']} -> {remote}")
447
+ require_origin_main(checkout)
448
+ base = _git(checkout, "rev-parse", "refs/remotes/origin/main^{commit}")
449
+ existing = _common.run_git(checkout, "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}")
450
+ if existing.returncode == 0:
451
+ tip = existing.stdout.strip()
452
+ locked = previous.get(member["name"])
453
+ # Recovery re-entry keeps the locked base; otherwise the branch must be unused.
454
+ if locked and locked["branch"] == branch and _common.run_git(
455
+ checkout, "merge-base", "--is-ancestor", locked["base"], tip
456
+ ).returncode == 0:
457
+ base = locked["base"]
458
+ elif _common.run_git(checkout, "merge-base", "--is-ancestor", tip, base).returncode == 0:
459
+ base = tip
460
+ else:
461
+ raise MembersError(f"member {member['name']} branch {branch} has commits not on origin/main")
462
+ else:
463
+ missing.append((checkout, base))
464
+ entries.append({"name": member["name"], "branch": branch, "base": base})
465
+ for checkout, base in missing:
466
+ _git(checkout, "update-ref", f"refs/heads/{branch}", base, "")
467
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
468
+ _common.atomic_write(lock_path, json.dumps({"schema": LOCK_SCHEMA, "members": entries}, indent=2) + "\n")
469
+ return entries
470
+
471
+
472
+ def render(members: Sequence[dict[str, str]]) -> str:
473
+ sections = [
474
+ f"## {member['name']}\n"
475
+ f"Checkout: {member['checkout']}\n"
476
+ f"Remote: {member['remote']}\n"
477
+ f"Integration: {member['integration']}\n"
478
+ for member in members
479
+ ]
480
+ return "\n".join([HEADER, *sections])
481
+
482
+
483
+ def add_member(repo: Path, name: str, checkout: Path, integration: str) -> list[dict[str, str]]:
484
+ root, state = _coordinator(repo)
485
+ if state.phase in {"build", "ship"}:
486
+ raise MembersError("members change only at a milestone boundary, not during build or ship")
487
+ if not NAME_RE.fullmatch(name):
488
+ raise MembersError(f"invalid member name: {name!r}")
489
+ members = read_members(root)
490
+ resolved = checkout.resolve()
491
+ remote = check_member(root, state.project, checkout)
492
+ common = Path(_git(resolved, "rev-parse", "--path-format=absolute", "--git-common-dir")).resolve()
493
+ identity = _remote_identity(remote)
494
+ for member in members:
495
+ previous = Path(member["checkout"])
496
+ if (member["name"] == name or previous == resolved
497
+ or _remote_identity(member["remote"]) == identity
498
+ or Path(_git(previous, "rev-parse", "--path-format=absolute", "--git-common-dir")).resolve() == common):
499
+ raise MembersError(f"member already recorded: {member['name']}")
500
+ _refuse_foreign_marker(resolved, root, state.project, name)
501
+ members.append(
502
+ {"name": name, "checkout": str(resolved), "remote": remote, "integration": integration}
503
+ )
504
+ _write_marker(resolved, root, state.project, name)
505
+ _common.atomic_write(root / ".project" / MEMBERS_FILE, render(members))
506
+ return members
507
+
508
+
509
+ MEMBER_CREATE_SCHEMA = "gsd-path/member-create/v1"
510
+ VISIBILITIES = ("public", "private", "internal")
511
+
512
+
513
+ def _gh(*arguments: str) -> subprocess.CompletedProcess:
514
+ return subprocess.run(["gh", *arguments], capture_output=True, text=True, check=False,
515
+ env={**os.environ, "GH_HOST": "github.com"})
516
+
517
+
518
+ def create_member(repo: Path, name: str, checkout: Path, integration: str, github: str,
519
+ visibility: str) -> list[dict[str, str]]:
520
+ """Greenfield member: create the approved GitHub repo, clone it, then join it.
521
+
522
+ A journal in the coordinator Git directory keeps the approved target before any
523
+ external action; a rerun resumes each step and never creates a second repo."""
524
+ root, state = _coordinator(repo)
525
+ if state.phase in {"build", "ship"}:
526
+ raise MembersError("members change only at a milestone boundary, not during build or ship")
527
+ if not NAME_RE.fullmatch(name) or not re.fullmatch(r"[A-Za-z0-9-]+/[A-Za-z0-9._-]+", github):
528
+ raise MembersError(f"invalid member name or GitHub repository: {name!r} {github!r}")
529
+ if visibility not in VISIBILITIES:
530
+ raise MembersError(f"visibility must be one of {', '.join(VISIBILITIES)}")
531
+ if checkout.is_symlink():
532
+ raise MembersError(f"member checkout path is a symlink: {checkout}")
533
+ resolved = checkout.resolve()
534
+ if resolved == root or root in resolved.parents or resolved in root.parents:
535
+ raise MembersError(f"member checkout is nested with the coordinator: {resolved}")
536
+ if not resolved.parent.is_dir():
537
+ raise MembersError(f"member checkout parent does not exist: {resolved.parent}")
538
+ target = {"schema": MEMBER_CREATE_SCHEMA, "name": name, "github": github, "visibility": visibility,
539
+ "checkout": str(resolved), "integration": integration}
540
+ common = Path(_git(root, "rev-parse", "--path-format=absolute", "--git-common-dir"))
541
+ journal = common / "gsd-path" / "member-create" / f"{name}.json"
542
+ url = f"https://github.com/{github}.git"
543
+ if journal.is_file():
544
+ recorded = json.loads(journal.read_text(encoding="utf-8"))
545
+ if {key: recorded.get(key) for key in target} != target or recorded.get("step") not in {"create", "clone", "join"}:
546
+ raise MembersError(f"member-create journal for {name} holds a different approved target: {journal}")
547
+ else:
548
+ if any(member["name"] == name for member in read_members(root)):
549
+ raise MembersError(f"member already recorded: {name}")
550
+ if resolved.is_symlink() or (resolved.exists() and not resolved.is_dir()):
551
+ raise MembersError(f"member checkout path is occupied: {resolved}")
552
+ occupied = resolved.exists() and any(resolved.iterdir())
553
+ if occupied and Path(_common.run_git(resolved, "rev-parse", "--show-toplevel").stdout.strip()).resolve() != resolved:
554
+ raise MembersError(f"member checkout path is occupied: {resolved}")
555
+ if occupied and _git(resolved, "config", "--get", "remote.origin.url") != url:
556
+ raise MembersError(f"member checkout {resolved} is not a clone of {url}")
557
+ if not journal.is_file():
558
+ journal.parent.mkdir(parents=True, exist_ok=True)
559
+ recorded = {**target, "step": "create"}
560
+ _common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
561
+ listed = [member for member in read_members(root) if member["name"] == name]
562
+ if listed:
563
+ if (len(listed) != 1 or listed[0] != {"name": name, "checkout": str(resolved), "remote": url,
564
+ "integration": integration}
565
+ or member_role(resolved) != {"coordinator": root, "project": state.project, "name": name}):
566
+ raise MembersError(f"member already recorded with a different checkout or marker: {name}")
567
+ journal.unlink()
568
+ return read_members(root)
569
+ viewed = _gh("repo", "view", github, "--json", "visibility")
570
+ if viewed.returncode != 0:
571
+ if "Could not resolve to a Repository" not in f"{viewed.stdout}\n{viewed.stderr}":
572
+ raise MembersError(f"could not inspect {github}: {(viewed.stderr or viewed.stdout).strip()}")
573
+ if recorded["step"] != "create":
574
+ raise MembersError(f"GitHub repository {github} disappeared after creation; refusing to create it again")
575
+ if occupied:
576
+ raise MembersError(f"member checkout {resolved} is a clone of a missing repository: {github}")
577
+ created = _gh("repo", "create", github, f"--{visibility}", "--add-readme")
578
+ if created.returncode != 0:
579
+ raise MembersError(f"could not create {github}: {(created.stderr or created.stdout).strip()}")
580
+ recorded["step"] = "clone"
581
+ _common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
582
+ viewed = _gh("repo", "view", github, "--json", "visibility")
583
+ if viewed.returncode != 0:
584
+ raise MembersError(f"could not inspect {github}: {(viewed.stderr or viewed.stdout).strip()}")
585
+ try:
586
+ actual_visibility = json.loads(viewed.stdout)["visibility"].lower()
587
+ except (ValueError, KeyError, AttributeError) as error:
588
+ raise MembersError(f"could not read {github} visibility") from error
589
+ if actual_visibility != visibility:
590
+ raise MembersError(f"GitHub repository visibility is {actual_visibility}, not {visibility}")
591
+ recorded["step"] = "clone"
592
+ _common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
593
+ if occupied:
594
+ fetched = _common.run_git(resolved, "fetch", "-q", "--prune", "origin")
595
+ if fetched.returncode != 0:
596
+ raise MembersError(f"could not fetch member checkout {resolved}: {(fetched.stderr or fetched.stdout).strip()}")
597
+ default = _common.run_git(resolved, "remote", "set-head", "origin", "--auto")
598
+ if default.returncode != 0 or _git(resolved, "symbolic-ref", "--short", "refs/remotes/origin/HEAD") != "origin/main":
599
+ raise MembersError(f"member remote default must be main: {resolved}")
600
+ baseline = _common.run_git(resolved, "rev-parse", "--verify", "--quiet", "refs/remotes/origin/main^{commit}")
601
+ if baseline.returncode != 0:
602
+ raise MembersError(f"member checkout {resolved} has no current origin/main")
603
+ head = _common.run_git(resolved, "rev-parse", "--verify", "--quiet", "HEAD^{commit}")
604
+ if head.returncode == 0:
605
+ if _common.run_git(resolved, "merge-base", "--is-ancestor", "HEAD", "origin/main").returncode != 0:
606
+ raise MembersError(f"member checkout {resolved} has history unrelated to current origin/main")
607
+ else:
608
+ _git(resolved, "checkout", "-B", "main", "origin/main")
609
+ else:
610
+ cloned = _common.run_git(resolved.parent, "clone", "-q", url, str(resolved))
611
+ if cloned.returncode != 0:
612
+ raise MembersError(f"could not clone {url}: {(cloned.stderr or cloned.stdout).strip()}")
613
+ recorded["step"] = "join"
614
+ _common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
615
+ joined = add_member(root, name, resolved, integration)
616
+ journal.unlink()
617
+ return joined
618
+
619
+
620
+ def validate_members(repo: Path) -> list[dict[str, str]]:
621
+ root, state = _coordinator(repo)
622
+ members = read_members(root)
623
+ for member in members:
624
+ checkout = Path(member["checkout"])
625
+ check_member(root, state.project, checkout, member["remote"], refuse_reserved_refs=False)
626
+ try:
627
+ role = member_role(checkout)
628
+ except MembersError as error:
629
+ raise MembersError(f"{error}; run members.py repair --repo {root}") from error
630
+ if role != {"coordinator": root, "project": state.project, "name": member["name"]}:
631
+ raise MembersError(
632
+ f"member marker for {member['name']} is missing or stale; "
633
+ f"run members.py repair --repo {root}"
634
+ )
635
+ return members
636
+
637
+
638
+ def repair_members(repo: Path) -> list[dict[str, str]]:
639
+ """Rewrite missing or stale markers; never take over another coordinator's member."""
640
+ root, state = _coordinator(repo)
641
+ members = read_members(root)
642
+ for member in members:
643
+ checkout = Path(member["checkout"])
644
+ check_member(root, state.project, checkout, member["remote"], refuse_reserved_refs=False)
645
+ _refuse_foreign_marker(checkout.resolve(), root, state.project, member["name"])
646
+ for member in members:
647
+ checkout = Path(member["checkout"]).resolve()
648
+ try:
649
+ role = member_role(checkout)
650
+ except MembersError:
651
+ role = None
652
+ if role != {"coordinator": root, "project": state.project, "name": member["name"]}:
653
+ _write_marker(checkout, root, state.project, member["name"])
654
+ return members
655
+
656
+
657
+ def parser() -> argparse.ArgumentParser:
658
+ result = argparse.ArgumentParser(description=__doc__)
659
+ commands = result.add_subparsers(dest="command", required=True)
660
+ add = commands.add_parser("add", help="record one member repository")
661
+ add.add_argument("--repo", required=True, type=Path, help="coordinator Git root")
662
+ add.add_argument("--name", required=True)
663
+ add.add_argument("--checkout", required=True, type=Path)
664
+ add.add_argument("--integration", choices=INTEGRATIONS, default="default")
665
+ add.add_argument("--create", metavar="OWNER/NAME",
666
+ help="greenfield: create this GitHub repo and clone it to --checkout first")
667
+ add.add_argument("--visibility", choices=VISIBILITIES, help="visibility for --create")
668
+ validate = commands.add_parser("validate", help="check MEMBERS.md and every member")
669
+ validate.add_argument("--repo", required=True, type=Path, help="coordinator Git root")
670
+ detect = commands.add_parser("detect", help="report whether a repo is a member, and of which coordinator")
671
+ detect.add_argument("--checkout", required=True, type=Path, help="repository to inspect")
672
+ repair = commands.add_parser("repair", help="rewrite missing or stale member markers")
673
+ repair.add_argument("--repo", required=True, type=Path, help="coordinator Git root")
674
+ return result
675
+
676
+
677
+ def main(argv: Optional[Sequence[str]] = None) -> int:
678
+ arguments = parser().parse_args(argv)
679
+ try:
680
+ if arguments.command == "detect":
681
+ print(json.dumps(detect_member(arguments.checkout), sort_keys=True))
682
+ return 0
683
+ if arguments.command == "add" and arguments.create:
684
+ if arguments.visibility is None:
685
+ raise MembersError("--create requires --visibility")
686
+ members = create_member(arguments.repo, arguments.name, arguments.checkout,
687
+ arguments.integration, arguments.create, arguments.visibility)
688
+ elif arguments.command == "add":
689
+ if arguments.visibility is not None:
690
+ raise MembersError("--visibility requires --create")
691
+ members = add_member(
692
+ arguments.repo, arguments.name, arguments.checkout, arguments.integration
693
+ )
694
+ elif arguments.command == "repair":
695
+ members = repair_members(arguments.repo)
696
+ else:
697
+ members = validate_members(arguments.repo)
698
+ except (MembersError, OSError) as error:
699
+ print(f"error: {error}", file=sys.stderr)
700
+ return 1
701
+ print(json.dumps({"members": members}, sort_keys=True))
702
+ return 0
703
+
704
+
705
+ if __name__ == "__main__":
706
+ raise SystemExit(main())