@opengsd/gsd-path 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. package/AGENTS.md +2 -207
  2. package/CHANGELOG.md +294 -0
  3. package/DOCS.md +19 -10
  4. package/FULL.md +9 -3
  5. package/GUIDE.md +1 -1
  6. package/HOOKS.md +9 -6
  7. package/QUICK.md +4 -1
  8. package/README.md +63 -11
  9. package/RUNTIME.md +1 -1
  10. package/UPDATE.md +61 -12
  11. package/WORKFLOW.md +22 -6
  12. package/package.json +9 -8
  13. package/scripts/_common.py +472 -19
  14. package/scripts/archive_milestone.py +146 -73
  15. package/scripts/bootstrap_repository.py +8 -5
  16. package/scripts/build_recovery.py +4 -1
  17. package/scripts/build_state.py +66 -23
  18. package/scripts/check_docs_audit.py +14 -3
  19. package/scripts/check_handoffs.py +116 -20
  20. package/scripts/check_task_briefs.py +60 -8
  21. package/scripts/check_update.py +2 -2
  22. package/scripts/core_hook_gate.py +10 -1
  23. package/scripts/core_hook_settings.py +11 -4
  24. package/scripts/detect_project.py +345 -15
  25. package/scripts/discussion_records.py +19 -13
  26. package/scripts/discussion_validate.py +66 -21
  27. package/scripts/dispatch_driver.py +111 -26
  28. package/scripts/git_guard.py +116 -29
  29. package/scripts/guard_hook.py +151 -29
  30. package/scripts/install.mjs +55 -39
  31. package/scripts/install.py +564 -41
  32. package/scripts/integration.py +434 -24
  33. package/scripts/isolation.py +1077 -28
  34. package/scripts/lean_verification.py +99 -19
  35. package/scripts/loop_run.py +20 -30
  36. package/scripts/members.py +704 -0
  37. package/scripts/migrate_core.py +2 -2
  38. package/scripts/path_config.py +2 -2
  39. package/scripts/pipeline_diagnose.py +82 -31
  40. package/scripts/pipeline_git.py +61 -22
  41. package/scripts/pipeline_state.py +176 -54
  42. package/scripts/pipeline_undo.py +117 -22
  43. package/scripts/promote_lookahead.py +1 -1
  44. package/scripts/review_panel.py +1 -1
  45. package/scripts/runtime_store.py +49 -11
  46. package/scripts/skill-resources.json +28 -3
  47. package/scripts/state_checkpoint.py +140 -31
  48. package/scripts/state_promote.py +2 -0
  49. package/scripts/status_runtime.py +36 -6
  50. package/scripts/sync_skill_resources.py +8 -7
  51. package/scripts/token_budget.py +12 -8
  52. package/scripts/workflow_run.py +103 -29
  53. package/scripts/worktree_paths.py +18 -8
  54. package/skills/gsd-path/BUILD.md +7 -1
  55. package/skills/gsd-path/DECIDE.md +11 -4
  56. package/skills/gsd-path/DEFINE.md +8 -1
  57. package/skills/gsd-path/DOCS-AUDIT.md +6 -4
  58. package/skills/gsd-path/INSPECT.md +5 -4
  59. package/skills/gsd-path/PLAN.md +7 -1
  60. package/skills/gsd-path/RESEARCH.md +2 -1
  61. package/skills/gsd-path/ROADMAP.md +2 -1
  62. package/skills/gsd-path/SHIP.md +27 -10
  63. package/skills/gsd-path/SKILL.md +45 -1
  64. package/skills/gsd-path/references/build-native.md +47 -14
  65. package/skills/gsd-path/references/codebase-mapper.md +2 -1
  66. package/skills/gsd-path/references/docs-auditor.md +2 -1
  67. package/skills/gsd-path/references/operating-rules.md +214 -0
  68. package/skills/gsd-path/references/planner.md +8 -0
  69. package/skills/gsd-path/references/reviewer-wave.md +3 -1
  70. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  71. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  72. package/skills/gsd-path/scripts/_common.py +472 -19
  73. package/skills/gsd-path/scripts/archive_milestone.py +146 -73
  74. package/skills/gsd-path/scripts/bootstrap_repository.py +8 -5
  75. package/skills/gsd-path/scripts/build_recovery.py +4 -1
  76. package/skills/gsd-path/scripts/build_state.py +66 -23
  77. package/skills/gsd-path/scripts/check_docs_audit.py +14 -3
  78. package/skills/gsd-path/scripts/check_handoffs.py +116 -20
  79. package/skills/gsd-path/scripts/check_task_briefs.py +60 -8
  80. package/skills/gsd-path/scripts/check_update.py +2 -2
  81. package/skills/gsd-path/scripts/detect_project.py +345 -15
  82. package/skills/gsd-path/scripts/discussion_records.py +19 -13
  83. package/skills/gsd-path/scripts/discussion_validate.py +66 -21
  84. package/skills/gsd-path/scripts/dispatch_driver.py +111 -26
  85. package/skills/gsd-path/scripts/git_guard.py +116 -29
  86. package/skills/gsd-path/scripts/integration.py +434 -24
  87. package/skills/gsd-path/scripts/isolation.py +1077 -28
  88. package/skills/gsd-path/scripts/lean_verification.py +99 -19
  89. package/skills/gsd-path/scripts/loop_run.py +20 -30
  90. package/skills/gsd-path/scripts/members.py +704 -0
  91. package/skills/gsd-path/scripts/path_config.py +2 -2
  92. package/skills/gsd-path/scripts/pipeline_git.py +61 -22
  93. package/skills/gsd-path/scripts/pipeline_state.py +176 -54
  94. package/skills/gsd-path/scripts/promote_lookahead.py +1 -1
  95. package/skills/gsd-path/scripts/review_panel.py +1 -1
  96. package/skills/gsd-path/scripts/state_checkpoint.py +140 -31
  97. package/skills/gsd-path/scripts/state_promote.py +2 -0
  98. package/skills/gsd-path/scripts/status_runtime.py +36 -6
  99. package/skills/gsd-path/scripts/token_budget.py +12 -8
  100. package/skills/gsd-path/scripts/workflow_run.py +103 -29
  101. package/skills/gsd-path/scripts/worktree_paths.py +18 -8
  102. package/skills/gsd-path/templates/codebase.md +1 -1
  103. package/skills/gsd-path/templates/docs-audit.md +1 -1
  104. package/skills/gsd-path/templates/final-review.md +5 -2
  105. package/skills/gsd-path/templates/gap-review.md +2 -1
  106. package/skills/gsd-path/templates/task.md +1 -0
  107. package/skills/gsd-path/templates/wave-review.md +3 -1
  108. package/skills/gsd-path-build/SKILL.md +7 -1
  109. package/skills/gsd-path-build/references/build-native.md +47 -14
  110. package/skills/gsd-path-build/references/operating-rules.md +214 -0
  111. package/skills/gsd-path-build/references/reviewer-wave.md +3 -1
  112. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  113. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  114. package/skills/gsd-path-build/scripts/_common.py +472 -19
  115. package/skills/gsd-path-build/scripts/archive_milestone.py +146 -73
  116. package/skills/gsd-path-build/scripts/build_recovery.py +4 -1
  117. package/skills/gsd-path-build/scripts/build_state.py +66 -23
  118. package/skills/gsd-path-build/scripts/check_handoffs.py +116 -20
  119. package/skills/gsd-path-build/scripts/check_task_briefs.py +60 -8
  120. package/skills/gsd-path-build/scripts/detect_project.py +345 -15
  121. package/skills/gsd-path-build/scripts/discussion_records.py +19 -13
  122. package/skills/gsd-path-build/scripts/discussion_validate.py +66 -21
  123. package/skills/gsd-path-build/scripts/dispatch_driver.py +111 -26
  124. package/skills/gsd-path-build/scripts/integration.py +434 -24
  125. package/skills/gsd-path-build/scripts/isolation.py +1077 -28
  126. package/skills/gsd-path-build/scripts/loop_run.py +20 -30
  127. package/skills/gsd-path-build/scripts/members.py +704 -0
  128. package/skills/gsd-path-build/scripts/path_config.py +2 -2
  129. package/skills/gsd-path-build/scripts/pipeline_git.py +61 -22
  130. package/skills/gsd-path-build/scripts/pipeline_state.py +176 -54
  131. package/skills/gsd-path-build/scripts/promote_lookahead.py +1 -1
  132. package/skills/gsd-path-build/scripts/review_panel.py +1 -1
  133. package/skills/gsd-path-build/scripts/state_checkpoint.py +140 -31
  134. package/skills/gsd-path-build/scripts/state_promote.py +2 -0
  135. package/skills/gsd-path-build/scripts/token_budget.py +12 -8
  136. package/skills/gsd-path-build/scripts/workflow_run.py +103 -29
  137. package/skills/gsd-path-build/scripts/worktree_paths.py +18 -8
  138. package/skills/gsd-path-build/templates/task.md +1 -0
  139. package/skills/gsd-path-build/templates/wave-review.md +3 -1
  140. package/skills/gsd-path-decide/SKILL.md +11 -4
  141. package/skills/gsd-path-decide/references/operating-rules.md +214 -0
  142. package/skills/gsd-path-decide/scripts/_common.py +472 -19
  143. package/skills/gsd-path-decide/scripts/archive_milestone.py +146 -73
  144. package/skills/gsd-path-decide/scripts/build_recovery.py +4 -1
  145. package/skills/gsd-path-decide/scripts/check_handoffs.py +116 -20
  146. package/skills/gsd-path-decide/scripts/check_task_briefs.py +60 -8
  147. package/skills/gsd-path-decide/scripts/detect_project.py +345 -15
  148. package/skills/gsd-path-decide/scripts/discussion_records.py +19 -13
  149. package/skills/gsd-path-decide/scripts/discussion_validate.py +66 -21
  150. package/skills/gsd-path-decide/scripts/integration.py +434 -24
  151. package/skills/gsd-path-decide/scripts/isolation.py +1077 -28
  152. package/skills/gsd-path-decide/scripts/loop_run.py +20 -30
  153. package/skills/gsd-path-decide/scripts/members.py +704 -0
  154. package/skills/gsd-path-decide/scripts/path_config.py +2 -2
  155. package/skills/gsd-path-decide/scripts/pipeline_git.py +61 -22
  156. package/skills/gsd-path-decide/scripts/pipeline_state.py +176 -54
  157. package/skills/gsd-path-decide/scripts/promote_lookahead.py +1 -1
  158. package/skills/gsd-path-decide/scripts/review_panel.py +1 -1
  159. package/skills/gsd-path-decide/scripts/state_checkpoint.py +140 -31
  160. package/skills/gsd-path-decide/scripts/state_promote.py +2 -0
  161. package/skills/gsd-path-decide/scripts/worktree_paths.py +18 -8
  162. package/skills/gsd-path-define/SKILL.md +8 -1
  163. package/skills/gsd-path-define/references/operating-rules.md +214 -0
  164. package/skills/gsd-path-define/scripts/_common.py +472 -19
  165. package/skills/gsd-path-define/scripts/archive_milestone.py +146 -73
  166. package/skills/gsd-path-define/scripts/build_recovery.py +4 -1
  167. package/skills/gsd-path-define/scripts/check_handoffs.py +116 -20
  168. package/skills/gsd-path-define/scripts/check_task_briefs.py +60 -8
  169. package/skills/gsd-path-define/scripts/detect_project.py +345 -15
  170. package/skills/gsd-path-define/scripts/discussion_records.py +19 -13
  171. package/skills/gsd-path-define/scripts/discussion_validate.py +66 -21
  172. package/skills/gsd-path-define/scripts/integration.py +434 -24
  173. package/skills/gsd-path-define/scripts/isolation.py +1077 -28
  174. package/skills/gsd-path-define/scripts/loop_run.py +20 -30
  175. package/skills/gsd-path-define/scripts/members.py +704 -0
  176. package/skills/gsd-path-define/scripts/path_config.py +2 -2
  177. package/skills/gsd-path-define/scripts/pipeline_git.py +61 -22
  178. package/skills/gsd-path-define/scripts/pipeline_state.py +176 -54
  179. package/skills/gsd-path-define/scripts/promote_lookahead.py +1 -1
  180. package/skills/gsd-path-define/scripts/review_panel.py +1 -1
  181. package/skills/gsd-path-define/scripts/state_checkpoint.py +140 -31
  182. package/skills/gsd-path-define/scripts/state_promote.py +2 -0
  183. package/skills/gsd-path-define/scripts/worktree_paths.py +18 -8
  184. package/skills/gsd-path-discuss/SKILL.md +2 -1
  185. package/skills/gsd-path-discuss/references/operating-rules.md +214 -0
  186. package/skills/gsd-path-discuss/scripts/_common.py +472 -19
  187. package/skills/gsd-path-discuss/scripts/discussion_records.py +19 -13
  188. package/skills/gsd-path-discuss/scripts/loop_run.py +20 -30
  189. package/skills/gsd-path-discuss/scripts/path_config.py +2 -2
  190. package/skills/gsd-path-discuss/scripts/review_panel.py +1 -1
  191. package/skills/gsd-path-discuss/scripts/worktree_paths.py +18 -8
  192. package/skills/gsd-path-docs-audit/SKILL.md +6 -4
  193. package/skills/gsd-path-docs-audit/references/docs-auditor.md +2 -1
  194. package/skills/gsd-path-docs-audit/references/operating-rules.md +214 -0
  195. package/skills/gsd-path-docs-audit/scripts/_common.py +472 -19
  196. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +146 -73
  197. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +4 -1
  198. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +14 -3
  199. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +116 -20
  200. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +60 -8
  201. package/skills/gsd-path-docs-audit/scripts/detect_project.py +345 -15
  202. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +19 -13
  203. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +66 -21
  204. package/skills/gsd-path-docs-audit/scripts/integration.py +434 -24
  205. package/skills/gsd-path-docs-audit/scripts/isolation.py +1077 -28
  206. package/skills/gsd-path-docs-audit/scripts/loop_run.py +20 -30
  207. package/skills/gsd-path-docs-audit/scripts/members.py +704 -0
  208. package/skills/gsd-path-docs-audit/scripts/path_config.py +2 -2
  209. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +61 -22
  210. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +176 -54
  211. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +1 -1
  212. package/skills/gsd-path-docs-audit/scripts/review_panel.py +1 -1
  213. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +140 -31
  214. package/skills/gsd-path-docs-audit/scripts/state_promote.py +2 -0
  215. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +18 -8
  216. package/skills/gsd-path-docs-audit/templates/docs-audit.md +1 -1
  217. package/skills/gsd-path-forensics/SKILL.md +2 -1
  218. package/skills/gsd-path-forensics/references/operating-rules.md +214 -0
  219. package/skills/gsd-path-forensics/scripts/_common.py +472 -19
  220. package/skills/gsd-path-forensics/scripts/discussion_records.py +19 -13
  221. package/skills/gsd-path-forensics/scripts/loop_run.py +20 -30
  222. package/skills/gsd-path-forensics/scripts/path_config.py +2 -2
  223. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +82 -31
  224. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +117 -22
  225. package/skills/gsd-path-forensics/scripts/review_panel.py +1 -1
  226. package/skills/gsd-path-forensics/scripts/worktree_paths.py +18 -8
  227. package/skills/gsd-path-inspect/SKILL.md +5 -4
  228. package/skills/gsd-path-inspect/references/codebase-mapper.md +2 -1
  229. package/skills/gsd-path-inspect/references/docs-auditor.md +2 -1
  230. package/skills/gsd-path-inspect/references/operating-rules.md +214 -0
  231. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  232. package/skills/gsd-path-inspect/scripts/_common.py +472 -19
  233. package/skills/gsd-path-inspect/scripts/archive_milestone.py +146 -73
  234. package/skills/gsd-path-inspect/scripts/build_recovery.py +4 -1
  235. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +14 -3
  236. package/skills/gsd-path-inspect/scripts/check_handoffs.py +116 -20
  237. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +60 -8
  238. package/skills/gsd-path-inspect/scripts/detect_project.py +345 -15
  239. package/skills/gsd-path-inspect/scripts/discussion_records.py +19 -13
  240. package/skills/gsd-path-inspect/scripts/discussion_validate.py +66 -21
  241. package/skills/gsd-path-inspect/scripts/integration.py +434 -24
  242. package/skills/gsd-path-inspect/scripts/isolation.py +1077 -28
  243. package/skills/gsd-path-inspect/scripts/loop_run.py +20 -30
  244. package/skills/gsd-path-inspect/scripts/members.py +704 -0
  245. package/skills/gsd-path-inspect/scripts/path_config.py +2 -2
  246. package/skills/gsd-path-inspect/scripts/pipeline_git.py +61 -22
  247. package/skills/gsd-path-inspect/scripts/pipeline_state.py +176 -54
  248. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +1 -1
  249. package/skills/gsd-path-inspect/scripts/review_panel.py +1 -1
  250. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +140 -31
  251. package/skills/gsd-path-inspect/scripts/state_promote.py +2 -0
  252. package/skills/gsd-path-inspect/scripts/workflow_run.py +103 -29
  253. package/skills/gsd-path-inspect/scripts/worktree_paths.py +18 -8
  254. package/skills/gsd-path-inspect/templates/codebase.md +1 -1
  255. package/skills/gsd-path-inspect/templates/docs-audit.md +1 -1
  256. package/skills/gsd-path-loop/SKILL.md +2 -1
  257. package/skills/gsd-path-loop/references/operating-rules.md +214 -0
  258. package/skills/gsd-path-loop/scripts/_common.py +472 -19
  259. package/skills/gsd-path-loop/scripts/discussion_records.py +19 -13
  260. package/skills/gsd-path-loop/scripts/loop_run.py +20 -30
  261. package/skills/gsd-path-loop/scripts/path_config.py +2 -2
  262. package/skills/gsd-path-loop/scripts/review_panel.py +1 -1
  263. package/skills/gsd-path-loop/scripts/worktree_paths.py +18 -8
  264. package/skills/gsd-path-migrate/SKILL.md +2 -1
  265. package/skills/gsd-path-migrate/references/operating-rules.md +214 -0
  266. package/skills/gsd-path-migrate/scripts/_common.py +472 -19
  267. package/skills/gsd-path-migrate/scripts/core_hook_gate.py +10 -1
  268. package/skills/gsd-path-migrate/scripts/core_hook_settings.py +11 -4
  269. package/skills/gsd-path-migrate/scripts/discussion_records.py +19 -13
  270. package/skills/gsd-path-migrate/scripts/loop_run.py +20 -30
  271. package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -2
  272. package/skills/gsd-path-migrate/scripts/path_config.py +2 -2
  273. package/skills/gsd-path-migrate/scripts/review_panel.py +1 -1
  274. package/skills/gsd-path-plan/SKILL.md +7 -1
  275. package/skills/gsd-path-plan/references/operating-rules.md +214 -0
  276. package/skills/gsd-path-plan/references/planner.md +8 -0
  277. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  278. package/skills/gsd-path-plan/scripts/_common.py +472 -19
  279. package/skills/gsd-path-plan/scripts/archive_milestone.py +146 -73
  280. package/skills/gsd-path-plan/scripts/build_recovery.py +4 -1
  281. package/skills/gsd-path-plan/scripts/check_handoffs.py +116 -20
  282. package/skills/gsd-path-plan/scripts/check_task_briefs.py +60 -8
  283. package/skills/gsd-path-plan/scripts/detect_project.py +345 -15
  284. package/skills/gsd-path-plan/scripts/discussion_records.py +19 -13
  285. package/skills/gsd-path-plan/scripts/discussion_validate.py +66 -21
  286. package/skills/gsd-path-plan/scripts/integration.py +434 -24
  287. package/skills/gsd-path-plan/scripts/isolation.py +1077 -28
  288. package/skills/gsd-path-plan/scripts/loop_run.py +20 -30
  289. package/skills/gsd-path-plan/scripts/members.py +704 -0
  290. package/skills/gsd-path-plan/scripts/path_config.py +2 -2
  291. package/skills/gsd-path-plan/scripts/pipeline_git.py +61 -22
  292. package/skills/gsd-path-plan/scripts/pipeline_state.py +176 -54
  293. package/skills/gsd-path-plan/scripts/promote_lookahead.py +1 -1
  294. package/skills/gsd-path-plan/scripts/review_panel.py +1 -1
  295. package/skills/gsd-path-plan/scripts/state_checkpoint.py +140 -31
  296. package/skills/gsd-path-plan/scripts/state_promote.py +2 -0
  297. package/skills/gsd-path-plan/scripts/workflow_run.py +103 -29
  298. package/skills/gsd-path-plan/scripts/worktree_paths.py +18 -8
  299. package/skills/gsd-path-plan/templates/task.md +1 -0
  300. package/skills/gsd-path-research/SKILL.md +2 -1
  301. package/skills/gsd-path-research/references/operating-rules.md +214 -0
  302. package/skills/gsd-path-research/scripts/_common.py +472 -19
  303. package/skills/gsd-path-research/scripts/archive_milestone.py +146 -73
  304. package/skills/gsd-path-research/scripts/build_recovery.py +4 -1
  305. package/skills/gsd-path-research/scripts/check_handoffs.py +116 -20
  306. package/skills/gsd-path-research/scripts/check_task_briefs.py +60 -8
  307. package/skills/gsd-path-research/scripts/detect_project.py +345 -15
  308. package/skills/gsd-path-research/scripts/discussion_records.py +19 -13
  309. package/skills/gsd-path-research/scripts/discussion_validate.py +66 -21
  310. package/skills/gsd-path-research/scripts/integration.py +434 -24
  311. package/skills/gsd-path-research/scripts/isolation.py +1077 -28
  312. package/skills/gsd-path-research/scripts/loop_run.py +20 -30
  313. package/skills/gsd-path-research/scripts/members.py +704 -0
  314. package/skills/gsd-path-research/scripts/path_config.py +2 -2
  315. package/skills/gsd-path-research/scripts/pipeline_git.py +61 -22
  316. package/skills/gsd-path-research/scripts/pipeline_state.py +176 -54
  317. package/skills/gsd-path-research/scripts/promote_lookahead.py +1 -1
  318. package/skills/gsd-path-research/scripts/review_panel.py +1 -1
  319. package/skills/gsd-path-research/scripts/state_checkpoint.py +140 -31
  320. package/skills/gsd-path-research/scripts/state_promote.py +2 -0
  321. package/skills/gsd-path-research/scripts/worktree_paths.py +18 -8
  322. package/skills/gsd-path-roadmap/SKILL.md +2 -1
  323. package/skills/gsd-path-roadmap/references/operating-rules.md +214 -0
  324. package/skills/gsd-path-roadmap/scripts/_common.py +472 -19
  325. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +146 -73
  326. package/skills/gsd-path-roadmap/scripts/build_recovery.py +4 -1
  327. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +116 -20
  328. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +60 -8
  329. package/skills/gsd-path-roadmap/scripts/detect_project.py +345 -15
  330. package/skills/gsd-path-roadmap/scripts/discussion_records.py +19 -13
  331. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +66 -21
  332. package/skills/gsd-path-roadmap/scripts/integration.py +434 -24
  333. package/skills/gsd-path-roadmap/scripts/isolation.py +1077 -28
  334. package/skills/gsd-path-roadmap/scripts/loop_run.py +20 -30
  335. package/skills/gsd-path-roadmap/scripts/members.py +704 -0
  336. package/skills/gsd-path-roadmap/scripts/path_config.py +2 -2
  337. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +61 -22
  338. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +176 -54
  339. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +1 -1
  340. package/skills/gsd-path-roadmap/scripts/review_panel.py +1 -1
  341. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +140 -31
  342. package/skills/gsd-path-roadmap/scripts/state_promote.py +2 -0
  343. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +18 -8
  344. package/skills/gsd-path-ship/SKILL.md +27 -10
  345. package/skills/gsd-path-ship/references/operating-rules.md +214 -0
  346. package/skills/gsd-path-ship/references/reviewer-wave.md +3 -1
  347. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  348. package/skills/gsd-path-ship/scripts/_common.py +472 -19
  349. package/skills/gsd-path-ship/scripts/archive_milestone.py +146 -73
  350. package/skills/gsd-path-ship/scripts/build_recovery.py +4 -1
  351. package/skills/gsd-path-ship/scripts/build_state.py +66 -23
  352. package/skills/gsd-path-ship/scripts/check_handoffs.py +116 -20
  353. package/skills/gsd-path-ship/scripts/check_task_briefs.py +60 -8
  354. package/skills/gsd-path-ship/scripts/detect_project.py +345 -15
  355. package/skills/gsd-path-ship/scripts/discussion_records.py +19 -13
  356. package/skills/gsd-path-ship/scripts/discussion_validate.py +66 -21
  357. package/skills/gsd-path-ship/scripts/integration.py +434 -24
  358. package/skills/gsd-path-ship/scripts/isolation.py +1077 -28
  359. package/skills/gsd-path-ship/scripts/lean_verification.py +99 -19
  360. package/skills/gsd-path-ship/scripts/loop_run.py +20 -30
  361. package/skills/gsd-path-ship/scripts/members.py +704 -0
  362. package/skills/gsd-path-ship/scripts/path_config.py +2 -2
  363. package/skills/gsd-path-ship/scripts/pipeline_git.py +61 -22
  364. package/skills/gsd-path-ship/scripts/pipeline_state.py +176 -54
  365. package/skills/gsd-path-ship/scripts/promote_lookahead.py +1 -1
  366. package/skills/gsd-path-ship/scripts/review_panel.py +1 -1
  367. package/skills/gsd-path-ship/scripts/state_checkpoint.py +140 -31
  368. package/skills/gsd-path-ship/scripts/state_promote.py +2 -0
  369. package/skills/gsd-path-ship/scripts/workflow_run.py +103 -29
  370. package/skills/gsd-path-ship/scripts/worktree_paths.py +18 -8
  371. package/skills/gsd-path-ship/templates/final-review.md +5 -2
  372. package/skills/gsd-path-ship/templates/gap-review.md +2 -1
  373. package/skills/gsd-path-ship/templates/wave-review.md +3 -1
  374. package/skills/gsd-path-undo/SKILL.md +2 -1
  375. package/skills/gsd-path-undo/references/operating-rules.md +214 -0
  376. package/skills/gsd-path-undo/scripts/_common.py +472 -19
  377. package/skills/gsd-path-undo/scripts/discussion_records.py +19 -13
  378. package/skills/gsd-path-undo/scripts/loop_run.py +20 -30
  379. package/skills/gsd-path-undo/scripts/path_config.py +2 -2
  380. package/skills/gsd-path-undo/scripts/pipeline_undo.py +117 -22
  381. package/skills/gsd-path-undo/scripts/review_panel.py +1 -1
  382. package/skills/gsd-path-undo/scripts/worktree_paths.py +18 -8
  383. package/skills/path/BUILD.md +7 -1
  384. package/skills/path/DECIDE.md +11 -4
  385. package/skills/path/DEFINE.md +8 -1
  386. package/skills/path/DOCS-AUDIT.md +6 -4
  387. package/skills/path/INSPECT.md +5 -4
  388. package/skills/path/PLAN.md +7 -1
  389. package/skills/path/RESEARCH.md +2 -1
  390. package/skills/path/ROADMAP.md +2 -1
  391. package/skills/path/SHIP.md +27 -10
  392. package/skills/path/SKILL.md +45 -1
  393. package/skills/path/references/build-native.md +47 -14
  394. package/skills/path/references/codebase-mapper.md +2 -1
  395. package/skills/path/references/docs-auditor.md +2 -1
  396. package/skills/path/references/operating-rules.md +214 -0
  397. package/skills/path/references/planner.md +8 -0
  398. package/skills/path/references/reviewer-wave.md +3 -1
  399. package/skills/path/scripts/_common.py +472 -19
  400. package/skills/path/scripts/archive_milestone.py +146 -73
  401. package/skills/path/scripts/bootstrap_repository.py +8 -5
  402. package/skills/path/scripts/build_recovery.py +4 -1
  403. package/skills/path/scripts/build_state.py +66 -23
  404. package/skills/path/scripts/check_docs_audit.py +14 -3
  405. package/skills/path/scripts/check_handoffs.py +116 -20
  406. package/skills/path/scripts/check_task_briefs.py +60 -8
  407. package/skills/path/scripts/check_update.py +2 -2
  408. package/skills/path/scripts/detect_project.py +345 -15
  409. package/skills/path/scripts/discussion_records.py +19 -13
  410. package/skills/path/scripts/discussion_validate.py +66 -21
  411. package/skills/path/scripts/dispatch_driver.py +111 -26
  412. package/skills/path/scripts/git_guard.py +116 -29
  413. package/skills/path/scripts/integration.py +434 -24
  414. package/skills/path/scripts/isolation.py +1077 -28
  415. package/skills/path/scripts/lean_verification.py +99 -19
  416. package/skills/path/scripts/loop_run.py +20 -30
  417. package/skills/path/scripts/members.py +704 -0
  418. package/skills/path/scripts/path_config.py +2 -2
  419. package/skills/path/scripts/pipeline_git.py +61 -22
  420. package/skills/path/scripts/pipeline_state.py +176 -54
  421. package/skills/path/scripts/promote_lookahead.py +1 -1
  422. package/skills/path/scripts/review_panel.py +1 -1
  423. package/skills/path/scripts/state_checkpoint.py +140 -31
  424. package/skills/path/scripts/state_promote.py +2 -0
  425. package/skills/path/scripts/status_runtime.py +36 -6
  426. package/skills/path/scripts/token_budget.py +12 -8
  427. package/skills/path/scripts/workflow_run.py +103 -29
  428. package/skills/path/scripts/worktree_paths.py +18 -8
  429. package/skills/path/templates/codebase.md +1 -1
  430. package/skills/path/templates/docs-audit.md +1 -1
  431. package/skills/path/templates/final-review.md +5 -2
  432. package/skills/path/templates/gap-review.md +2 -1
  433. package/skills/path/templates/task.md +1 -0
  434. package/skills/path/templates/wave-review.md +3 -1
