@opengsd/gsd-path 1.5.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 (352) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/DOCS.md +4 -3
  3. package/FULL.md +64 -4
  4. package/HOOKS.md +63 -2
  5. package/MIGRATE.md +12 -0
  6. package/QUICK.md +4 -2
  7. package/README.md +67 -15
  8. package/RUNTIME.md +23 -5
  9. package/UPDATE.md +35 -0
  10. package/WORKFLOW.md +58 -3
  11. package/package.json +1 -1
  12. package/scripts/_common.py +107 -1
  13. package/scripts/build_recovery.py +62 -30
  14. package/scripts/build_state.py +195 -5
  15. package/scripts/check_handoffs.py +92 -30
  16. package/scripts/check_task_briefs.py +19 -14
  17. package/scripts/detect_project.py +17 -2
  18. package/scripts/discussion_records.py +9 -1
  19. package/scripts/discussion_validate.py +7 -6
  20. package/scripts/dispatch_driver.py +157 -31
  21. package/scripts/git_guard.py +24 -5
  22. package/scripts/guard_hook.py +1216 -47
  23. package/scripts/install.mjs +76 -3
  24. package/scripts/install.py +182 -37
  25. package/scripts/integration.py +98 -64
  26. package/scripts/isolation.py +148 -11
  27. package/scripts/members.py +68 -25
  28. package/scripts/migrate_core.py +2 -0
  29. package/scripts/model_policy.py +5 -1
  30. package/scripts/pipeline_diagnose.py +17 -2
  31. package/scripts/pipeline_git.py +68 -30
  32. package/scripts/pipeline_state.py +28 -6
  33. package/scripts/pipeline_undo.py +17 -8
  34. package/scripts/promote_lookahead.py +3 -3
  35. package/scripts/review_findings.py +180 -44
  36. package/scripts/roadmap.py +9 -1
  37. package/scripts/runtime_store.py +1 -0
  38. package/scripts/state_checkpoint.py +30 -9
  39. package/scripts/state_promote.py +15 -4
  40. package/scripts/task_context.py +2 -0
  41. package/scripts/workflow_run.py +9 -2
  42. package/skills/gsd-path/DEFINE.md +6 -2
  43. package/skills/gsd-path/PLAN.md +3 -0
  44. package/skills/gsd-path/ROADMAP.md +16 -6
  45. package/skills/gsd-path/SHIP.md +26 -4
  46. package/skills/gsd-path/SKILL.md +52 -24
  47. package/skills/gsd-path/references/build-native.md +96 -9
  48. package/skills/gsd-path/references/build-recovery.md +13 -0
  49. package/skills/gsd-path/references/coder.md +7 -3
  50. package/skills/gsd-path/references/operating-rules.md +11 -3
  51. package/skills/gsd-path/references/planner.md +13 -1
  52. package/skills/gsd-path/references/reviewer-final.md +5 -5
  53. package/skills/gsd-path/references/reviewer-wave.md +46 -13
  54. package/skills/gsd-path/references/runtime-selection.md +9 -0
  55. package/skills/gsd-path/scripts/_common.py +107 -1
  56. package/skills/gsd-path/scripts/build_recovery.py +62 -30
  57. package/skills/gsd-path/scripts/build_state.py +195 -5
  58. package/skills/gsd-path/scripts/check_handoffs.py +92 -30
  59. package/skills/gsd-path/scripts/check_task_briefs.py +19 -14
  60. package/skills/gsd-path/scripts/detect_project.py +17 -2
  61. package/skills/gsd-path/scripts/discussion_records.py +9 -1
  62. package/skills/gsd-path/scripts/discussion_validate.py +7 -6
  63. package/skills/gsd-path/scripts/dispatch_driver.py +157 -31
  64. package/skills/gsd-path/scripts/git_guard.py +24 -5
  65. package/skills/gsd-path/scripts/integration.py +98 -64
  66. package/skills/gsd-path/scripts/isolation.py +148 -11
  67. package/skills/gsd-path/scripts/members.py +68 -25
  68. package/skills/gsd-path/scripts/model_policy.py +5 -1
  69. package/skills/gsd-path/scripts/pipeline_git.py +68 -30
  70. package/skills/gsd-path/scripts/pipeline_state.py +28 -6
  71. package/skills/gsd-path/scripts/promote_lookahead.py +3 -3
  72. package/skills/gsd-path/scripts/review_findings.py +180 -44
  73. package/skills/gsd-path/scripts/roadmap.py +9 -1
  74. package/skills/gsd-path/scripts/state_checkpoint.py +30 -9
  75. package/skills/gsd-path/scripts/state_promote.py +15 -4
  76. package/skills/gsd-path/scripts/task_context.py +2 -0
  77. package/skills/gsd-path/scripts/workflow_run.py +9 -2
  78. package/skills/gsd-path/templates/final-review.md +5 -5
  79. package/skills/gsd-path/templates/state.md +1 -0
  80. package/skills/gsd-path/templates/task.md +1 -0
  81. package/skills/gsd-path/templates/wave-review.md +18 -9
  82. package/skills/gsd-path-build/references/build-native.md +96 -9
  83. package/skills/gsd-path-build/references/build-recovery.md +13 -0
  84. package/skills/gsd-path-build/references/coder.md +7 -3
  85. package/skills/gsd-path-build/references/operating-rules.md +11 -3
  86. package/skills/gsd-path-build/references/reviewer-final.md +5 -5
  87. package/skills/gsd-path-build/references/reviewer-wave.md +46 -13
  88. package/skills/gsd-path-build/references/runtime-selection.md +9 -0
  89. package/skills/gsd-path-build/scripts/_common.py +107 -1
  90. package/skills/gsd-path-build/scripts/build_recovery.py +62 -30
  91. package/skills/gsd-path-build/scripts/build_state.py +195 -5
  92. package/skills/gsd-path-build/scripts/check_handoffs.py +92 -30
  93. package/skills/gsd-path-build/scripts/check_task_briefs.py +19 -14
  94. package/skills/gsd-path-build/scripts/detect_project.py +17 -2
  95. package/skills/gsd-path-build/scripts/discussion_records.py +9 -1
  96. package/skills/gsd-path-build/scripts/discussion_validate.py +7 -6
  97. package/skills/gsd-path-build/scripts/dispatch_driver.py +157 -31
  98. package/skills/gsd-path-build/scripts/integration.py +98 -64
  99. package/skills/gsd-path-build/scripts/isolation.py +148 -11
  100. package/skills/gsd-path-build/scripts/members.py +68 -25
  101. package/skills/gsd-path-build/scripts/model_policy.py +5 -1
  102. package/skills/gsd-path-build/scripts/pipeline_git.py +68 -30
  103. package/skills/gsd-path-build/scripts/pipeline_state.py +28 -6
  104. package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -3
  105. package/skills/gsd-path-build/scripts/review_findings.py +180 -44
  106. package/skills/gsd-path-build/scripts/roadmap.py +9 -1
  107. package/skills/gsd-path-build/scripts/state_checkpoint.py +30 -9
  108. package/skills/gsd-path-build/scripts/state_promote.py +15 -4
  109. package/skills/gsd-path-build/scripts/task_context.py +2 -0
  110. package/skills/gsd-path-build/scripts/workflow_run.py +9 -2
  111. package/skills/gsd-path-build/templates/task.md +1 -0
  112. package/skills/gsd-path-build/templates/wave-review.md +18 -9
  113. package/skills/gsd-path-decide/references/operating-rules.md +11 -3
  114. package/skills/gsd-path-decide/references/runtime-selection.md +9 -0
  115. package/skills/gsd-path-decide/scripts/_common.py +107 -1
  116. package/skills/gsd-path-decide/scripts/build_recovery.py +62 -30
  117. package/skills/gsd-path-decide/scripts/check_handoffs.py +92 -30
  118. package/skills/gsd-path-decide/scripts/check_task_briefs.py +19 -14
  119. package/skills/gsd-path-decide/scripts/detect_project.py +17 -2
  120. package/skills/gsd-path-decide/scripts/discussion_records.py +9 -1
  121. package/skills/gsd-path-decide/scripts/discussion_validate.py +7 -6
  122. package/skills/gsd-path-decide/scripts/integration.py +98 -64
  123. package/skills/gsd-path-decide/scripts/isolation.py +148 -11
  124. package/skills/gsd-path-decide/scripts/members.py +68 -25
  125. package/skills/gsd-path-decide/scripts/model_policy.py +5 -1
  126. package/skills/gsd-path-decide/scripts/pipeline_git.py +68 -30
  127. package/skills/gsd-path-decide/scripts/pipeline_state.py +28 -6
  128. package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -3
  129. package/skills/gsd-path-decide/scripts/roadmap.py +9 -1
  130. package/skills/gsd-path-decide/scripts/state_checkpoint.py +30 -9
  131. package/skills/gsd-path-decide/scripts/state_promote.py +15 -4
  132. package/skills/gsd-path-define/SKILL.md +6 -2
  133. package/skills/gsd-path-define/references/build-recovery.md +13 -0
  134. package/skills/gsd-path-define/references/operating-rules.md +11 -3
  135. package/skills/gsd-path-define/references/runtime-selection.md +9 -0
  136. package/skills/gsd-path-define/scripts/_common.py +107 -1
  137. package/skills/gsd-path-define/scripts/build_recovery.py +62 -30
  138. package/skills/gsd-path-define/scripts/check_handoffs.py +92 -30
  139. package/skills/gsd-path-define/scripts/check_task_briefs.py +19 -14
  140. package/skills/gsd-path-define/scripts/detect_project.py +17 -2
  141. package/skills/gsd-path-define/scripts/discussion_records.py +9 -1
  142. package/skills/gsd-path-define/scripts/discussion_validate.py +7 -6
  143. package/skills/gsd-path-define/scripts/integration.py +98 -64
  144. package/skills/gsd-path-define/scripts/isolation.py +148 -11
  145. package/skills/gsd-path-define/scripts/members.py +68 -25
  146. package/skills/gsd-path-define/scripts/model_policy.py +5 -1
  147. package/skills/gsd-path-define/scripts/pipeline_git.py +68 -30
  148. package/skills/gsd-path-define/scripts/pipeline_state.py +28 -6
  149. package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -3
  150. package/skills/gsd-path-define/scripts/roadmap.py +9 -1
  151. package/skills/gsd-path-define/scripts/state_checkpoint.py +30 -9
  152. package/skills/gsd-path-define/scripts/state_promote.py +15 -4
  153. package/skills/gsd-path-define/templates/state.md +1 -0
  154. package/skills/gsd-path-discuss/references/operating-rules.md +11 -3
  155. package/skills/gsd-path-discuss/references/runtime-selection.md +9 -0
  156. package/skills/gsd-path-discuss/scripts/_common.py +107 -1
  157. package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
  158. package/skills/gsd-path-discuss/scripts/model_policy.py +5 -1
  159. package/skills/gsd-path-docs-audit/references/operating-rules.md +11 -3
  160. package/skills/gsd-path-docs-audit/references/runtime-selection.md +9 -0
  161. package/skills/gsd-path-docs-audit/scripts/_common.py +107 -1
  162. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +62 -30
  163. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +92 -30
  164. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +19 -14
  165. package/skills/gsd-path-docs-audit/scripts/detect_project.py +17 -2
  166. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +9 -1
  167. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +7 -6
  168. package/skills/gsd-path-docs-audit/scripts/integration.py +98 -64
  169. package/skills/gsd-path-docs-audit/scripts/isolation.py +148 -11
  170. package/skills/gsd-path-docs-audit/scripts/members.py +68 -25
  171. package/skills/gsd-path-docs-audit/scripts/model_policy.py +5 -1
  172. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +68 -30
  173. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +28 -6
  174. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -3
  175. package/skills/gsd-path-docs-audit/scripts/roadmap.py +9 -1
  176. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +30 -9
  177. package/skills/gsd-path-docs-audit/scripts/state_promote.py +15 -4
  178. package/skills/gsd-path-forensics/references/operating-rules.md +11 -3
  179. package/skills/gsd-path-forensics/references/runtime-selection.md +9 -0
  180. package/skills/gsd-path-forensics/scripts/_common.py +107 -1
  181. package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
  182. package/skills/gsd-path-forensics/scripts/model_policy.py +5 -1
  183. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +17 -2
  184. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +17 -8
  185. package/skills/gsd-path-inspect/references/operating-rules.md +11 -3
  186. package/skills/gsd-path-inspect/references/runtime-selection.md +9 -0
  187. package/skills/gsd-path-inspect/scripts/_common.py +107 -1
  188. package/skills/gsd-path-inspect/scripts/build_recovery.py +62 -30
  189. package/skills/gsd-path-inspect/scripts/check_handoffs.py +92 -30
  190. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +19 -14
  191. package/skills/gsd-path-inspect/scripts/detect_project.py +17 -2
  192. package/skills/gsd-path-inspect/scripts/discussion_records.py +9 -1
  193. package/skills/gsd-path-inspect/scripts/discussion_validate.py +7 -6
  194. package/skills/gsd-path-inspect/scripts/integration.py +98 -64
  195. package/skills/gsd-path-inspect/scripts/isolation.py +148 -11
  196. package/skills/gsd-path-inspect/scripts/members.py +68 -25
  197. package/skills/gsd-path-inspect/scripts/model_policy.py +5 -1
  198. package/skills/gsd-path-inspect/scripts/pipeline_git.py +68 -30
  199. package/skills/gsd-path-inspect/scripts/pipeline_state.py +28 -6
  200. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -3
  201. package/skills/gsd-path-inspect/scripts/roadmap.py +9 -1
  202. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +30 -9
  203. package/skills/gsd-path-inspect/scripts/state_promote.py +15 -4
  204. package/skills/gsd-path-inspect/scripts/workflow_run.py +9 -2
  205. package/skills/gsd-path-inspect/templates/state.md +1 -0
  206. package/skills/gsd-path-loop/references/operating-rules.md +11 -3
  207. package/skills/gsd-path-loop/references/runtime-selection.md +9 -0
  208. package/skills/gsd-path-loop/scripts/_common.py +107 -1
  209. package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
  210. package/skills/gsd-path-loop/scripts/model_policy.py +5 -1
  211. package/skills/gsd-path-migrate/SKILL.md +8 -0
  212. package/skills/gsd-path-migrate/references/operating-rules.md +11 -3
  213. package/skills/gsd-path-migrate/references/runtime-selection.md +9 -0
  214. package/skills/gsd-path-migrate/scripts/_common.py +107 -1
  215. package/skills/gsd-path-migrate/scripts/discussion_records.py +9 -1
  216. package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -0
  217. package/skills/gsd-path-migrate/scripts/model_policy.py +5 -1
  218. package/skills/gsd-path-plan/SKILL.md +3 -0
  219. package/skills/gsd-path-plan/references/build-recovery.md +13 -0
  220. package/skills/gsd-path-plan/references/operating-rules.md +11 -3
  221. package/skills/gsd-path-plan/references/planner.md +13 -1
  222. package/skills/gsd-path-plan/references/runtime-selection.md +9 -0
  223. package/skills/gsd-path-plan/scripts/_common.py +107 -1
  224. package/skills/gsd-path-plan/scripts/build_recovery.py +62 -30
  225. package/skills/gsd-path-plan/scripts/check_handoffs.py +92 -30
  226. package/skills/gsd-path-plan/scripts/check_task_briefs.py +19 -14
  227. package/skills/gsd-path-plan/scripts/detect_project.py +17 -2
  228. package/skills/gsd-path-plan/scripts/discussion_records.py +9 -1
  229. package/skills/gsd-path-plan/scripts/discussion_validate.py +7 -6
  230. package/skills/gsd-path-plan/scripts/integration.py +98 -64
  231. package/skills/gsd-path-plan/scripts/isolation.py +148 -11
  232. package/skills/gsd-path-plan/scripts/members.py +68 -25
  233. package/skills/gsd-path-plan/scripts/model_policy.py +5 -1
  234. package/skills/gsd-path-plan/scripts/pipeline_git.py +68 -30
  235. package/skills/gsd-path-plan/scripts/pipeline_state.py +28 -6
  236. package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -3
  237. package/skills/gsd-path-plan/scripts/roadmap.py +9 -1
  238. package/skills/gsd-path-plan/scripts/state_checkpoint.py +30 -9
  239. package/skills/gsd-path-plan/scripts/state_promote.py +15 -4
  240. package/skills/gsd-path-plan/scripts/workflow_run.py +9 -2
  241. package/skills/gsd-path-plan/templates/task.md +1 -0
  242. package/skills/gsd-path-research/references/operating-rules.md +11 -3
  243. package/skills/gsd-path-research/references/runtime-selection.md +9 -0
  244. package/skills/gsd-path-research/scripts/_common.py +107 -1
  245. package/skills/gsd-path-research/scripts/build_recovery.py +62 -30
  246. package/skills/gsd-path-research/scripts/check_handoffs.py +92 -30
  247. package/skills/gsd-path-research/scripts/check_task_briefs.py +19 -14
  248. package/skills/gsd-path-research/scripts/detect_project.py +17 -2
  249. package/skills/gsd-path-research/scripts/discussion_records.py +9 -1
  250. package/skills/gsd-path-research/scripts/discussion_validate.py +7 -6
  251. package/skills/gsd-path-research/scripts/integration.py +98 -64
  252. package/skills/gsd-path-research/scripts/isolation.py +148 -11
  253. package/skills/gsd-path-research/scripts/members.py +68 -25
  254. package/skills/gsd-path-research/scripts/model_policy.py +5 -1
  255. package/skills/gsd-path-research/scripts/pipeline_git.py +68 -30
  256. package/skills/gsd-path-research/scripts/pipeline_state.py +28 -6
  257. package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -3
  258. package/skills/gsd-path-research/scripts/roadmap.py +9 -1
  259. package/skills/gsd-path-research/scripts/state_checkpoint.py +30 -9
  260. package/skills/gsd-path-research/scripts/state_promote.py +15 -4
  261. package/skills/gsd-path-roadmap/SKILL.md +16 -6
  262. package/skills/gsd-path-roadmap/references/operating-rules.md +11 -3
  263. package/skills/gsd-path-roadmap/references/runtime-selection.md +9 -0
  264. package/skills/gsd-path-roadmap/scripts/_common.py +107 -1
  265. package/skills/gsd-path-roadmap/scripts/build_recovery.py +62 -30
  266. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +92 -30
  267. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +19 -14
  268. package/skills/gsd-path-roadmap/scripts/detect_project.py +17 -2
  269. package/skills/gsd-path-roadmap/scripts/discussion_records.py +9 -1
  270. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +7 -6
  271. package/skills/gsd-path-roadmap/scripts/integration.py +98 -64
  272. package/skills/gsd-path-roadmap/scripts/isolation.py +148 -11
  273. package/skills/gsd-path-roadmap/scripts/members.py +68 -25
  274. package/skills/gsd-path-roadmap/scripts/model_policy.py +5 -1
  275. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +68 -30
  276. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +28 -6
  277. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -3
  278. package/skills/gsd-path-roadmap/scripts/roadmap.py +9 -1
  279. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +30 -9
  280. package/skills/gsd-path-roadmap/scripts/state_promote.py +15 -4
  281. package/skills/gsd-path-ship/SKILL.md +26 -4
  282. package/skills/gsd-path-ship/references/operating-rules.md +11 -3
  283. package/skills/gsd-path-ship/references/reviewer-final.md +5 -5
  284. package/skills/gsd-path-ship/references/reviewer-wave.md +46 -13
  285. package/skills/gsd-path-ship/references/runtime-selection.md +9 -0
  286. package/skills/gsd-path-ship/scripts/_common.py +107 -1
  287. package/skills/gsd-path-ship/scripts/build_recovery.py +62 -30
  288. package/skills/gsd-path-ship/scripts/build_state.py +195 -5
  289. package/skills/gsd-path-ship/scripts/check_handoffs.py +92 -30
  290. package/skills/gsd-path-ship/scripts/check_task_briefs.py +19 -14
  291. package/skills/gsd-path-ship/scripts/detect_project.py +17 -2
  292. package/skills/gsd-path-ship/scripts/discussion_records.py +9 -1
  293. package/skills/gsd-path-ship/scripts/discussion_validate.py +7 -6
  294. package/skills/gsd-path-ship/scripts/integration.py +98 -64
  295. package/skills/gsd-path-ship/scripts/isolation.py +148 -11
  296. package/skills/gsd-path-ship/scripts/members.py +68 -25
  297. package/skills/gsd-path-ship/scripts/model_policy.py +5 -1
  298. package/skills/gsd-path-ship/scripts/pipeline_git.py +68 -30
  299. package/skills/gsd-path-ship/scripts/pipeline_state.py +28 -6
  300. package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -3
  301. package/skills/gsd-path-ship/scripts/roadmap.py +9 -1
  302. package/skills/gsd-path-ship/scripts/state_checkpoint.py +30 -9
  303. package/skills/gsd-path-ship/scripts/state_promote.py +15 -4
  304. package/skills/gsd-path-ship/scripts/workflow_run.py +9 -2
  305. package/skills/gsd-path-ship/templates/final-review.md +5 -5
  306. package/skills/gsd-path-ship/templates/wave-review.md +18 -9
  307. package/skills/gsd-path-undo/references/operating-rules.md +11 -3
  308. package/skills/gsd-path-undo/references/runtime-selection.md +9 -0
  309. package/skills/gsd-path-undo/scripts/_common.py +107 -1
  310. package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
  311. package/skills/gsd-path-undo/scripts/model_policy.py +5 -1
  312. package/skills/gsd-path-undo/scripts/pipeline_undo.py +17 -8
  313. package/skills/path/DEFINE.md +6 -2
  314. package/skills/path/PLAN.md +3 -0
  315. package/skills/path/ROADMAP.md +16 -6
  316. package/skills/path/SHIP.md +26 -4
  317. package/skills/path/SKILL.md +52 -24
  318. package/skills/path/references/build-native.md +96 -9
  319. package/skills/path/references/build-recovery.md +13 -0
  320. package/skills/path/references/coder.md +7 -3
  321. package/skills/path/references/operating-rules.md +11 -3
  322. package/skills/path/references/planner.md +13 -1
  323. package/skills/path/references/reviewer-final.md +5 -5
  324. package/skills/path/references/reviewer-wave.md +46 -13
  325. package/skills/path/references/runtime-selection.md +9 -0
  326. package/skills/path/scripts/_common.py +107 -1
  327. package/skills/path/scripts/build_recovery.py +62 -30
  328. package/skills/path/scripts/build_state.py +195 -5
  329. package/skills/path/scripts/check_handoffs.py +92 -30
  330. package/skills/path/scripts/check_task_briefs.py +19 -14
  331. package/skills/path/scripts/detect_project.py +17 -2
  332. package/skills/path/scripts/discussion_records.py +9 -1
  333. package/skills/path/scripts/discussion_validate.py +7 -6
  334. package/skills/path/scripts/dispatch_driver.py +157 -31
  335. package/skills/path/scripts/git_guard.py +24 -5
  336. package/skills/path/scripts/integration.py +98 -64
  337. package/skills/path/scripts/isolation.py +148 -11
  338. package/skills/path/scripts/members.py +68 -25
  339. package/skills/path/scripts/model_policy.py +5 -1
  340. package/skills/path/scripts/pipeline_git.py +68 -30
  341. package/skills/path/scripts/pipeline_state.py +28 -6
  342. package/skills/path/scripts/promote_lookahead.py +3 -3
  343. package/skills/path/scripts/review_findings.py +180 -44
  344. package/skills/path/scripts/roadmap.py +9 -1
  345. package/skills/path/scripts/state_checkpoint.py +30 -9
  346. package/skills/path/scripts/state_promote.py +15 -4
  347. package/skills/path/scripts/task_context.py +2 -0
  348. package/skills/path/scripts/workflow_run.py +9 -2
  349. package/skills/path/templates/final-review.md +5 -5
  350. package/skills/path/templates/state.md +1 -0
  351. package/skills/path/templates/task.md +1 -0
  352. package/skills/path/templates/wave-review.md +18 -9