@@ -19,8 +19,6 @@ import hashlib
19
19
  import json
20
20
  import os
21
21
  import re
22
- import shlex
23
- import signal
24
22
  import subprocess
25
23
  import sys
26
24
  import tempfile
@@ -170,12 +168,7 @@ def child_running(state: Dict[str, object]) -> bool:
170
168
  return state.get("finished_at") is None and bool(state.get("pid")) and process_alive(int(state["pid"]))
171
169
 
172
170
 
173
- def process_alive(pid: int) -> bool:
174
- try:
175
- os.kill(pid, 0)
176
- except ProcessLookupError:
177
- return False
178
- return True
171
+ process_alive = _common.process_alive
179
172
 
180
173
 
181
174
  # --- child process ---------------------------------------------------------
@@ -208,13 +201,13 @@ def child_main(state_path: Path) -> int:
208
201
  attempt_dir = state_path.parent
209
202
  with open(attempt_dir / "brief.md", "rb") as brief, \
210
203
  open(attempt_dir / "stdout", "wb") as out, open(attempt_dir / "stderr", "wb") as err:
211
- process = subprocess.Popen(state["command"], cwd=state["worktree"], stdin=brief,
212
- stdout=out, stderr=err, start_new_session=True)
204
+ process = _common.popen_group(_common.resolve_argv(state["command"]), cwd=state["worktree"],
205
+ stdin=brief, stdout=out, stderr=err)
213
206
  try:
214
207
  exit_code: Optional[int] = process.wait(timeout=state.get("child_timeout"))
215
208
  timed_out = False
216
209
  except subprocess.TimeoutExpired:
217
- os.killpg(process.pid, signal.SIGKILL)
210
+ _common.kill_tree(process)
218
211
  process.wait()
219
212
  exit_code, timed_out = None, True
220
213
  save_state(attempt_dir / "exit.json",
@@ -224,6 +217,7 @@ def child_main(state_path: Path) -> int:
224
217
 
225
218
  def brief_text(state: Dict[str, object], role_brief: Path, task_template: Path) -> str:
226
219
  worktree = Path(str(state["worktree"]))
220
+ project_root = Path(str(state.get("project_root") or worktree))
227
221
  task_id = state["task_id"]
228
222
  agents = worktree / "AGENTS.md"
229
223
  workflow = worktree / "WORKFLOW.md"
@@ -234,12 +228,14 @@ def brief_text(state: Dict[str, object], role_brief: Path, task_template: Path)
234
228
  f"Work only in this worktree root: {worktree}",
235
229
  f"Task file: {worktree / str(state['task_file'])}",
236
230
  f"Task template: {task_template}",
237
- f"INTENT.md: {worktree / '.project/intent/INTENT.md'}",
231
+ f"INTENT.md: {project_root / '.project/intent/INTENT.md'}",
238
232
  f"AGENTS.md: {agents if agents.exists() else 'absent'}",
239
233
  f"WORKFLOW.md: {workflow if workflow.exists() else 'absent'}",
240
234
  f"Recorded base: {state['base']}",
241
235
  f"Dispatch mode: {state['mode']} ("
242
236
  + ("the primary worktree on the bound branch" if state["mode"] == "serial"
237
+ else f"an isolated worktree of member {state['member']}; never touch the member team's "
238
+ "checkout or the coordinator" if state["mode"] == "member"
243
239
  else "an isolated task worktree; never touch the primary") + ").",
244
240
  "Next consumer: the build orchestrator reruns Verify in an isolated checkout and lands "
245
241
  "the task. Gate: Verify passes and only the task's declared files plus its Log changed.",
@@ -248,7 +244,7 @@ def brief_text(state: Dict[str, object], role_brief: Path, task_template: Path)
248
244
  if state.get("answered"):
249
245
  lines.append(f"The task Log now records an `{ANSWER_MARK}` to your earlier question; "
250
246
  "continue from it.")
251
- lines.append(task_context.render(worktree, worktree / str(state['task_file'])))
247
+ lines.append(task_context.render(project_root, worktree / str(state['task_file'])))
252
248
  return "\n".join(lines) + "\n"
253
249
 
254
250
 
@@ -282,7 +278,7 @@ def selected_command(state: Dict[str, object], options: argparse.Namespace, comm
282
278
  if caps_path is None:
283
279
  if overrides or previous or model_policy.policy_path(primary, project_dir).exists():
284
280
  raise model_policy.PolicyError('model policy requires --model-capabilities for this dispatch')
285
- return shlex.split(command), None
281
+ return _common.split_command(command), None
286
282
  caps = model_policy.read_json(caps_path)
287
283
  action = getattr(options, 'action', 'round')
288
284
  role = {'round': 'coder', 'review': 'reviewer', 'panel': 'review_panel',
@@ -297,7 +293,7 @@ def selected_command(state: Dict[str, object], options: argparse.Namespace, comm
297
293
  dict(overrides, model=state['slug']), previous)
298
294
  model_policy.validate_panel(selection, state['family'], state.get('excluded_families', []))
299
295
  state['slug'] = selection['selected']['model']
300
- return model_policy.command_args(shlex.split(command), selection), selection
296
+ return model_policy.command_args(_common.split_command(command), selection), selection
301
297
 
302
298
 
303
299
  def retain_selection(root: Path, state: Dict[str, object]) -> Dict[str, object]:
@@ -340,12 +336,12 @@ def spawn(root: Path, state: Dict[str, object], options: argparse.Namespace,
340
336
  "child_timeout": options.child_timeout, "outcome": None,
341
337
  "dispatched_at": now(), "origin": "question" if state.get("answered") else "dispatch"})
342
338
  state_path = attempt_dir / "state.json"
343
- (attempt_dir / "brief.md").write_text(brief(state), encoding="utf-8")
339
+ (attempt_dir / "brief.md").write_bytes(brief(state).encode("utf-8"))
344
340
  save_state(state_path, state)
345
- wrapper = subprocess.Popen(
341
+ wrapper = _common.popen_detached(
346
342
  [sys.executable, "-B", str(Path(__file__).resolve()), "_child", "--state", str(state_path)],
347
343
  cwd=state["worktree"], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
348
- stderr=subprocess.DEVNULL, start_new_session=True,
344
+ stderr=subprocess.DEVNULL,
349
345
  )
350
346
  state["_path"] = str(state_path)
351
347
  update_state(state, pid=wrapper.pid)
@@ -356,8 +352,11 @@ def spawn(root: Path, state: Dict[str, object], options: argparse.Namespace,
356
352
 
357
353
 
358
354
  def log_delta(primary: Path, state: Dict[str, object]) -> str:
359
- base_text = isolation.git_text(primary, "show", f"{state['base']}:{state['task_file']}")
355
+ contract_file = state.get("contract_file") or state["task_file"]
356
+ base_text = isolation.git_text(primary, "show", f"{state['base']}:{contract_file}")
360
357
  text = (Path(str(state["worktree"])) / str(state["task_file"])).read_text(encoding="utf-8")
358
+ if state["mode"] == "member":
359
+ return isolation.member_log_delta(base_text, text)
361
360
  return isolation.task_log_delta(base_text, text)
362
361
 
363
362
 
@@ -374,14 +373,14 @@ def snapshot_tree(primary: Path) -> str:
374
373
  env = dict(os.environ, GIT_INDEX_FILE=str(Path(temporary) / "index"))
375
374
  for arguments in (("read-tree", "HEAD"), ("add", "-A"), ("write-tree",)):
376
375
  completed = subprocess.run(("git", "-C", str(primary), *arguments), env=env,
377
- text=True, capture_output=True)
376
+ encoding="utf-8", errors="replace", capture_output=True)
378
377
  if completed.returncode:
379
378
  raise DriverStop(f"git {' '.join(arguments)} failed: {completed.stderr.strip()}")
380
379
  return completed.stdout.strip()
381
380
 
382
381
 
383
382
  def run_verify(command: str, cwd: Path) -> Dict[str, object]:
384
- completed = _common.run_command("bash", "-c", command, cwd=cwd)
383
+ completed = _common.run_command(*_common.bash_argv(command), cwd=cwd)
385
384
  return {"exit_code": completed.returncode, "stdout": completed.stdout, "stderr": completed.stderr}
386
385
 
387
386
 
@@ -394,6 +393,30 @@ def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
394
393
  break
395
394
  else:
396
395
  raise DriverStop(f"task {task_id} has no task file")
396
+ member = fields.get("repo")
397
+ if member:
398
+ # A member activation lives in the live copy inside its member sidecar.
399
+ try:
400
+ checkout, project, _ = isolation._member_context(primary, str(member))
401
+ except isolation.IsolationError as error:
402
+ raise DriverStop(str(error), task=task_id) from error
403
+ task_file = task_path.relative_to(primary).as_posix()
404
+ sidecar = isolation.sidecar_root(checkout, "task", f"{project}-{task_id}").resolve()
405
+ copy = isolation.member_task_copy(sidecar, task_file)
406
+ live, _ = isolation.task_frontmatter(copy.read_text(encoding="utf-8")) if copy.is_file() else ({}, None)
407
+ if not live or live.get("status") != "in-progress" or not live.get("base") or not live.get("member_base"):
408
+ raise DriverStop(f"member task {task_id} has no activated live copy in {sidecar}", task=task_id)
409
+ return {"task_id": task_id, "title": str(live.get("title")), "task_file": copy.relative_to(sidecar).as_posix(),
410
+ "contract_file": task_file, "files": list(live.get("files") or []), "base": str(live["base"]),
411
+ "worktree": str(sidecar), "task_branch": f"{isolation.TASK_BRANCH_PREFIX}{project}-{task_id}",
412
+ "mode": "member", "member": str(member), "member_base": str(live["member_base"]),
413
+ "project_root": str(primary), "sidecar": None}
414
+ if fields.get("status") != "in-progress" or not fields.get("base"):
415
+ # A parallel activation lives only in its isolate; the primary copy stays pending.
416
+ isolate = isolation.sidecar_root(primary, "task", task_id).resolve()
417
+ if isolation.authorized_task_worktree(isolate, isolation.require_bound(primary)):
418
+ fields, _ = isolation.task_frontmatter(
419
+ (isolate / task_path.relative_to(primary)).read_text(encoding="utf-8"))
397
420
  if fields.get("status") != "in-progress" or not fields.get("base"):
398
421
  raise DriverStop(f"task {task_id} is not an in-progress task with a recorded base")
399
422
  def value(key: str) -> Optional[str]:
@@ -453,7 +476,21 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
453
476
  if not passed:
454
477
  if state["mode"] == "parallel":
455
478
  isolation.deactivate_task(worktree, task_id, str(state["task_branch"]))
479
+ elif state["mode"] == "member":
480
+ isolation.deactivate_member_task(primary, str(state["member"]), task_id)
456
481
  raise DriverStop("Verify failed in the isolate", task=task_id, execution=execution)
482
+ if state["mode"] == "member":
483
+ member = str(state["member"])
484
+ landed = isolation.land_member(primary, member, task_id, str(state["title"]),
485
+ str(state["contract_file"]), base, str(state["member_base"]))
486
+ landing = str(landed["landing"])
487
+ # Only a landing whose parent is the member base proves the verified member tree.
488
+ ledger = isolation.git_output(worktree, "rev-parse", f"{landing}^") == str(state["member_base"])
489
+ if ledger:
490
+ build_state.verify_record(str(primary), command, landing, "pass", execution=execution, member=member)
491
+ isolation.retire_member_task(primary, member, task_id)
492
+ return {"task": task_id, "commit": str(landed["commit"]), "mode": "member",
493
+ "landing": landing, "verify": execution, "ledger": ledger}
457
494
  landed = isolation.land(primary, worktree, base, task_id, str(state["title"]), task_file,
458
495
  list(state["files"]))
459
496
  commit = str(landed["commit"])
@@ -485,7 +522,8 @@ def recover_report(primary: Path, project_dir: str, receipt: Dict[str, object])
485
522
 
486
523
  def checkpoint_project(primary: Path, receipt: Dict[str, object], subject: str, body: str) -> Optional[str]:
487
524
  """Commit .project bookkeeping through the canonical checkpoint; None when nothing is dirty."""
488
- if not any(path.startswith(".project/") for path in isolation.uncommitted_paths(primary)):
525
+ if not any(path.startswith(".project/") for path in isolation.uncommitted_paths(primary)) \
526
+ and not isolation.ignored_legacy_ledger(primary):
489
527
  return None
490
528
  result = isolation.checkpoint(primary, isolation.current_sha(primary), subject, body, [".project"])
491
529
  receipt["steps"].append({"script": "isolation.py checkpoint", "result": result})
@@ -582,7 +620,7 @@ class Round:
582
620
  """Run the bundled token_budget.py against this milestone's ledger; blocked is a stop."""
583
621
  command = [sys.executable, "-B", str(Path(__file__).resolve().parent / "token_budget.py"),
584
622
  action, "--ledger", str(budget_ledger(self.primary)), *arguments]
585
- completed = subprocess.run(command, cwd=self.primary, capture_output=True, text=True)
623
+ completed = subprocess.run(command, cwd=self.primary, capture_output=True, encoding="utf-8", errors="replace")
586
624
  result = json.loads(completed.stdout) if completed.stdout.strip() else {}
587
625
  self.receipt["steps"].append({"script": f"token_budget.py {action}", "command": command,
588
626
  "exit_code": completed.returncode, "result": result,
@@ -605,12 +643,24 @@ class Round:
605
643
  # recovery -------------------------------------------------------------
606
644
 
607
645
  def recover(self) -> None:
646
+ # A journaled member landing is safe to finish; it never lands the same work twice.
647
+ journals = isolation.common_git_dir(self.primary).joinpath(*isolation.MEMBER_LANDING_DIR)
648
+ retired_members = set()
649
+ for journal in sorted(journals.glob("*.json")) if journals.is_dir() else []:
650
+ result = isolation.recover_member_landing(self.primary, journal.stem)
651
+ self.receipt["steps"].append({"script": "isolation.recover_member_landing", "task": journal.stem,
652
+ "result": result})
653
+ if result["state"] == "landed":
654
+ isolation.retire_member_task(self.primary, str(result["member"]), journal.stem)
655
+ retired_members.add(journal.stem)
608
656
  report = recover_report(self.primary, self.project_dir, self.receipt)
609
657
  for task in report["tasks"]:
610
658
  worktree = task.get("worktree")
611
659
  verdict = task["verdict"]
612
660
  if verdict in ("recovered", "attested"):
613
661
  self.proven[str(task.get("task_id"))] = str(task.get("commit"))
662
+ if verdict == "recovered" and task.get("member") and task.get("task_id") not in retired_members:
663
+ isolation.retire_member_task(self.primary, str(task["member"]), str(task["task_id"]))
614
664
  if verdict == "recovered" and worktree and worktree["clean"] and task.get("task_branch"):
615
665
  isolation.retire(self.primary, Path(str(worktree["path"])),
616
666
  str(task["task_branch"]), False)
@@ -730,6 +780,8 @@ class Round:
730
780
  if state.get("wave") != self.receipt["wave"]:
731
781
  raise DriverStop("task is outside the requested wave", task=state["task_id"],
732
782
  wave=state.get("wave"), requested_wave=self.receipt["wave"])
783
+ if state.get("mode") == "member":
784
+ isolation.ensure_member_hooks(self.primary, str(state["member"]), str(state["task_id"]))
733
785
  fresh = spawn(self.root, state, self.options,
734
786
  lambda fresh: brief_text(fresh, self.options.role_brief, self.options.task_template))
735
787
  self.receipt["dispatched"].append(self.summary(fresh))
@@ -805,6 +857,28 @@ class Round:
805
857
  self.runner("lint-round", head)
806
858
  round_size = len(selected) + len(in_flight)
807
859
  for task in selected:
860
+ if task.get("repo"):
861
+ member = str(task["repo"])
862
+ retained = isolation.retained_member_task(self.primary, member, task["id"],
863
+ task["task_file"], head)
864
+ if retained and retained["state"] == "unused":
865
+ isolation.retire_member_task(self.primary, member, task["id"])
866
+ retained = None
867
+ if retained:
868
+ isolate, active = retained["isolate"], retained["active"]
869
+ else:
870
+ isolate = isolation.isolate_member_task(self.primary, member, task["id"])
871
+ active = isolation.activate_member_task(self.primary, member, task["id"],
872
+ f"build_{task['id'].lower()}", task["task_file"], head)
873
+ sidecar_path = Path(str(active["worktree"]))
874
+ self.launch({"task_id": task["id"], "title": task["title"],
875
+ "task_file": Path(str(active["copy"])).relative_to(sidecar_path).as_posix(),
876
+ "contract_file": task["task_file"], "files": task["files"], "wave": task["wave"],
877
+ "verify_heavy": task["verify_heavy"], "base": head, "worktree": str(sidecar_path),
878
+ "task_branch": isolate["task_branch"], "mode": "member", "member": member,
879
+ "member_base": isolate["member_base"], "project_root": str(self.primary),
880
+ "sidecar": None, "answered": False, "model_selection": task['model_selection']})
881
+ continue
808
882
  prepared = self.runner("prepare-task", head, task["id"], round_size)
809
883
  isolate = prepared[0]["result"]
810
884
  sidecar = prepared[1]["result"] if len(prepared) > 1 else None
@@ -1178,7 +1252,7 @@ class Review:
1178
1252
  def helper_json(receipt: Dict[str, object], script: str, *arguments: str, cwd: Path) -> Dict[str, object]:
1179
1253
  """Run a bundled helper and return its JSON stdout; a non-zero exit is a typed stop."""
1180
1254
  command = [sys.executable, "-B", str(Path(__file__).resolve().parent / script), *arguments]
1181
- completed = subprocess.run(command, cwd=cwd, capture_output=True, text=True)
1255
+ completed = subprocess.run(command, cwd=cwd, capture_output=True, encoding="utf-8", errors="replace")
1182
1256
  step = {"script": script, "command": command, "exit_code": completed.returncode,
1183
1257
  "stdout": completed.stdout, "stderr": completed.stderr}
1184
1258
  receipt["steps"].append(step)
@@ -1746,7 +1820,7 @@ Heavy: {'yes' if heavy else 'no'}
1746
1820
  receipt["plan_wave"] = fix_wave
1747
1821
  lint = subprocess.run([sys.executable, "-B", str(Path(__file__).resolve().parent / "check_task_briefs.py"),
1748
1822
  "--repo", str(primary), "--base", isolation.current_sha(primary),
1749
- "--tasks-dir", f"{options.project_dir}/tasks"], capture_output=True, text=True)
1823
+ "--tasks-dir", f"{options.project_dir}/tasks"], capture_output=True, encoding="utf-8", errors="replace")
1750
1824
  receipt["steps"].append({"script": "check_task_briefs.py", "exit_code": lint.returncode,
1751
1825
  "stdout": lint.stdout, "stderr": lint.stderr})
1752
1826
  if lint.returncode:
@@ -1981,6 +2055,17 @@ def main(argv=None) -> int:
1981
2055
  current.recover()
1982
2056
  records = [state for state in latest_states(current.root)
1983
2057
  if state["task_id"] == arguments.task_id]
2058
+ native = None
2059
+ if records and records[0].get("outcome") is not None:
2060
+ # A settled driver record at an older base is superseded by a native retry.
2061
+ try:
2062
+ native = state_from_task(primary, arguments.task_id)
2063
+ except DriverStop:
2064
+ pass
2065
+ if native and native["base"] != records[0].get("base"):
2066
+ records = []
2067
+ else:
2068
+ native = None
1984
2069
  if arguments.task_id in current.proven:
1985
2070
  commit = current.proven[arguments.task_id]
1986
2071
  if records and records[0].get("outcome") is None:
@@ -1997,7 +2082,7 @@ def main(argv=None) -> int:
1997
2082
  current.classify(record)
1998
2083
  else: # a coder dispatched by hand: derive the isolate from the task frontmatter
1999
2084
  current.receipt["landed"].append(
2000
- finish_task(primary, state_from_task(primary, arguments.task_id)))
2085
+ finish_task(primary, native or state_from_task(primary, arguments.task_id)))
2001
2086
  result = {**current.receipt, "status": "landed" if current.receipt["landed"] else "blocked"}
2002
2087
  elif arguments.action == "answer":
2003
2088
  result = answer(primary, arguments.task_id, arguments.answer)
@@ -6,6 +6,7 @@ hook wiring. Inspection failures and violations both exit 1: unreadable Git
6
6
  state cannot prove archive immutability or safe publication.
7
7
  """
8
8
 
9
+ import json
9
10
  import re
10
11
  import subprocess
11
12
  from functools import lru_cache
@@ -19,8 +20,9 @@ sys.path.insert(0, str(_HERE / "runtime" if (_HERE / "runtime" / "isolation.py")
19
20
  sys.dont_write_bytecode = True # a hook must not leave __pycache__ in the worktree
20
21
  try:
21
22
  from isolation import BOOKKEEPING_PREFIXES, NULL_SHA, _landing_state, task_frontmatter
22
- from pipeline_git import is_ship_subject, task_commit_body
23
+ from pipeline_git import is_ship_subject, ship_commit_body, task_commit_body
23
24
  from pipeline_state import _completion_status
25
+ import members
24
26
  except ImportError as error: # pragma: no cover - broken install
25
27
  print(
26
28
  f"gsd-path guard: pipeline runtime is missing ({error}); commit blocked; "
@@ -57,6 +59,10 @@ PRODUCT_HINT = (
57
59
  f"while this worktree carries unfinished milestone state, {LANDING_HINT}; "
58
60
  "findings during ship reopen through the patch plan"
59
61
  )
62
+ MEMBER_PUSH_HINT = (
63
+ "a member publishes its coordinator's Path refs, or work from them, only with "
64
+ "a coordinator authorization for that exact ref and object"
65
+ )
60
66
  PUBLICATION_HINT = (
61
67
  "unshipped milestone work reaches a remote only as its ship commit "
62
68
  "(STATE shipped/done) through archive_milestone.py integrate; findings during "
@@ -84,7 +90,7 @@ def staged_entries():
84
90
  output = subprocess.run(
85
91
  ["git", "diff", "--cached", "--name-status", "-z", "--find-renames"],
86
92
  capture_output=True,
87
- text=True,
93
+ encoding="utf-8", errors="replace",
88
94
  check=True,
89
95
  ).stdout
90
96
  tokens = output.split("\0")
@@ -139,7 +145,7 @@ def committed_archive_roots():
139
145
  head = subprocess.run(
140
146
  ["git", "rev-parse", "--verify", "--quiet", "HEAD^{commit}"],
141
147
  capture_output=True,
142
- text=True,
148
+ encoding="utf-8", errors="replace",
143
149
  check=False,
144
150
  )
145
151
  if head.returncode == 1:
@@ -148,7 +154,7 @@ def committed_archive_roots():
148
154
  output = subprocess.run(
149
155
  ["git", "ls-tree", "-r", "--name-only", "HEAD", "--", ARCHIVE_PREFIX],
150
156
  capture_output=True,
151
- text=True,
157
+ encoding="utf-8", errors="replace",
152
158
  check=True,
153
159
  ).stdout
154
160
  return {
@@ -162,7 +168,7 @@ def staged_file(path):
162
168
  return subprocess.run(
163
169
  ["git", "show", f":{path}"],
164
170
  capture_output=True,
165
- text=True,
171
+ encoding="utf-8", errors="replace",
166
172
  check=True,
167
173
  ).stdout
168
174
 
@@ -176,7 +182,7 @@ def shown_file(revspec):
176
182
  shown = subprocess.run(
177
183
  ["git", "show", revspec],
178
184
  capture_output=True,
179
- text=True,
185
+ encoding="utf-8", errors="replace",
180
186
  check=False,
181
187
  )
182
188
  return shown.stdout if shown.returncode == 0 else None
@@ -187,7 +193,7 @@ def state_at(revision):
187
193
  subprocess.run(
188
194
  ["git", "cat-file", "-e", revision],
189
195
  capture_output=True,
190
- text=True,
196
+ encoding="utf-8", errors="replace",
191
197
  check=True,
192
198
  )
193
199
  content = shown_file(f"{revision}:.project/STATE.md")
@@ -288,12 +294,25 @@ def ship_contract_violations(entries, subject, body, new_archives, state):
288
294
  reviewed_head = subprocess.run(
289
295
  ["git", "rev-parse", "--verify", "HEAD"],
290
296
  capture_output=True,
291
- text=True,
297
+ encoding="utf-8", errors="replace",
292
298
  check=True,
293
299
  ).stdout.strip()
294
- expected_body = f"Archive: {archive}\nReviewed-HEAD: {reviewed_head}"
300
+ lock = f"{archive}/build/members.json"
301
+ staged_lock = subprocess.run(
302
+ ["git", "ls-files", "--cached", "--", lock],
303
+ capture_output=True,
304
+ encoding="utf-8", errors="replace",
305
+ check=True,
306
+ ).stdout
307
+ locked = [entry["name"] for entry in json.loads(staged_file(lock))["members"]] if staged_lock else []
308
+ rows = members.read_member_close(
309
+ Path(git_output("rev-parse", "--path-format=absolute", "--git-common-dir")), Path(archive).name
310
+ ) if locked else []
311
+ if [row["name"] for row in rows] != locked or any(row["status"] != "integrated" for row in rows):
312
+ found.append("ship commit requires every locked member closed; run archive_milestone.py close-members")
313
+ expected_body = ship_commit_body(archive, reviewed_head, rows).rstrip("\n")
295
314
  if body != expected_body:
296
- found.append("ship commit body does not match Archive and Reviewed-HEAD")
315
+ found.append("ship commit body does not match Archive, Reviewed-HEAD, and closed members")
297
316
  changed_paths = {
298
317
  new if code in "CR" else old
299
318
  for code, old, new in entries
@@ -383,11 +402,15 @@ def staged_abandon_target(new_archives, state):
383
402
 
384
403
 
385
404
  @lru_cache(maxsize=None)
405
+ def git_output(*arguments):
406
+ return subprocess.run(["git", *arguments], capture_output=True, encoding="utf-8", errors="replace", check=True).stdout.strip()
407
+
408
+
386
409
  def current_branch():
387
410
  return subprocess.run(
388
411
  ["git", "branch", "--show-current"],
389
412
  capture_output=True,
390
- text=True,
413
+ encoding="utf-8", errors="replace",
391
414
  check=True,
392
415
  ).stdout.strip()
393
416
 
@@ -398,7 +421,7 @@ def closed_milestone_reason():
398
421
  return None
399
422
  commits = subprocess.run(
400
423
  ["git", "log", "--format=%H", "--grep=^ship:", "HEAD"],
401
- capture_output=True, text=True, check=True,
424
+ capture_output=True, encoding="utf-8", errors="replace", check=True,
402
425
  ).stdout.splitlines()
403
426
  if any(ship_commit_at(sha, branch) for sha in commits):
404
427
  return (f"closed milestone {branch} accepts no new work; finish integration "
@@ -411,7 +434,7 @@ def head_descends_from(commit):
411
434
  subprocess.run(
412
435
  ["git", "merge-base", "--is-ancestor", commit, "HEAD"],
413
436
  capture_output=True,
414
- text=True,
437
+ encoding="utf-8", errors="replace",
415
438
  check=False,
416
439
  ).returncode
417
440
  == 0
@@ -429,7 +452,7 @@ def repo_root():
429
452
  subprocess.run(
430
453
  ["git", "rev-parse", "--show-toplevel"],
431
454
  capture_output=True,
432
- text=True,
455
+ encoding="utf-8", errors="replace",
433
456
  check=True,
434
457
  ).stdout.strip()
435
458
  )
@@ -505,7 +528,7 @@ def ship_commit_at(sha, bound):
505
528
  shown = subprocess.run(
506
529
  ["git", "show", "--no-patch", "--format=%s", sha],
507
530
  capture_output=True,
508
- text=True,
531
+ encoding="utf-8", errors="replace",
509
532
  check=True,
510
533
  )
511
534
  archive = state.get("archive") or ""
@@ -514,13 +537,7 @@ def ship_commit_at(sha, bound):
514
537
  )
515
538
 
516
539
 
517
- def pre_push_violations(lines):
518
- """Refuse ref updates that publish unshipped milestone work.
519
-
520
- A bound-named ref moves only to, or away from, its strict ship commit. Any
521
- other ref may not carry a commit whose STATE.md still owes a ship commit.
522
- """
523
- found = []
540
+ def _push_updates(lines):
524
541
  for line in lines:
525
542
  parts = line.split()
526
543
  if len(parts) != 4:
@@ -528,6 +545,60 @@ def pre_push_violations(lines):
528
545
  local_ref, local_sha, remote_ref, remote_sha = parts
529
546
  if any(re.fullmatch(r"[0-9a-fA-F]{40}", sha) is None for sha in (local_sha, remote_sha)):
530
547
  raise ValueError(f"unexpected pre-push SHA: {line!r}")
548
+ yield local_ref, local_sha, remote_ref, remote_sha
549
+
550
+
551
+ def _commits(*arguments):
552
+ listed = subprocess.run(
553
+ ["git", "rev-list", *arguments], capture_output=True, encoding="utf-8", errors="replace", check=True
554
+ )
555
+ return set(listed.stdout.split())
556
+
557
+
558
+ def member_pre_push_violations(lines, role):
559
+ """Member mode: the member's own STATE is ignored; coordinator refs need authorization."""
560
+ checkout, project = repo_root(), role["project"]
561
+ reserved = members.member_ref_prefixes(project)
562
+ members.require_origin_main(checkout)
563
+ exclude = ["--not", "refs/remotes/origin/main"]
564
+ member_work = None
565
+ found = []
566
+ for _, local_sha, remote_ref, remote_sha in _push_updates(lines):
567
+ if local_sha == NULL_SHA:
568
+ if remote_ref.startswith(reserved) and not members.authorized(
569
+ checkout, project, "delete", remote_ref, remote_sha
570
+ ):
571
+ found.append(f"{remote_ref} deletion is not authorized by {project}; {MEMBER_PUSH_HINT}")
572
+ continue
573
+ if members.authorized(checkout, project, "push", remote_ref, local_sha):
574
+ continue
575
+ if remote_ref.startswith(reserved):
576
+ found.append(
577
+ f"{remote_ref} <- {local_sha[:12]} is not authorized by {project}; {MEMBER_PUSH_HINT}"
578
+ )
579
+ continue
580
+ if member_work is None:
581
+ bound = [ref for ref in subprocess.run(
582
+ ["git", "for-each-ref", "--format=%(refname)", "refs/heads/gsd-path/"],
583
+ capture_output=True, encoding="utf-8", errors="replace", check=True,
584
+ ).stdout.split() if ref.startswith(f"refs/heads/gsd-path/{project}-")]
585
+ member_work = _commits(*bound, *exclude) if bound else set()
586
+ if member_work & _commits(local_sha, *exclude):
587
+ found.append(
588
+ f"{remote_ref} <- {local_sha[:12]} carries unpublished {project} member work; "
589
+ f"{MEMBER_PUSH_HINT}"
590
+ )
591
+ return found
592
+
593
+
594
+ def pre_push_violations(lines):
595
+ """Refuse ref updates that publish unshipped milestone work.
596
+
597
+ A bound-named ref moves only to, or away from, its strict ship commit. Any
598
+ other ref may not carry a commit whose STATE.md still owes a ship commit.
599
+ """
600
+ found = []
601
+ for local_ref, local_sha, remote_ref, remote_sha in _push_updates(lines):
531
602
  names = [ref.removeprefix("refs/heads/") for ref in (local_ref, remote_ref)]
532
603
  bounds = {name for name in names if BOUND_BRANCH.fullmatch(name)}
533
604
  if bounds:
@@ -574,7 +645,7 @@ def default_branch():
574
645
  origin_head = subprocess.run(
575
646
  ["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
576
647
  capture_output=True,
577
- text=True,
648
+ encoding="utf-8", errors="replace",
578
649
  check=False,
579
650
  )
580
651
  if origin_head.returncode == 0 and origin_head.stdout.strip().startswith("origin/"):
@@ -582,7 +653,7 @@ def default_branch():
582
653
  configured = subprocess.run(
583
654
  ["git", "config", "--get", "init.defaultBranch"],
584
655
  capture_output=True,
585
- text=True,
656
+ encoding="utf-8", errors="replace",
586
657
  check=False,
587
658
  )
588
659
  if configured.returncode == 0 and configured.stdout.strip():
@@ -604,7 +675,7 @@ def is_integration_merge(subject):
604
675
  merge_head = subprocess.run(
605
676
  ["git", "rev-parse", "--verify", "--quiet", "MERGE_HEAD^{commit}"],
606
677
  capture_output=True,
607
- text=True,
678
+ encoding="utf-8", errors="replace",
608
679
  check=False,
609
680
  )
610
681
  bound_head = subprocess.run(
@@ -616,7 +687,7 @@ def is_integration_merge(subject):
616
687
  f"refs/heads/{match.group('branch')}^{{commit}}",
617
688
  ],
618
689
  capture_output=True,
619
- text=True,
690
+ encoding="utf-8", errors="replace",
620
691
  check=False,
621
692
  )
622
693
  return (
@@ -708,18 +779,34 @@ def report(found, action):
708
779
 
709
780
 
710
781
  def main(argv):
782
+ # Git reads hook output as UTF-8; a Windows pipe would default to cp1252.
783
+ # In-process callers may substitute text streams.
784
+ for stream in (sys.stdout, sys.stderr):
785
+ if hasattr(stream, "reconfigure"):
786
+ stream.reconfigure(encoding="utf-8", errors="replace")
711
787
  if len(argv) > 1 and argv[1] == "closed-milestone":
712
788
  reason = closed_milestone_reason()
713
789
  return report([reason] if reason else [], "mutation")
714
790
  if len(argv) > 1 and argv[1] == "pre-push":
715
791
  try:
716
- found = pre_push_violations(sys.stdin.read().splitlines())
792
+ stdin = getattr(sys.stdin, "buffer", None)
793
+ text = (stdin.read().decode("utf-8", errors="surrogateescape")
794
+ if stdin is not None else sys.stdin.read())
795
+ lines = text.splitlines()
796
+ role = members.member_role(repo_root())
797
+ found = member_pre_push_violations(lines, role) if role else pre_push_violations(lines)
717
798
  except Exception as error:
718
799
  print(f"gsd-path guard: inspection failed; push blocked ({error})", file=sys.stderr)
719
800
  return 1
720
801
  return report(found, "push")
721
802
  try:
722
- reason = closed_milestone_reason()
803
+ # A member follows its coordinator, so its own STATE does not gate commits.
804
+ role = members.member_role(repo_root())
805
+ member = role is not None
806
+ if member and current_branch().startswith(f"gsd-path/{role['project']}-"):
807
+ return report([f"direct commits on the member bound branch {current_branch()} are refused; "
808
+ "member tasks land there through Path"], "commit")
809
+ reason = None if member else closed_milestone_reason()
723
810
  if reason:
724
811
  return report([reason], "commit")
725
812
  entries = staged_entries()
@@ -740,7 +827,7 @@ def main(argv):
740
827
  )
741
828
  if abandon is not None:
742
829
  current_archive = abandon[0]
743
- landing = product_commit_violations(entries, subject, body)
830
+ landing = [] if member else product_commit_violations(entries, subject, body)
744
831
  except Exception as error:
745
832
  print(
746
833
  f"gsd-path guard: inspection failed; commit blocked ({error}); "