@@ -11,6 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  import argparse
13
13
  import contextlib
14
+ import functools
14
15
  import hashlib
15
16
  import io
16
17
  import json
@@ -103,6 +104,10 @@ class IsolationError(RuntimeError):
103
104
  """Raised when isolation, landing, or retirement cannot proceed."""
104
105
 
105
106
 
107
+ class RemovalError(IsolationError):
108
+ """Raised when the removal of a sidecar worktree fails."""
109
+
110
+
106
111
  run_git = _common.run_git
107
112
 
108
113
 
@@ -146,7 +151,10 @@ def require_commit(repo: Path, value: str) -> str:
146
151
 
147
152
  def require_full_sha(value: str) -> str:
148
153
  if not re.fullmatch(r"[0-9a-f]{40}", value):
149
- raise IsolationError(f"base must be a full 40-character SHA: {value}")
154
+ raise IsolationError(
155
+ f"base must be a full 40-character SHA: {value}; "
156
+ "resolve a ref or a short SHA with git rev-parse first"
157
+ )
150
158
  return value
151
159
 
152
160
 
@@ -714,6 +722,18 @@ def member_checkout(coordinator: Path, member: str) -> Tuple[Path, str]:
714
722
  return checkout, str(role["project"])
715
723
 
716
724
 
725
+ def member_default_branch(coordinator: Path, member: str) -> str:
726
+ """The default branch that MEMBERS.md records for a member; a row without it means main."""
727
+ try:
728
+ import members
729
+ except ImportError: # pragma: no cover - package import used by tests
730
+ from scripts import members
731
+ try:
732
+ return members.member_default_branch(require_directory(coordinator, "coordinator"), member)
733
+ except members.MembersError as error:
734
+ raise IsolationError(str(error)) from error
735
+
736
+
717
737
  def member_bound_checkout(coordinator: Path, member: str) -> Dict[str, object]:
718
738
  """Path's own checkout of a locked member's bound branch, outside the team's checkout."""
719
739
  checkout, _, entry = _member_context(coordinator, member)
@@ -833,7 +853,10 @@ def retire_member_task(
833
853
  if destination.resolve() in registered:
834
854
  if registered[destination.resolve()] != ref:
835
855
  raise IsolationError(f"member task sidecar is on another branch: {destination}")
836
- _remove_worktree(checkout, destination, force)
856
+ try:
857
+ _remove_worktree(checkout, destination, force)
858
+ except IsolationError as error:
859
+ raise RemovalError(str(error)) from error
837
860
  authorization = run_git(checkout, "update-ref", "-d", member_task_authorization_ref(project, task_id))
838
861
  if authorization.returncode != 0:
839
862
  raise IsolationError((authorization.stderr or authorization.stdout).strip() or "git update-ref failed")
@@ -1177,7 +1200,7 @@ def _prove_member_commit(
1177
1200
  parent, commit).splitlines())
1178
1201
  if not changed:
1179
1202
  return "commit changes nothing"
1180
- undeclared = sorted(changed - allowed)
1203
+ undeclared = undeclared_paths(repo, parent, changed, allowed)
1181
1204
  if undeclared:
1182
1205
  return "undeclared paths: " + ", ".join(undeclared)
1183
1206
  if git_output(repo, "log", "-1", "--format=%s", commit) != subject:
@@ -1193,7 +1216,7 @@ def _commit_member_source(sidecar: Path, journal: Dict[str, object], allowed: Se
1193
1216
  if pending:
1194
1217
  if current_sha(sidecar) != member_base:
1195
1218
  raise IsolationError("dirty member source HEAD must equal the member base")
1196
- undeclared = sorted(pending - allowed)
1219
+ undeclared = undeclared_paths(sidecar, member_base, pending, allowed)
1197
1220
  if undeclared:
1198
1221
  raise IsolationError("undeclared paths: " + ", ".join(undeclared))
1199
1222
  git_output(sidecar, "add", "-A", "--", *sorted(pending))
@@ -1671,7 +1694,7 @@ def _commit_pending(
1671
1694
  mode = "-f" if path in forced else "-A"
1672
1695
  added = run_git(repo, "add", mode, "--", path)
1673
1696
  if added.returncode != 0:
1674
- raise IsolationError(f"could not stage {path}")
1697
+ raise IsolationError(f"could not stage {path}: {added.stderr.strip()}")
1675
1698
  staged = _split_paths(
1676
1699
  git_output(
1677
1700
  repo,
@@ -1690,6 +1713,32 @@ def _commit_pending(
1690
1713
  return current_sha(repo)
1691
1714
 
1692
1715
 
1716
+ def undeclared_paths(
1717
+ repo: Path, base: str, paths: Set[str], declared: Set[str]
1718
+ ) -> list[str]:
1719
+ """Return the sorted paths that the declared entries do not cover.
1720
+
1721
+ An entry covers itself and, as a declared directory, the files below it.
1722
+ An entry that is a file at `base` covers only itself: the task-brief lint
1723
+ rejects a path below a base file.
1724
+ """
1725
+
1726
+ @functools.lru_cache(maxsize=None)
1727
+ def directory(entry: str) -> bool:
1728
+ kind = run_git(repo, "cat-file", "-t", f"{base}:{entry}")
1729
+ return kind.returncode != 0 or kind.stdout.strip() == "tree"
1730
+
1731
+ return sorted(
1732
+ path
1733
+ for path in paths
1734
+ if path not in declared
1735
+ and not any(
1736
+ str(parent) in declared and directory(str(parent))
1737
+ for parent in list(PurePosixPath(path).parents)[:-1]
1738
+ )
1739
+ )
1740
+
1741
+
1693
1742
  def validate_allowed_changes(
1694
1743
  repo: Path,
1695
1744
  base: str,
@@ -1703,7 +1752,7 @@ def validate_allowed_changes(
1703
1752
  if not landing:
1704
1753
  raise IsolationError("no changes to land")
1705
1754
  since_base = committed_paths_since(repo, base) | landing
1706
- unexpected = sorted(path for path in since_base if path not in allowed)
1755
+ unexpected = undeclared_paths(repo, base, since_base, allowed)
1707
1756
  if unexpected:
1708
1757
  raise IsolationError(
1709
1758
  "unexpected paths: " + ", ".join(unexpected)
@@ -1740,7 +1789,7 @@ def commit_allowed_changes(
1740
1789
  for path in sorted(staging):
1741
1790
  added = run_git(repo, "add", "-A", "--", path)
1742
1791
  if added.returncode != 0:
1743
- raise IsolationError(f"could not stage {path}")
1792
+ raise IsolationError(f"could not stage {path}: {added.stderr.strip()}")
1744
1793
  staged = _split_paths(
1745
1794
  git_output(repo, "diff", "--cached", "--name-only", "--relative")
1746
1795
  )
@@ -2619,7 +2668,7 @@ def _prove_task_commit(
2619
2668
  )
2620
2669
  if task_file not in changed:
2621
2670
  return None, "commit does not touch the task file"
2622
- stray = sorted(changed - contract_paths)
2671
+ stray = undeclared_paths(repo, base, changed, contract_paths)
2623
2672
  if stray:
2624
2673
  return None, f"commit touches undeclared paths: {', '.join(stray)}"
2625
2674
  if landing_error:
@@ -2711,6 +2760,56 @@ def _task_branch_proof_error(
2711
2760
  return None
2712
2761
 
2713
2762
 
2763
+ def only_missing_files(worktree: Path) -> bool:
2764
+ """True when the only difference of a worktree from its HEAD is tracked files that are missing.
2765
+
2766
+ An untracked or a gitignored file is content that Git does not have, so it makes this False.
2767
+ """
2768
+ status = run_git(worktree, "status", "--porcelain", "--untracked-files=all", "--ignored=matching")
2769
+ lines = status.stdout.splitlines()
2770
+ return status.returncode == 0 and bool(lines) and all(line.startswith(" D") for line in lines)
2771
+
2772
+
2773
+ def holds_only_landed_files(repo: Path, path: Path, commits: Sequence[str]) -> bool:
2774
+ """True when `path` is a directory, not a usable worktree, that holds only files of `commits`.
2775
+
2776
+ Each file must be the same blob at the same path in one of the commits, so the removal of
2777
+ the directory loses nothing that Git does not have. A check that cannot be done is False.
2778
+ """
2779
+
2780
+ def unreadable(error: OSError) -> None:
2781
+ raise error
2782
+
2783
+ try:
2784
+ if path.is_symlink() or not path.is_dir() or path.resolve() != path:
2785
+ return False
2786
+ root = run_git(path, "rev-parse", "--show-toplevel")
2787
+ if root.returncode == 0 and Path(root.stdout.strip()).resolve() == path:
2788
+ return False
2789
+ files = []
2790
+ for folder, folders, names in os.walk(path, onerror=unreadable):
2791
+ if any(os.path.islink(os.path.join(folder, name)) for name in folders):
2792
+ return False
2793
+ files += [os.path.join(folder, name) for name in names]
2794
+ # What a removal left of the worktree link is not content.
2795
+ files = [name for name in files if name != str(path / ".git")]
2796
+ if not all(stat.S_ISREG(os.lstat(name).st_mode) for name in files):
2797
+ return False
2798
+ known = {
2799
+ entry.split(" ", 2)[2]
2800
+ for commit in commits
2801
+ for entry in git_text(repo, "ls-tree", "-r", "-z", commit).split("\0")
2802
+ if entry
2803
+ }
2804
+ hashed = run_git(repo, "hash-object", "--stdin-paths", input="".join(f"{name}\n" for name in files))
2805
+ except (OSError, IsolationError):
2806
+ return False
2807
+ hashes = hashed.stdout.split()
2808
+ return hashed.returncode == 0 and len(hashes) == len(files) and all(
2809
+ f"{sha}\t{Path(name).relative_to(path).as_posix()}" in known for sha, name in zip(hashes, files)
2810
+ )
2811
+
2812
+
2714
2813
  def _worktree_report(
2715
2814
  primary: Path, path: Path, expected_branch: str
2716
2815
  ) -> tuple[Optional[Dict[str, object]], Optional[str]]:
@@ -2944,6 +3043,7 @@ def _recover_task(
2944
3043
  head: str,
2945
3044
  bound: str,
2946
3045
  canonical_task_file: Optional[str] = None,
3046
+ inspect_isolate: bool = True,
2947
3047
  ) -> Dict[str, object]:
2948
3048
  task_file = canonical_task_file or relative_posix(str(path.relative_to(primary)))
2949
3049
  report: Dict[str, object] = {"task": task_file}
@@ -2951,6 +3051,11 @@ def _recover_task(
2951
3051
  def result(verdict: str, **extra: object) -> Dict[str, object]:
2952
3052
  return {**report, "verdict": verdict, **extra}
2953
3053
 
3054
+ def leftover(commit: object, base: object) -> Dict[str, object]:
3055
+ # The landing is proven, so this block names only what is left to remove.
3056
+ return result("block", reason=f"task landed at {commit}; remove its leftover isolate: {sidecar}",
3057
+ commit=commit, base=base, leftover=str(sidecar))
3058
+
2954
3059
  try:
2955
3060
  task_text, _ = _read_task_text(path)
2956
3061
  except IsolationError as error:
@@ -2977,14 +3082,22 @@ def _recover_task(
2977
3082
  return result(**_prove_member_task(primary, task_file, fields, subject, history))
2978
3083
  task_branch = task_branch_name(task_id)
2979
3084
  branch_exists = task_branch in branches
2980
- worktree, worktree_error = _worktree_report(
2981
- primary, sidecar_root(primary, "task", task_id), task_branch
3085
+ sidecar = sidecar_root(primary, "task", task_id)
3086
+ worktree, worktree_error = (
3087
+ _worktree_report(primary, sidecar, task_branch) if inspect_isolate else (None, None)
2982
3088
  )
2983
3089
  report["worktree"] = worktree
2984
3090
  report["task_branch"] = task_branch if branch_exists else None
2985
3091
  report["rejected"] = []
2986
3092
 
2987
3093
  if worktree_error:
3094
+ landed = _recover_task(
3095
+ primary, path, history, branches, head, bound, canonical_task_file, False
3096
+ ) if status == "done" else {}
3097
+ if landed.get("verdict") == "recovered" and holds_only_landed_files(
3098
+ primary, sidecar, [str(landed["commit"]), *([task_branch] if branch_exists else [])]
3099
+ ):
3100
+ return leftover(landed["commit"], landed["base"])
2988
3101
  return result("block", reason=worktree_error)
2989
3102
 
2990
3103
  valid_statuses = {"pending", "in-progress", "done", "failed", "blocked"}
@@ -3238,6 +3351,9 @@ def _recover_task(
3238
3351
  if branch_error:
3239
3352
  return result("block", reason=branch_error)
3240
3353
  if worktree is not None and not bool(worktree["clean"]):
3354
+ if only_missing_files(sidecar):
3355
+ # Only landed files are missing: a removal stopped part way, and nothing unknown is there.
3356
+ return leftover(commit, base)
3241
3357
  return result(
3242
3358
  "block",
3243
3359
  reason="proven task retains a dirty worktree",
@@ -3803,7 +3919,11 @@ def _attest_body_fields(body: str) -> tuple[Dict[str, str], list[str]]:
3803
3919
 
3804
3920
 
3805
3921
  def _attested_changes(repo: Path, base: str, head: str, declared: Set[str]) -> list[str]:
3806
- """Declared paths that changed between base and head, matched literally like land."""
3922
+ """Declared paths that changed between base and head, matched literally.
3923
+
3924
+ Land also accepts files below a declared directory. Attestation does not:
3925
+ a recorded attestation body must stay provable.
3926
+ """
3807
3927
  changed = set(git_output(repo, "diff", "--name-only", base, head).splitlines())
3808
3928
  return sorted(changed & declared)
3809
3929
 
@@ -4711,6 +4831,23 @@ def retire_unused_verify(
4711
4831
  return retire(primary, expected, branch, False)
4712
4832
 
4713
4833
 
4834
+ def retire_stale_verify(primary: Path, worktree: Path, branch: str) -> Dict[str, object]:
4835
+ """Retire the Verify sidecar of an earlier prepare-task only when it is proven untouched.
4836
+
4837
+ The reflog of its branch is the record of its creation base. It must hold only the
4838
+ creation, at a commit of the bound branch; `retire_unused_verify` then proves the checkout.
4839
+ """
4840
+ try:
4841
+ created = git_output(primary, "reflog", "show", "--format=%H", f"refs/heads/{branch}").split()
4842
+ if len(created) != 1:
4843
+ raise IsolationError("its branch moved after its creation, or has no record of its creation")
4844
+ if run_git(primary, "merge-base", "--is-ancestor", created[0], "HEAD").returncode != 0:
4845
+ raise IsolationError("its branch is not at a commit of the bound branch")
4846
+ return retire_unused_verify(primary, worktree, created[0], branch)
4847
+ except IsolationError as error:
4848
+ raise IsolationError(f"Verify sidecar {worktree} is kept: {error}") from error
4849
+
4850
+
4714
4851
  def parser() -> argparse.ArgumentParser:
4715
4852
  argument_parser = argparse.ArgumentParser(description=__doc__)
4716
4853
  subparsers = argument_parser.add_subparsers(dest="command", required=True)
@@ -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,17 +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,
277
+ recorded: Optional[dict[str, str]] = None,
258
278
  refuse_reserved_refs: bool = True,
259
- ) -> str:
260
- """Refuse a checkout that cannot join; return its origin URL."""
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."""
261
284
  if checkout.is_symlink() or not checkout.is_dir():
262
285
  raise MembersError(f"member checkout is not a real directory: {checkout}")
263
286
  checkout = checkout.resolve()
@@ -279,12 +302,21 @@ def check_member(
279
302
  remote = _git(checkout, "config", "--get", "remote.origin.url")
280
303
  if not GITHUB_REMOTE_RE.fullmatch(remote):
281
304
  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)
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)
288
320
  member_state = checkout / ".project" / "STATE.md"
289
321
  if member_state.exists() or member_state.is_symlink():
290
322
  if member_state.is_symlink() or not member_state.is_file():
@@ -303,7 +335,7 @@ def check_member(
303
335
  colliding = [ref for ref in refs if ref.startswith(prefixes)]
304
336
  if colliding:
305
337
  raise MembersError("member refs collide with coordinator names: " + ", ".join(colliding))
306
- return remote
338
+ return remote, default
307
339
 
308
340
 
309
341
  MEMBER_CLOSE_SCHEMA = "gsd-path/member-close/v1"
@@ -338,12 +370,14 @@ def write_member_close(common: Path, archive_name: str, rows: list[dict]) -> Non
338
370
  _common.atomic_write(path, json.dumps({"schema": MEMBER_CLOSE_SCHEMA, "members": rows}, indent=2) + "\n")
339
371
 
340
372
 
341
- 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`."""
342
375
  baseline = _common.run_git(
343
- checkout, "rev-parse", "--verify", "--quiet", "refs/remotes/origin/main^{commit}"
376
+ checkout, "rev-parse", "--verify", "--quiet", f"refs/remotes/origin/{branch}^{{commit}}"
344
377
  )
345
378
  if baseline.returncode != 0:
346
- 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()
347
381
 
348
382
 
349
383
  def member_ref_prefixes(project: str) -> tuple[str, ...]:
@@ -405,7 +439,8 @@ def _task_members(coordinator: Path) -> set[str]:
405
439
 
406
440
  def lock_build_members(coordinator: Path) -> Optional[list[dict[str, str]]]:
407
441
  """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."""
442
+ each member's bound branch at its recorded default branch on origin. None when
443
+ no task names a member."""
409
444
  root, state = _coordinator(coordinator)
410
445
  named = _task_members(root)
411
446
  lock_path = root / LOCK_PATH
@@ -444,8 +479,8 @@ def lock_build_members(coordinator: Path) -> Optional[list[dict[str, str]]]:
444
479
  remote = _git(checkout, "config", "--get", "remote.origin.url")
445
480
  if remote != member["remote"]:
446
481
  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}")
482
+ default = default_branch(member)
483
+ base = require_origin_branch(checkout, default)
449
484
  existing = _common.run_git(checkout, "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}")
450
485
  if existing.returncode == 0:
451
486
  tip = existing.stdout.strip()
@@ -458,7 +493,7 @@ def lock_build_members(coordinator: Path) -> Optional[list[dict[str, str]]]:
458
493
  elif _common.run_git(checkout, "merge-base", "--is-ancestor", tip, base).returncode == 0:
459
494
  base = tip
460
495
  else:
461
- 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}")
462
497
  else:
463
498
  missing.append((checkout, base))
464
499
  entries.append({"name": member["name"], "branch": branch, "base": base})
@@ -475,6 +510,8 @@ def render(members: Sequence[dict[str, str]]) -> str:
475
510
  f"Checkout: {member['checkout']}\n"
476
511
  f"Remote: {member['remote']}\n"
477
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 "")
478
515
  for member in members
479
516
  ]
480
517
  return "\n".join([HEADER, *sections])
@@ -488,7 +525,7 @@ def add_member(repo: Path, name: str, checkout: Path, integration: str) -> list[
488
525
  raise MembersError(f"invalid member name: {name!r}")
489
526
  members = read_members(root)
490
527
  resolved = checkout.resolve()
491
- remote = check_member(root, state.project, checkout)
528
+ remote, default = check_member(root, state.project, checkout)
492
529
  common = Path(_git(resolved, "rev-parse", "--path-format=absolute", "--git-common-dir")).resolve()
493
530
  identity = _remote_identity(remote)
494
531
  for member in members:
@@ -499,7 +536,8 @@ def add_member(repo: Path, name: str, checkout: Path, integration: str) -> list[
499
536
  raise MembersError(f"member already recorded: {member['name']}")
500
537
  _refuse_foreign_marker(resolved, root, state.project, name)
501
538
  members.append(
502
- {"name": name, "checkout": str(resolved), "remote": remote, "integration": integration}
539
+ {"name": name, "checkout": str(resolved), "remote": remote, "integration": integration,
540
+ "default_branch": default}
503
541
  )
504
542
  _write_marker(resolved, root, state.project, name)
505
543
  _common.atomic_write(root / ".project" / MEMBERS_FILE, render(members))
@@ -560,8 +598,9 @@ def create_member(repo: Path, name: str, checkout: Path, integration: str, githu
560
598
  _common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
561
599
  listed = [member for member in read_members(root) if member["name"] == name]
562
600
  if listed:
563
- if (len(listed) != 1 or listed[0] != {"name": name, "checkout": str(resolved), "remote": url,
564
- "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"}
565
604
  or member_role(resolved) != {"coordinator": root, "project": state.project, "name": name}):
566
605
  raise MembersError(f"member already recorded with a different checkout or marker: {name}")
567
606
  journal.unlink()
@@ -610,6 +649,10 @@ def create_member(repo: Path, name: str, checkout: Path, integration: str, githu
610
649
  cloned = _common.run_git(resolved.parent, "clone", "-q", url, str(resolved))
611
650
  if cloned.returncode != 0:
612
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}")
613
656
  recorded["step"] = "join"
614
657
  _common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
615
658
  joined = add_member(root, name, resolved, integration)
@@ -622,7 +665,7 @@ def validate_members(repo: Path) -> list[dict[str, str]]:
622
665
  members = read_members(root)
623
666
  for member in members:
624
667
  checkout = Path(member["checkout"])
625
- check_member(root, state.project, checkout, member["remote"], refuse_reserved_refs=False)
668
+ check_member(root, state.project, checkout, member, refuse_reserved_refs=False)
626
669
  try:
627
670
  role = member_role(checkout)
628
671
  except MembersError as error:
@@ -641,7 +684,7 @@ def repair_members(repo: Path) -> list[dict[str, str]]:
641
684
  members = read_members(root)
642
685
  for member in members:
643
686
  checkout = Path(member["checkout"])
644
- check_member(root, state.project, checkout, member["remote"], refuse_reserved_refs=False)
687
+ check_member(root, state.project, checkout, member, refuse_reserved_refs=False)
645
688
  _refuse_foreign_marker(checkout.resolve(), root, state.project, member["name"])
646
689
  for member in members:
647
690
  checkout = Path(member["checkout"]).resolve()
@@ -10,6 +10,8 @@ import shutil
10
10
  import stat
11
11
  import sys
12
12
 
13
+ import _common
14
+
13
15
 
14
16
  def hook_inventory(settings):
15
17
  if settings.is_symlink() or not settings.is_file():
@@ -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,6 +13,7 @@ import json
13
13
  import os
14
14
  import shlex
15
15
  import sys
16
+ import traceback
16
17
 
17
18
  # Runtime helpers must not modify their immutable installation.
18
19
  sys.dont_write_bytecode = True
@@ -86,7 +87,7 @@ def _load_diagnose_modules():
86
87
  pipeline_git,
87
88
  pipeline_state,
88
89
  )
89
- except ModuleNotFoundError as error:
90
+ except ImportError as error: # a foreign `scripts` package
90
91
  if error.name not in {
91
92
  "scripts",
92
93
  "scripts.archive_milestone",
@@ -98,6 +99,13 @@ def _load_diagnose_modules():
98
99
  "scripts.pipeline_state",
99
100
  }:
100
101
  raise
102
+ # A plain ImportError raised while a `scripts` module loaded is not a
103
+ # foreign package. A missing module falls back, as before.
104
+ if not isinstance(error, ModuleNotFoundError) and any(
105
+ frame.f_globals.get("__name__", "").startswith("scripts.")
106
+ for frame, _ in traceback.walk_tb(error.__traceback__)
107
+ ):
108
+ raise
101
109
  shared = Path(__file__).resolve().parents[2] / "gsd-path" / "scripts"
102
110
  sys.path.insert(0, str(shared))
103
111
  import archive_milestone
@@ -147,9 +155,16 @@ else:
147
155
  raise
148
156
  try:
149
157
  from scripts import pipeline_undo
150
- except ModuleNotFoundError as error:
158
+ except ImportError as error: # a foreign `scripts` package
151
159
  if error.name not in {"scripts", "scripts.pipeline_undo"}:
152
160
  raise
161
+ # A plain ImportError raised while a `scripts` module loaded is not a
162
+ # foreign package. A missing module falls back, as before.
163
+ if not isinstance(error, ModuleNotFoundError) and any(
164
+ frame.f_globals.get("__name__", "").startswith("scripts.")
165
+ for frame, _ in traceback.walk_tb(error.__traceback__)
166
+ ):
167
+ raise
153
168
  shared = Path(__file__).resolve().parents[2] / "gsd-path" / "scripts"
154
169
  if str(shared) not in sys.path:
155
170
  sys.path.insert(0, str(shared))