@opengsd/gsd-path 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. package/AGENTS.md +2 -207
  2. package/CHANGELOG.md +331 -69
  3. package/DOCS.md +25 -9
  4. package/FULL.md +12 -5
  5. package/GUIDE.md +1 -1
  6. package/HOOKS.md +104 -14
  7. package/QUICK.md +5 -2
  8. package/README.md +131 -111
  9. package/RUNTIME.md +2 -2
  10. package/UPDATE.md +61 -11
  11. package/WORKFLOW.md +44 -7
  12. package/package.json +13 -9
  13. package/platforms/shared-agents/dispatch.md +13 -0
  14. package/scripts/_common.py +619 -18
  15. package/scripts/archive_milestone.py +160 -86
  16. package/scripts/bootstrap_repository.py +8 -5
  17. package/scripts/build_recovery.py +100 -32
  18. package/scripts/build_state.py +143 -24
  19. package/scripts/check_docs_audit.py +91 -7
  20. package/scripts/check_handoffs.py +662 -50
  21. package/scripts/check_task_briefs.py +338 -30
  22. package/scripts/check_update.py +2 -2
  23. package/scripts/core_hook_gate.py +10 -1
  24. package/scripts/core_hook_settings.py +11 -4
  25. package/scripts/detect_project.py +587 -32
  26. package/scripts/discussion_records.py +19 -13
  27. package/scripts/discussion_validate.py +152 -50
  28. package/scripts/dispatch_driver.py +501 -46
  29. package/scripts/git_guard.py +118 -31
  30. package/scripts/guard_hook.py +1567 -194
  31. package/scripts/install.mjs +77 -46
  32. package/scripts/install.py +611 -50
  33. package/scripts/integration.py +580 -25
  34. package/scripts/isolation.py +1438 -57
  35. package/scripts/lean_verification.py +99 -19
  36. package/scripts/loop_run.py +31 -29
  37. package/scripts/members.py +706 -0
  38. package/scripts/migrate_core.py +2 -2
  39. package/scripts/path_config.py +5 -5
  40. package/scripts/pipeline_diagnose.py +82 -31
  41. package/scripts/pipeline_git.py +124 -34
  42. package/scripts/pipeline_state.py +287 -87
  43. package/scripts/pipeline_undo.py +218 -30
  44. package/scripts/promote_lookahead.py +1 -1
  45. package/scripts/review_panel.py +1 -1
  46. package/scripts/runtime_store.py +55 -11
  47. package/scripts/skill-resources.json +37 -5
  48. package/scripts/state_checkpoint.py +135 -44
  49. package/scripts/state_promote.py +2 -0
  50. package/scripts/status_runtime.py +58 -10
  51. package/scripts/sync_skill_resources.py +8 -7
  52. package/scripts/task_context.py +12 -3
  53. package/scripts/token_budget.py +12 -8
  54. package/scripts/wizard.mjs +1 -0
  55. package/scripts/workflow_run.py +107 -30
  56. package/scripts/worktree_paths.py +72 -10
  57. package/skills/gsd-path/BUILD.md +7 -1
  58. package/skills/gsd-path/DECIDE.md +11 -4
  59. package/skills/gsd-path/DEFINE.md +36 -4
  60. package/skills/gsd-path/DOCS-AUDIT.md +6 -4
  61. package/skills/gsd-path/INSPECT.md +5 -4
  62. package/skills/gsd-path/PLAN.md +18 -4
  63. package/skills/gsd-path/RESEARCH.md +13 -1
  64. package/skills/gsd-path/ROADMAP.md +2 -1
  65. package/skills/gsd-path/SHIP.md +33 -11
  66. package/skills/gsd-path/SKILL.md +45 -1
  67. package/skills/gsd-path/references/build-native.md +63 -15
  68. package/skills/gsd-path/references/codebase-mapper.md +2 -1
  69. package/skills/gsd-path/references/dispatch.md +13 -0
  70. package/skills/gsd-path/references/docs-auditor.md +7 -1
  71. package/skills/gsd-path/references/operating-rules.md +214 -0
  72. package/skills/gsd-path/references/planner.md +22 -4
  73. package/skills/gsd-path/references/reviewer-final.md +20 -2
  74. package/skills/gsd-path/references/reviewer-wave.md +18 -5
  75. package/skills/gsd-path/references/reviewer.md +3 -1
  76. package/skills/gsd-path/references/spec-probes.md +150 -0
  77. package/skills/gsd-path/scripts/_common.py +619 -18
  78. package/skills/gsd-path/scripts/archive_milestone.py +160 -86
  79. package/skills/gsd-path/scripts/bootstrap_repository.py +8 -5
  80. package/skills/gsd-path/scripts/build_recovery.py +100 -32
  81. package/skills/gsd-path/scripts/build_state.py +143 -24
  82. package/skills/gsd-path/scripts/check_docs_audit.py +91 -7
  83. package/skills/gsd-path/scripts/check_handoffs.py +662 -50
  84. package/skills/gsd-path/scripts/check_task_briefs.py +338 -30
  85. package/skills/gsd-path/scripts/check_update.py +2 -2
  86. package/skills/gsd-path/scripts/detect_project.py +587 -32
  87. package/skills/gsd-path/scripts/discussion_records.py +19 -13
  88. package/skills/gsd-path/scripts/discussion_validate.py +152 -50
  89. package/skills/gsd-path/scripts/dispatch_driver.py +501 -46
  90. package/skills/gsd-path/scripts/git_guard.py +118 -31
  91. package/skills/gsd-path/scripts/integration.py +580 -25
  92. package/skills/gsd-path/scripts/isolation.py +1438 -57
  93. package/skills/gsd-path/scripts/lean_verification.py +99 -19
  94. package/skills/gsd-path/scripts/loop_run.py +31 -29
  95. package/skills/gsd-path/scripts/members.py +706 -0
  96. package/skills/gsd-path/scripts/path_config.py +5 -5
  97. package/skills/gsd-path/scripts/pipeline_git.py +124 -34
  98. package/skills/gsd-path/scripts/pipeline_state.py +287 -87
  99. package/skills/gsd-path/scripts/promote_lookahead.py +1 -1
  100. package/skills/gsd-path/scripts/review_panel.py +1 -1
  101. package/skills/gsd-path/scripts/state_checkpoint.py +135 -44
  102. package/skills/gsd-path/scripts/state_promote.py +2 -0
  103. package/skills/gsd-path/scripts/status_runtime.py +58 -10
  104. package/skills/gsd-path/scripts/task_context.py +12 -3
  105. package/skills/gsd-path/scripts/token_budget.py +12 -8
  106. package/skills/gsd-path/scripts/workflow_run.py +107 -30
  107. package/skills/gsd-path/scripts/worktree_paths.py +72 -10
  108. package/skills/gsd-path/templates/codebase.md +1 -1
  109. package/skills/gsd-path/templates/docs-audit.md +6 -2
  110. package/skills/gsd-path/templates/final-review.md +16 -4
  111. package/skills/gsd-path/templates/gap-review.md +2 -1
  112. package/skills/gsd-path/templates/intent.md +30 -0
  113. package/skills/gsd-path/templates/patch-findings.md +3 -3
  114. package/skills/gsd-path/templates/plan.md +17 -2
  115. package/skills/gsd-path/templates/research-handoff.md +14 -3
  116. package/skills/gsd-path/templates/state.md +1 -1
  117. package/skills/gsd-path/templates/task.md +1 -0
  118. package/skills/gsd-path/templates/wave-review.md +16 -4
  119. package/skills/gsd-path-build/SKILL.md +7 -1
  120. package/skills/gsd-path-build/references/build-native.md +63 -15
  121. package/skills/gsd-path-build/references/dispatch.md +13 -0
  122. package/skills/gsd-path-build/references/operating-rules.md +214 -0
  123. package/skills/gsd-path-build/references/reviewer-final.md +20 -2
  124. package/skills/gsd-path-build/references/reviewer-wave.md +18 -5
  125. package/skills/gsd-path-build/references/reviewer.md +3 -1
  126. package/skills/gsd-path-build/references/spec-probes.md +150 -0
  127. package/skills/gsd-path-build/scripts/_common.py +619 -18
  128. package/skills/gsd-path-build/scripts/archive_milestone.py +160 -86
  129. package/skills/gsd-path-build/scripts/build_recovery.py +100 -32
  130. package/skills/gsd-path-build/scripts/build_state.py +143 -24
  131. package/skills/gsd-path-build/scripts/check_handoffs.py +662 -50
  132. package/skills/gsd-path-build/scripts/check_task_briefs.py +338 -30
  133. package/skills/gsd-path-build/scripts/detect_project.py +587 -32
  134. package/skills/gsd-path-build/scripts/discussion_records.py +19 -13
  135. package/skills/gsd-path-build/scripts/discussion_validate.py +152 -50
  136. package/skills/gsd-path-build/scripts/dispatch_driver.py +501 -46
  137. package/skills/gsd-path-build/scripts/integration.py +580 -25
  138. package/skills/gsd-path-build/scripts/isolation.py +1438 -57
  139. package/skills/gsd-path-build/scripts/loop_run.py +31 -29
  140. package/skills/gsd-path-build/scripts/members.py +706 -0
  141. package/skills/gsd-path-build/scripts/path_config.py +5 -5
  142. package/skills/gsd-path-build/scripts/pipeline_git.py +124 -34
  143. package/skills/gsd-path-build/scripts/pipeline_state.py +287 -87
  144. package/skills/gsd-path-build/scripts/promote_lookahead.py +1 -1
  145. package/skills/gsd-path-build/scripts/review_panel.py +1 -1
  146. package/skills/gsd-path-build/scripts/state_checkpoint.py +135 -44
  147. package/skills/gsd-path-build/scripts/state_promote.py +2 -0
  148. package/skills/gsd-path-build/scripts/task_context.py +12 -3
  149. package/skills/gsd-path-build/scripts/token_budget.py +12 -8
  150. package/skills/gsd-path-build/scripts/workflow_run.py +107 -30
  151. package/skills/gsd-path-build/scripts/worktree_paths.py +72 -10
  152. package/skills/gsd-path-build/templates/task.md +1 -0
  153. package/skills/gsd-path-build/templates/wave-review.md +16 -4
  154. package/skills/gsd-path-decide/SKILL.md +11 -4
  155. package/skills/gsd-path-decide/references/dispatch.md +13 -0
  156. package/skills/gsd-path-decide/references/operating-rules.md +214 -0
  157. package/skills/gsd-path-decide/scripts/_common.py +619 -18
  158. package/skills/gsd-path-decide/scripts/archive_milestone.py +160 -86
  159. package/skills/gsd-path-decide/scripts/build_recovery.py +100 -32
  160. package/skills/gsd-path-decide/scripts/check_handoffs.py +662 -50
  161. package/skills/gsd-path-decide/scripts/check_task_briefs.py +338 -30
  162. package/skills/gsd-path-decide/scripts/detect_project.py +587 -32
  163. package/skills/gsd-path-decide/scripts/discussion_records.py +19 -13
  164. package/skills/gsd-path-decide/scripts/discussion_validate.py +152 -50
  165. package/skills/gsd-path-decide/scripts/integration.py +580 -25
  166. package/skills/gsd-path-decide/scripts/isolation.py +1438 -57
  167. package/skills/gsd-path-decide/scripts/loop_run.py +31 -29
  168. package/skills/gsd-path-decide/scripts/members.py +706 -0
  169. package/skills/gsd-path-decide/scripts/path_config.py +5 -5
  170. package/skills/gsd-path-decide/scripts/pipeline_git.py +124 -34
  171. package/skills/gsd-path-decide/scripts/pipeline_state.py +287 -87
  172. package/skills/gsd-path-decide/scripts/promote_lookahead.py +1 -1
  173. package/skills/gsd-path-decide/scripts/review_panel.py +1 -1
  174. package/skills/gsd-path-decide/scripts/state_checkpoint.py +135 -44
  175. package/skills/gsd-path-decide/scripts/state_promote.py +2 -0
  176. package/skills/gsd-path-decide/scripts/worktree_paths.py +72 -10
  177. package/skills/gsd-path-define/SKILL.md +36 -4
  178. package/skills/gsd-path-define/references/operating-rules.md +214 -0
  179. package/skills/gsd-path-define/references/spec-probes.md +150 -0
  180. package/skills/gsd-path-define/scripts/_common.py +619 -18
  181. package/skills/gsd-path-define/scripts/archive_milestone.py +160 -86
  182. package/skills/gsd-path-define/scripts/build_recovery.py +100 -32
  183. package/skills/gsd-path-define/scripts/check_handoffs.py +662 -50
  184. package/skills/gsd-path-define/scripts/check_task_briefs.py +338 -30
  185. package/skills/gsd-path-define/scripts/detect_project.py +587 -32
  186. package/skills/gsd-path-define/scripts/discussion_records.py +19 -13
  187. package/skills/gsd-path-define/scripts/discussion_validate.py +152 -50
  188. package/skills/gsd-path-define/scripts/integration.py +580 -25
  189. package/skills/gsd-path-define/scripts/isolation.py +1438 -57
  190. package/skills/gsd-path-define/scripts/loop_run.py +31 -29
  191. package/skills/gsd-path-define/scripts/members.py +706 -0
  192. package/skills/gsd-path-define/scripts/path_config.py +5 -5
  193. package/skills/gsd-path-define/scripts/pipeline_git.py +124 -34
  194. package/skills/gsd-path-define/scripts/pipeline_state.py +287 -87
  195. package/skills/gsd-path-define/scripts/promote_lookahead.py +1 -1
  196. package/skills/gsd-path-define/scripts/review_panel.py +1 -1
  197. package/skills/gsd-path-define/scripts/state_checkpoint.py +135 -44
  198. package/skills/gsd-path-define/scripts/state_promote.py +2 -0
  199. package/skills/gsd-path-define/scripts/worktree_paths.py +72 -10
  200. package/skills/gsd-path-define/templates/intent.md +30 -0
  201. package/skills/gsd-path-define/templates/state.md +1 -1
  202. package/skills/gsd-path-discuss/SKILL.md +2 -1
  203. package/skills/gsd-path-discuss/references/operating-rules.md +214 -0
  204. package/skills/gsd-path-discuss/scripts/_common.py +619 -18
  205. package/skills/gsd-path-discuss/scripts/discussion_records.py +19 -13
  206. package/skills/gsd-path-discuss/scripts/loop_run.py +31 -29
  207. package/skills/gsd-path-discuss/scripts/path_config.py +5 -5
  208. package/skills/gsd-path-discuss/scripts/review_panel.py +1 -1
  209. package/skills/gsd-path-discuss/scripts/worktree_paths.py +72 -10
  210. package/skills/gsd-path-docs-audit/SKILL.md +6 -4
  211. package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
  212. package/skills/gsd-path-docs-audit/references/docs-auditor.md +7 -1
  213. package/skills/gsd-path-docs-audit/references/operating-rules.md +214 -0
  214. package/skills/gsd-path-docs-audit/scripts/_common.py +619 -18
  215. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +160 -86
  216. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +100 -32
  217. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +91 -7
  218. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +662 -50
  219. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +338 -30
  220. package/skills/gsd-path-docs-audit/scripts/detect_project.py +587 -32
  221. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +19 -13
  222. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +152 -50
  223. package/skills/gsd-path-docs-audit/scripts/integration.py +580 -25
  224. package/skills/gsd-path-docs-audit/scripts/isolation.py +1438 -57
  225. package/skills/gsd-path-docs-audit/scripts/loop_run.py +31 -29
  226. package/skills/gsd-path-docs-audit/scripts/members.py +706 -0
  227. package/skills/gsd-path-docs-audit/scripts/path_config.py +5 -5
  228. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +124 -34
  229. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +287 -87
  230. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +1 -1
  231. package/skills/gsd-path-docs-audit/scripts/review_panel.py +1 -1
  232. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +135 -44
  233. package/skills/gsd-path-docs-audit/scripts/state_promote.py +2 -0
  234. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +72 -10
  235. package/skills/gsd-path-docs-audit/templates/docs-audit.md +6 -2
  236. package/skills/gsd-path-forensics/SKILL.md +2 -1
  237. package/skills/gsd-path-forensics/references/operating-rules.md +214 -0
  238. package/skills/gsd-path-forensics/scripts/_common.py +619 -18
  239. package/skills/gsd-path-forensics/scripts/discussion_records.py +19 -13
  240. package/skills/gsd-path-forensics/scripts/loop_run.py +31 -29
  241. package/skills/gsd-path-forensics/scripts/path_config.py +5 -5
  242. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +82 -31
  243. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +218 -30
  244. package/skills/gsd-path-forensics/scripts/review_panel.py +1 -1
  245. package/skills/gsd-path-forensics/scripts/worktree_paths.py +72 -10
  246. package/skills/gsd-path-inspect/SKILL.md +5 -4
  247. package/skills/gsd-path-inspect/references/codebase-mapper.md +2 -1
  248. package/skills/gsd-path-inspect/references/dispatch.md +13 -0
  249. package/skills/gsd-path-inspect/references/docs-auditor.md +7 -1
  250. package/skills/gsd-path-inspect/references/operating-rules.md +214 -0
  251. package/skills/gsd-path-inspect/scripts/_common.py +619 -18
  252. package/skills/gsd-path-inspect/scripts/archive_milestone.py +160 -86
  253. package/skills/gsd-path-inspect/scripts/build_recovery.py +100 -32
  254. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +91 -7
  255. package/skills/gsd-path-inspect/scripts/check_handoffs.py +662 -50
  256. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +338 -30
  257. package/skills/gsd-path-inspect/scripts/detect_project.py +587 -32
  258. package/skills/gsd-path-inspect/scripts/discussion_records.py +19 -13
  259. package/skills/gsd-path-inspect/scripts/discussion_validate.py +152 -50
  260. package/skills/gsd-path-inspect/scripts/integration.py +580 -25
  261. package/skills/gsd-path-inspect/scripts/isolation.py +1438 -57
  262. package/skills/gsd-path-inspect/scripts/loop_run.py +31 -29
  263. package/skills/gsd-path-inspect/scripts/members.py +706 -0
  264. package/skills/gsd-path-inspect/scripts/path_config.py +5 -5
  265. package/skills/gsd-path-inspect/scripts/pipeline_git.py +124 -34
  266. package/skills/gsd-path-inspect/scripts/pipeline_state.py +287 -87
  267. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +1 -1
  268. package/skills/gsd-path-inspect/scripts/review_panel.py +1 -1
  269. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +135 -44
  270. package/skills/gsd-path-inspect/scripts/state_promote.py +2 -0
  271. package/skills/gsd-path-inspect/scripts/workflow_run.py +107 -30
  272. package/skills/gsd-path-inspect/scripts/worktree_paths.py +72 -10
  273. package/skills/gsd-path-inspect/templates/codebase.md +1 -1
  274. package/skills/gsd-path-inspect/templates/docs-audit.md +6 -2
  275. package/skills/gsd-path-inspect/templates/state.md +1 -1
  276. package/skills/gsd-path-loop/SKILL.md +2 -1
  277. package/skills/gsd-path-loop/references/operating-rules.md +214 -0
  278. package/skills/gsd-path-loop/scripts/_common.py +619 -18
  279. package/skills/gsd-path-loop/scripts/discussion_records.py +19 -13
  280. package/skills/gsd-path-loop/scripts/loop_run.py +31 -29
  281. package/skills/gsd-path-loop/scripts/path_config.py +5 -5
  282. package/skills/gsd-path-loop/scripts/review_panel.py +1 -1
  283. package/skills/gsd-path-loop/scripts/worktree_paths.py +72 -10
  284. package/skills/gsd-path-migrate/SKILL.md +2 -1
  285. package/skills/gsd-path-migrate/references/operating-rules.md +214 -0
  286. package/skills/gsd-path-migrate/scripts/_common.py +619 -18
  287. package/skills/gsd-path-migrate/scripts/core_hook_gate.py +10 -1
  288. package/skills/gsd-path-migrate/scripts/core_hook_settings.py +11 -4
  289. package/skills/gsd-path-migrate/scripts/discussion_records.py +19 -13
  290. package/skills/gsd-path-migrate/scripts/loop_run.py +31 -29
  291. package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -2
  292. package/skills/gsd-path-migrate/scripts/path_config.py +5 -5
  293. package/skills/gsd-path-migrate/scripts/review_panel.py +1 -1
  294. package/skills/gsd-path-plan/SKILL.md +18 -4
  295. package/skills/gsd-path-plan/references/dispatch.md +13 -0
  296. package/skills/gsd-path-plan/references/operating-rules.md +214 -0
  297. package/skills/gsd-path-plan/references/planner.md +22 -4
  298. package/skills/gsd-path-plan/references/spec-probes.md +150 -0
  299. package/skills/gsd-path-plan/scripts/_common.py +619 -18
  300. package/skills/gsd-path-plan/scripts/archive_milestone.py +160 -86
  301. package/skills/gsd-path-plan/scripts/build_recovery.py +100 -32
  302. package/skills/gsd-path-plan/scripts/check_handoffs.py +662 -50
  303. package/skills/gsd-path-plan/scripts/check_task_briefs.py +338 -30
  304. package/skills/gsd-path-plan/scripts/detect_project.py +587 -32
  305. package/skills/gsd-path-plan/scripts/discussion_records.py +19 -13
  306. package/skills/gsd-path-plan/scripts/discussion_validate.py +152 -50
  307. package/skills/gsd-path-plan/scripts/integration.py +580 -25
  308. package/skills/gsd-path-plan/scripts/isolation.py +1438 -57
  309. package/skills/gsd-path-plan/scripts/loop_run.py +31 -29
  310. package/skills/gsd-path-plan/scripts/members.py +706 -0
  311. package/skills/gsd-path-plan/scripts/path_config.py +5 -5
  312. package/skills/gsd-path-plan/scripts/pipeline_git.py +124 -34
  313. package/skills/gsd-path-plan/scripts/pipeline_state.py +287 -87
  314. package/skills/gsd-path-plan/scripts/promote_lookahead.py +1 -1
  315. package/skills/gsd-path-plan/scripts/review_panel.py +1 -1
  316. package/skills/gsd-path-plan/scripts/state_checkpoint.py +135 -44
  317. package/skills/gsd-path-plan/scripts/state_promote.py +2 -0
  318. package/skills/gsd-path-plan/scripts/workflow_run.py +107 -30
  319. package/skills/gsd-path-plan/scripts/worktree_paths.py +72 -10
  320. package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
  321. package/skills/gsd-path-plan/templates/plan.md +17 -2
  322. package/skills/gsd-path-plan/templates/task.md +1 -0
  323. package/skills/gsd-path-research/SKILL.md +13 -1
  324. package/skills/gsd-path-research/references/dispatch.md +13 -0
  325. package/skills/gsd-path-research/references/operating-rules.md +214 -0
  326. package/skills/gsd-path-research/scripts/_common.py +619 -18
  327. package/skills/gsd-path-research/scripts/archive_milestone.py +160 -86
  328. package/skills/gsd-path-research/scripts/build_recovery.py +100 -32
  329. package/skills/gsd-path-research/scripts/check_handoffs.py +662 -50
  330. package/skills/gsd-path-research/scripts/check_task_briefs.py +338 -30
  331. package/skills/gsd-path-research/scripts/detect_project.py +587 -32
  332. package/skills/gsd-path-research/scripts/discussion_records.py +19 -13
  333. package/skills/gsd-path-research/scripts/discussion_validate.py +152 -50
  334. package/skills/gsd-path-research/scripts/integration.py +580 -25
  335. package/skills/gsd-path-research/scripts/isolation.py +1438 -57
  336. package/skills/gsd-path-research/scripts/loop_run.py +31 -29
  337. package/skills/gsd-path-research/scripts/members.py +706 -0
  338. package/skills/gsd-path-research/scripts/path_config.py +5 -5
  339. package/skills/gsd-path-research/scripts/pipeline_git.py +124 -34
  340. package/skills/gsd-path-research/scripts/pipeline_state.py +287 -87
  341. package/skills/gsd-path-research/scripts/promote_lookahead.py +1 -1
  342. package/skills/gsd-path-research/scripts/review_panel.py +1 -1
  343. package/skills/gsd-path-research/scripts/state_checkpoint.py +135 -44
  344. package/skills/gsd-path-research/scripts/state_promote.py +2 -0
  345. package/skills/gsd-path-research/scripts/worktree_paths.py +72 -10
  346. package/skills/gsd-path-research/templates/research-handoff.md +14 -3
  347. package/skills/gsd-path-roadmap/SKILL.md +2 -1
  348. package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
  349. package/skills/gsd-path-roadmap/references/operating-rules.md +214 -0
  350. package/skills/gsd-path-roadmap/scripts/_common.py +619 -18
  351. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +160 -86
  352. package/skills/gsd-path-roadmap/scripts/build_recovery.py +100 -32
  353. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +662 -50
  354. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +338 -30
  355. package/skills/gsd-path-roadmap/scripts/detect_project.py +587 -32
  356. package/skills/gsd-path-roadmap/scripts/discussion_records.py +19 -13
  357. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +152 -50
  358. package/skills/gsd-path-roadmap/scripts/integration.py +580 -25
  359. package/skills/gsd-path-roadmap/scripts/isolation.py +1438 -57
  360. package/skills/gsd-path-roadmap/scripts/loop_run.py +31 -29
  361. package/skills/gsd-path-roadmap/scripts/members.py +706 -0
  362. package/skills/gsd-path-roadmap/scripts/path_config.py +5 -5
  363. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +124 -34
  364. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +287 -87
  365. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +1 -1
  366. package/skills/gsd-path-roadmap/scripts/review_panel.py +1 -1
  367. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +135 -44
  368. package/skills/gsd-path-roadmap/scripts/state_promote.py +2 -0
  369. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +72 -10
  370. package/skills/gsd-path-ship/SKILL.md +33 -11
  371. package/skills/gsd-path-ship/references/dispatch.md +13 -0
  372. package/skills/gsd-path-ship/references/operating-rules.md +214 -0
  373. package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
  374. package/skills/gsd-path-ship/references/reviewer-wave.md +18 -5
  375. package/skills/gsd-path-ship/references/reviewer.md +3 -1
  376. package/skills/gsd-path-ship/references/spec-probes.md +150 -0
  377. package/skills/gsd-path-ship/scripts/_common.py +619 -18
  378. package/skills/gsd-path-ship/scripts/archive_milestone.py +160 -86
  379. package/skills/gsd-path-ship/scripts/build_recovery.py +100 -32
  380. package/skills/gsd-path-ship/scripts/build_state.py +143 -24
  381. package/skills/gsd-path-ship/scripts/check_handoffs.py +662 -50
  382. package/skills/gsd-path-ship/scripts/check_task_briefs.py +338 -30
  383. package/skills/gsd-path-ship/scripts/detect_project.py +587 -32
  384. package/skills/gsd-path-ship/scripts/discussion_records.py +19 -13
  385. package/skills/gsd-path-ship/scripts/discussion_validate.py +152 -50
  386. package/skills/gsd-path-ship/scripts/integration.py +580 -25
  387. package/skills/gsd-path-ship/scripts/isolation.py +1438 -57
  388. package/skills/gsd-path-ship/scripts/lean_verification.py +99 -19
  389. package/skills/gsd-path-ship/scripts/loop_run.py +31 -29
  390. package/skills/gsd-path-ship/scripts/members.py +706 -0
  391. package/skills/gsd-path-ship/scripts/path_config.py +5 -5
  392. package/skills/gsd-path-ship/scripts/pipeline_git.py +124 -34
  393. package/skills/gsd-path-ship/scripts/pipeline_state.py +287 -87
  394. package/skills/gsd-path-ship/scripts/promote_lookahead.py +1 -1
  395. package/skills/gsd-path-ship/scripts/review_panel.py +1 -1
  396. package/skills/gsd-path-ship/scripts/state_checkpoint.py +135 -44
  397. package/skills/gsd-path-ship/scripts/state_promote.py +2 -0
  398. package/skills/gsd-path-ship/scripts/workflow_run.py +107 -30
  399. package/skills/gsd-path-ship/scripts/worktree_paths.py +72 -10
  400. package/skills/gsd-path-ship/templates/final-review.md +16 -4
  401. package/skills/gsd-path-ship/templates/gap-review.md +2 -1
  402. package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
  403. package/skills/gsd-path-ship/templates/wave-review.md +16 -4
  404. package/skills/gsd-path-undo/SKILL.md +2 -1
  405. package/skills/gsd-path-undo/references/operating-rules.md +214 -0
  406. package/skills/gsd-path-undo/scripts/_common.py +619 -18
  407. package/skills/gsd-path-undo/scripts/discussion_records.py +19 -13
  408. package/skills/gsd-path-undo/scripts/loop_run.py +31 -29
  409. package/skills/gsd-path-undo/scripts/path_config.py +5 -5
  410. package/skills/gsd-path-undo/scripts/pipeline_undo.py +218 -30
  411. package/skills/gsd-path-undo/scripts/review_panel.py +1 -1
  412. package/skills/gsd-path-undo/scripts/worktree_paths.py +72 -10
  413. package/skills/path/BUILD.md +7 -1
  414. package/skills/path/DECIDE.md +11 -4
  415. package/skills/path/DEFINE.md +36 -4
  416. package/skills/path/DOCS-AUDIT.md +6 -4
  417. package/skills/path/INSPECT.md +5 -4
  418. package/skills/path/PLAN.md +18 -4
  419. package/skills/path/RESEARCH.md +13 -1
  420. package/skills/path/ROADMAP.md +2 -1
  421. package/skills/path/SHIP.md +33 -11
  422. package/skills/path/SKILL.md +45 -1
  423. package/skills/path/references/build-native.md +63 -15
  424. package/skills/path/references/codebase-mapper.md +2 -1
  425. package/skills/path/references/dispatch.md +13 -0
  426. package/skills/path/references/docs-auditor.md +7 -1
  427. package/skills/path/references/operating-rules.md +214 -0
  428. package/skills/path/references/planner.md +22 -4
  429. package/skills/path/references/reviewer-final.md +20 -2
  430. package/skills/path/references/reviewer-wave.md +18 -5
  431. package/skills/path/references/reviewer.md +3 -1
  432. package/skills/path/references/spec-probes.md +150 -0
  433. package/skills/path/scripts/_common.py +619 -18
  434. package/skills/path/scripts/archive_milestone.py +160 -86
  435. package/skills/path/scripts/bootstrap_repository.py +8 -5
  436. package/skills/path/scripts/build_recovery.py +100 -32
  437. package/skills/path/scripts/build_state.py +143 -24
  438. package/skills/path/scripts/check_docs_audit.py +91 -7
  439. package/skills/path/scripts/check_handoffs.py +662 -50
  440. package/skills/path/scripts/check_task_briefs.py +338 -30
  441. package/skills/path/scripts/check_update.py +2 -2
  442. package/skills/path/scripts/detect_project.py +587 -32
  443. package/skills/path/scripts/discussion_records.py +19 -13
  444. package/skills/path/scripts/discussion_validate.py +152 -50
  445. package/skills/path/scripts/dispatch_driver.py +501 -46
  446. package/skills/path/scripts/git_guard.py +118 -31
  447. package/skills/path/scripts/integration.py +580 -25
  448. package/skills/path/scripts/isolation.py +1438 -57
  449. package/skills/path/scripts/lean_verification.py +99 -19
  450. package/skills/path/scripts/loop_run.py +31 -29
  451. package/skills/path/scripts/members.py +706 -0
  452. package/skills/path/scripts/path_config.py +5 -5
  453. package/skills/path/scripts/pipeline_git.py +124 -34
  454. package/skills/path/scripts/pipeline_state.py +287 -87
  455. package/skills/path/scripts/promote_lookahead.py +1 -1
  456. package/skills/path/scripts/review_panel.py +1 -1
  457. package/skills/path/scripts/state_checkpoint.py +135 -44
  458. package/skills/path/scripts/state_promote.py +2 -0
  459. package/skills/path/scripts/status_runtime.py +58 -10
  460. package/skills/path/scripts/task_context.py +12 -3
  461. package/skills/path/scripts/token_budget.py +12 -8
  462. package/skills/path/scripts/workflow_run.py +107 -30
  463. package/skills/path/scripts/worktree_paths.py +72 -10
  464. package/skills/path/templates/codebase.md +1 -1
  465. package/skills/path/templates/docs-audit.md +6 -2
  466. package/skills/path/templates/final-review.md +16 -4
  467. package/skills/path/templates/gap-review.md +2 -1
  468. package/skills/path/templates/intent.md +30 -0
  469. package/skills/path/templates/patch-findings.md +3 -3
  470. package/skills/path/templates/plan.md +17 -2
  471. package/skills/path/templates/research-handoff.md +14 -3
  472. package/skills/path/templates/state.md +1 -1
  473. package/skills/path/templates/task.md +1 -0
  474. package/skills/path/templates/wave-review.md +16 -4
  475. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  476. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  477. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  478. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  479. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  480. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  481. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
@@ -14,12 +14,45 @@ import sys
14
14
  # Runtime helpers must not modify their immutable installation.
15
15
  sys.dont_write_bytecode = True
16
16
 
17
+ import contextlib
18
+ import errno
17
19
  import json
18
20
  import os
19
21
  import re
22
+ import shlex
23
+ import shutil
24
+ import signal
20
25
  import subprocess
26
+ import time
21
27
  from pathlib import Path
22
- from typing import Optional
28
+ from typing import BinaryIO, Iterator, List, Optional, Sequence
29
+
30
+ # Chosen by what imports, not os.name, so tests can emulate Windows on POSIX.
31
+ try:
32
+ import fcntl
33
+ except ImportError:
34
+ fcntl = None # type: ignore[assignment]
35
+ import msvcrt
36
+
37
+ # Windows opens descriptors in text mode unless asked otherwise.
38
+ O_BINARY = getattr(os, "O_BINARY", 0)
39
+
40
+
41
+ def utf8_stdio() -> None:
42
+ """Write stdout and stderr as UTF-8, as every caller of these CLIs reads them.
43
+
44
+ A Windows pipe defaults to the locale code page (cp1252), so an em dash
45
+ reaches the parent as byte 0x97. Streams a caller substituted are left alone.
46
+ """
47
+ if os.name != "nt":
48
+ return
49
+ for stream in (sys.stdout, sys.stderr):
50
+ if hasattr(stream, "reconfigure") and (stream.encoding or "").lower() != "utf-8":
51
+ stream.reconfigure(encoding="utf-8", errors="replace")
52
+
53
+
54
+ # Every runtime script imports this module before producing output.
55
+ utf8_stdio()
23
56
 
24
57
  PIPELINE_MARKER = "gsd-path/v2"
25
58
  BOUND_BRANCH_RE = re.compile(r"^gsd-path/M(\d{3,})$")
@@ -32,6 +65,101 @@ VERIFY_RESULTS = ("pass", "fail")
32
65
  VERIFY_LEDGER_SCHEMA = "gsd-path/verify-ledger/v2"
33
66
  VERIFY_BLOCK_PATTERN = re.compile(r"```bash[ \t]*\n(?P<block>.*?)```", re.DOTALL)
34
67
 
68
+ REVIEW_BULLET_HEADING_PATTERN = re.compile(r"^#{2,3} ")
69
+ REVIEW_BULLET_FIELD_LABEL_PATTERN = re.compile(r"^- \*\*[^*]+\*\*:")
70
+
71
+
72
+ def review_bullet_field_label_pattern(field: str) -> re.Pattern[str]:
73
+ return re.compile(rf"^- \*\*{re.escape(field)}\*\*:[ \t]*(.*)$")
74
+
75
+
76
+ def _review_bullet_field_value(lines: Sequence[str], index: int, inline: str) -> str:
77
+ if inline:
78
+ return inline
79
+ collected: list[str] = []
80
+ for line in lines[index + 1 :]:
81
+ if REVIEW_BULLET_FIELD_LABEL_PATTERN.match(line) or REVIEW_BULLET_HEADING_PATTERN.match(
82
+ line
83
+ ):
84
+ break
85
+ if line and not line[0].isspace():
86
+ break
87
+ stripped = line.strip()
88
+ if stripped:
89
+ collected.append(stripped)
90
+ return "\n".join(collected)
91
+
92
+
93
+ def find_review_bullet_field_values(lines: Sequence[str], field: str) -> list[str]:
94
+ pattern = review_bullet_field_label_pattern(field)
95
+ values: list[str] = []
96
+ for index, line in enumerate(lines):
97
+ match = pattern.fullmatch(line)
98
+ if match is None:
99
+ continue
100
+ values.append(_review_bullet_field_value(lines, index, match.group(1)))
101
+ return values
102
+
103
+
104
+ def parse_review_bullet_field(lines: Sequence[str], field: str) -> str:
105
+ """Parse one `- **Field**:` review bullet, including indented continuations."""
106
+ values = find_review_bullet_field_values(lines, field)
107
+ if len(values) != 1:
108
+ raise ValueError(field)
109
+ return values[0]
110
+
111
+
112
+ # Sample paths the pipeline must commit. A product rule such as an unanchored
113
+ # `build/` would otherwise drop them silently from every commit.
114
+ PROJECT_PROBE_PATHS = (
115
+ ".project/STATE.md",
116
+ ".project/LESSONS.md",
117
+ ".project/REPOSITORY.md",
118
+ ".project/MEMBERS.md",
119
+ ".project/CHARTER.md",
120
+ ".project/ROADMAP.md",
121
+ ".project/SYNTHESIS.md",
122
+ VERIFY_LEDGER_PATH,
123
+ ".project/build/members.json",
124
+ ".project/build/evidence.json",
125
+ ".project/archive/001-probe/build/evidence.json",
126
+ ".project/archive/001-probe/build/verify-ledger.jsonl",
127
+ ".project/intent/probe.md",
128
+ ".project/research/probe.md",
129
+ ".project/plan/probe.md",
130
+ ".project/plan/PLAN.md",
131
+ ".project/tasks/probe.md",
132
+ ".project/review/probe.md",
133
+ ".project/discuss/probe.md",
134
+ ".project/next/probe.md",
135
+ )
136
+
137
+
138
+ def project_ignore_error(repo: Path) -> Optional[str]:
139
+ """Why ignore rules would drop pipeline state from commits, or None."""
140
+ result = subprocess.run(
141
+ ("git", "-C", str(repo), "check-ignore", "-v", "-n", "-z", "--no-index", "--stdin"),
142
+ input="\0".join(PROJECT_PROBE_PATHS) + "\0",
143
+ encoding="utf-8",
144
+ errors="surrogateescape",
145
+ capture_output=True,
146
+ check=False,
147
+ )
148
+ if result.returncode not in (0, 1):
149
+ return f"could not check ignore rules: {result.stderr.strip()}"
150
+ fields = result.stdout.split("\0")
151
+ hits = []
152
+ for index in range(0, len(fields) - 3, 4):
153
+ source, line, pattern, path = fields[index:index + 4]
154
+ # A `!` match re-includes the path; only a plain match excludes it.
155
+ if pattern and not pattern.startswith("!"):
156
+ hits.append(f"{source}:{line}:{pattern} excludes {path}")
157
+ if not hits:
158
+ return None
159
+ return ("ignore rules exclude GSD Path state that must be committed: "
160
+ + "; ".join(hits)
161
+ + ". Anchor the product rule (for example `/build/`) in a task that owns it.")
162
+
35
163
 
36
164
  def section_body(text: str, heading: str) -> Optional[str]:
37
165
  """The text under `## <heading>` up to the next `## `, or None when absent."""
@@ -48,9 +176,11 @@ def task_verify_command(task_text: str) -> str:
48
176
  return block.group("block").removesuffix("\n") if block else ""
49
177
 
50
178
 
51
- def latest_verify_entry(entries: list, command: str, commit: str) -> Optional[dict]:
179
+ def latest_verify_entry(entries: list, command: str, commit: str,
180
+ repo: Optional[str] = None) -> Optional[dict]:
181
+ """Rows are keyed by (command, repo, commit); a row without `repo` is a coordinator row."""
52
182
  for entry in reversed(entries):
53
- if (entry.get("schema") == VERIFY_LEDGER_SCHEMA
183
+ if (entry.get("schema") == VERIFY_LEDGER_SCHEMA and entry.get("repo") == repo
54
184
  and entry["command"] == command and entry["commit"] == commit):
55
185
  return entry
56
186
  return None
@@ -63,6 +193,11 @@ def verify_ledger_entries(path: Path) -> list:
63
193
  return parse_verify_ledger(path.read_text(encoding="utf-8"))
64
194
 
65
195
 
196
+ def read_user_text(path: Path) -> str:
197
+ """Read a user-edited text file with CRLF normalized to LF."""
198
+ return path.read_text(encoding="utf-8").replace("\r\n", "\n").replace("\r", "\n")
199
+
200
+
66
201
  def parse_verify_ledger(text: str) -> list:
67
202
  entries = []
68
203
  for number, line in enumerate(text.splitlines(), start=1):
@@ -78,34 +213,166 @@ def parse_verify_ledger(text: str) -> list:
78
213
  or not isinstance(entry.get("commit"), str)
79
214
  or entry.get("result") not in VERIFY_RESULTS
80
215
  or not isinstance(entry.get("recorded_at"), str)
216
+ or ("repo" in entry and not isinstance(entry["repo"], str))
81
217
  ):
82
218
  raise ValueError(f"{VERIFY_LEDGER_PATH} line {number} has invalid fields")
83
219
  entries.append(entry)
84
220
  return entries
85
221
 
86
222
 
87
- def run_command(
88
- *arguments: str, cwd: Optional[Path] = None
223
+ def _run_exact(
224
+ argv: Sequence[str], *, cwd: Optional[Path] = None, input: Optional[str] = None
89
225
  ) -> subprocess.CompletedProcess[str]:
90
- return subprocess.run(
91
- arguments,
226
+ """subprocess.run with UTF-8 text and no newline translation.
227
+
228
+ Text-mode pipes on Windows turn "\\n" into "\\r\\n" on the way in, so text
229
+ handed to git hash-object would no longer match the committed blob.
230
+ """
231
+ completed = subprocess.run(
232
+ argv,
92
233
  cwd=cwd,
93
- text=True,
234
+ input=None if input is None else input.encode("utf-8", "surrogateescape"),
94
235
  capture_output=True,
95
236
  check=False,
96
237
  )
238
+ return subprocess.CompletedProcess(
239
+ completed.args,
240
+ completed.returncode,
241
+ completed.stdout.decode("utf-8", "surrogateescape"),
242
+ completed.stderr.decode("utf-8", "surrogateescape"),
243
+ )
244
+
245
+
246
+ def run_command(
247
+ *arguments: str, cwd: Optional[Path] = None
248
+ ) -> subprocess.CompletedProcess[str]:
249
+ return _run_exact(resolve_argv(arguments), cwd=cwd)
97
250
 
98
251
 
99
252
  def run_git(
100
253
  repo: Path, *arguments: str, input: Optional[str] = None
101
254
  ) -> subprocess.CompletedProcess[str]:
102
- return subprocess.run(
103
- ("git", "-C", str(repo), *arguments),
104
- input=input,
105
- text=True,
106
- capture_output=True,
107
- check=False,
255
+ return _run_exact(git_command(repo, *arguments), input=input)
256
+
257
+
258
+ def git_command(repo: Path, *arguments: str) -> tuple[str, ...]:
259
+ """Git argv for repo, including WSL drvfs workarounds when needed."""
260
+ return ("git", "-C", str(repo), *git_drvfs_config_flags(repo), *arguments)
261
+
262
+
263
+ def git_drvfs_config_flags(repo: Path) -> tuple[str, ...]:
264
+ """Disable index preload on 9p/drvfs mounts where git commit races on index.lock (#207)."""
265
+ if os.name != "posix":
266
+ return ()
267
+ try:
268
+ root = repo.resolve()
269
+ mounts = Path("/proc/mounts").read_text(encoding="utf-8", errors="replace").splitlines()
270
+ except OSError:
271
+ return ()
272
+ best = ""
273
+ fstype = ""
274
+ for line in mounts:
275
+ parts = line.split()
276
+ if len(parts) < 3:
277
+ continue
278
+ mount_point, mount_type = parts[1], parts[2]
279
+ if mount_type not in {"9p", "drvfs"}:
280
+ continue
281
+ if root == Path(mount_point) or root.is_relative_to(mount_point):
282
+ if len(mount_point) >= len(best):
283
+ best, fstype = mount_point, mount_type
284
+ if not fstype:
285
+ return ()
286
+ return ("-c", "core.preloadindex=false", "-c", "index.threads=1")
287
+
288
+
289
+ def git_index_lock_retryable(stderr: str) -> bool:
290
+ text = stderr.casefold()
291
+ return "index.lock" in text and "file exists" in text
292
+
293
+
294
+ # OS files that are never pipeline artifacts when git ignores them.
295
+ # ponytail: .DS_Store only (#149); add Thumbs.db and the like when reported.
296
+ OS_JUNK_NAMES = frozenset({".DS_Store"})
297
+
298
+
299
+ def is_ignored_junk(path: Path) -> bool:
300
+ """True for an OS junk file that git ignores and does not track."""
301
+ if path.name not in OS_JUNK_NAMES or path.is_symlink() or not path.is_file():
302
+ return False
303
+ return run_git(path.parent, "check-ignore", "-q", "--", path.name).returncode == 0
304
+
305
+
306
+ def git_visible_entries(directory: Path) -> set:
307
+ """Top-level names under directory that git tracks or would add (not ignored)."""
308
+ result = run_git(directory, "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ".")
309
+ if result.returncode != 0:
310
+ raise RuntimeError(f"git ls-files failed in {directory}: {result.stderr.strip()}")
311
+ return {path.split("/", 1)[0] for path in result.stdout.split("\0") if path}
312
+
313
+
314
+ RUNTIME_PIN_PATH = ".gsd-path/runtime.json"
315
+ RUNTIME_PIN_ALLOWANCE = (
316
+ "only runtime-pin commits (.gsd-path/runtime.json) may follow the reviewed HEAD"
317
+ )
318
+
319
+
320
+ def reviewed_head_covers(repo: Path, reviewed: str, head: str) -> bool:
321
+ """True when a review of `reviewed` still covers `head`.
322
+
323
+ That is the same commit, or `reviewed` is an ancestor of `head` and every
324
+ commit on the first-parent path in ``reviewed..head`` is a non-merge commit
325
+ whose only change is ``.gsd-path/runtime.json``. An empty commit does not
326
+ qualify. Any other path, any merge commit, an unresolvable SHA, or a
327
+ non-ancestor returns False.
328
+ """
329
+ if not reviewed or not head:
330
+ return False
331
+ resolved_repo = Path(repo)
332
+ resolved: List[str] = []
333
+ for value in (reviewed, head):
334
+ result = run_git(
335
+ resolved_repo, "rev-parse", "--verify", "--quiet", f"{value}^{{commit}}"
336
+ )
337
+ if result.returncode != 0 or not result.stdout.strip():
338
+ return False
339
+ resolved.append(result.stdout.strip())
340
+ reviewed_sha, head_sha = resolved
341
+ if reviewed_sha == head_sha:
342
+ return True
343
+ if (
344
+ run_git(resolved_repo, "merge-base", "--is-ancestor", reviewed_sha, head_sha).returncode
345
+ != 0
346
+ ):
347
+ return False
348
+ listing = run_git(
349
+ resolved_repo,
350
+ "rev-list",
351
+ "--first-parent",
352
+ "--parents",
353
+ f"{reviewed_sha}..{head_sha}",
108
354
  )
355
+ if listing.returncode != 0:
356
+ return False
357
+ for line in listing.stdout.splitlines():
358
+ fields = line.split()
359
+ if len(fields) != 2:
360
+ return False
361
+ commit_sha, parent_sha = fields[0], fields[1]
362
+ changed = run_git(
363
+ resolved_repo,
364
+ "diff",
365
+ "--name-only",
366
+ "--no-renames",
367
+ "--no-ext-diff",
368
+ parent_sha,
369
+ commit_sha,
370
+ )
371
+ if changed.returncode != 0:
372
+ return False
373
+ if {path for path in changed.stdout.splitlines() if path} != {RUNTIME_PIN_PATH}:
374
+ return False
375
+ return True
109
376
 
110
377
 
111
378
  def atomic_replace(path: Path, temporary_path: Path, content: str) -> None:
@@ -116,14 +383,15 @@ def atomic_replace(path: Path, temporary_path: Path, content: str) -> None:
116
383
  temporary_path.unlink()
117
384
  descriptor = os.open(
118
385
  temporary_path,
119
- os.O_WRONLY | os.O_CREAT | os.O_EXCL,
386
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | O_BINARY,
120
387
  0o666,
121
388
  )
122
- handle = os.fdopen(descriptor, "w", encoding="utf-8")
389
+ # Bytes, so Windows text mode never turns "\n" into "\r\n".
390
+ handle = os.fdopen(descriptor, "wb")
123
391
  descriptor = None
124
392
  with handle:
125
- handle.write(content)
126
- os.replace(temporary_path, path)
393
+ handle.write(content.encode("utf-8"))
394
+ replace(temporary_path, path)
127
395
  finally:
128
396
  if descriptor is not None:
129
397
  os.close(descriptor)
@@ -183,3 +451,336 @@ def unquote(value: str) -> str:
183
451
  ):
184
452
  return cleaned[1:-1]
185
453
  return cleaned
454
+
455
+
456
+ # --- Platform helpers --------------------------------------------------------
457
+ # Each helper keeps the POSIX behavior the scripts had before and adds the
458
+ # native Windows equivalent. Windows has no fcntl, cannot open a directory as
459
+ # a descriptor, and treats os.kill(pid, 0) as a Ctrl+C, not a probe.
460
+
461
+ # Sharing violation and access denied: another process briefly holds the file.
462
+ _WINDOWS_BUSY_ERRORS = (5, 32)
463
+ _REPLACE_ATTEMPTS = 20
464
+
465
+
466
+ def replace(source: Path, destination: Path) -> None:
467
+ """os.replace, retried while a Windows reader briefly holds the destination."""
468
+ if os.name != "nt":
469
+ os.replace(source, destination)
470
+ return
471
+ for attempt in range(_REPLACE_ATTEMPTS):
472
+ try:
473
+ os.replace(source, destination)
474
+ return
475
+ except PermissionError as error:
476
+ if (getattr(error, "winerror", None) not in _WINDOWS_BUSY_ERRORS
477
+ or attempt == _REPLACE_ATTEMPTS - 1):
478
+ raise
479
+ time.sleep(min(0.01 * 2 ** attempt, 0.25))
480
+
481
+
482
+ def fsync_directory(path: Path) -> None:
483
+ """Persist a directory entry change; Windows cannot open a directory to fsync it."""
484
+ if os.name == "nt":
485
+ return
486
+ descriptor = os.open(path, os.O_RDONLY)
487
+ try:
488
+ os.fsync(descriptor)
489
+ finally:
490
+ os.close(descriptor)
491
+
492
+
493
+ @contextlib.contextmanager
494
+ def exclusive_lock(
495
+ path: Path, *, blocking: bool = True, timeout: Optional[float] = None
496
+ ) -> Iterator[BinaryIO]:
497
+ """Hold an exclusive lock on the file at path, creating it when absent.
498
+
499
+ A non-blocking attempt that loses raises BlockingIOError; a blocking one
500
+ that outlives timeout raises TimeoutError. Windows msvcrt LK_LOCK gives up
501
+ after ten seconds, so both platforms poll a non-blocking lock instead.
502
+ """
503
+ path.parent.mkdir(parents=True, exist_ok=True)
504
+ deadline = None if timeout is None else time.monotonic() + timeout
505
+ with open(path, "a+b") as handle:
506
+ if fcntl is None:
507
+ # msvcrt locks bytes from the current position; keep one to lock.
508
+ handle.seek(0, os.SEEK_END)
509
+ if handle.tell() == 0:
510
+ handle.write(b"\0")
511
+ handle.flush()
512
+ delay = 0.01
513
+ while True:
514
+ try:
515
+ if fcntl is None:
516
+ handle.seek(0)
517
+ msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
518
+ elif blocking and deadline is None:
519
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
520
+ else:
521
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
522
+ break
523
+ except OSError as error:
524
+ if error.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
525
+ raise
526
+ if not blocking:
527
+ raise BlockingIOError(errno.EAGAIN, f"lock is held: {path}") from error
528
+ if deadline is not None and time.monotonic() >= deadline:
529
+ raise TimeoutError(f"timed out waiting for lock: {path}") from error
530
+ time.sleep(delay)
531
+ delay = min(delay * 2, 0.25)
532
+ try:
533
+ yield handle
534
+ finally:
535
+ if fcntl is None:
536
+ handle.seek(0)
537
+ msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
538
+ else:
539
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
540
+
541
+
542
+ _WAIT_OBJECT_0 = 0x0
543
+ _WAIT_ABANDONED = 0x80
544
+ _WAIT_TIMEOUT = 0x102
545
+
546
+
547
+ @contextlib.contextmanager
548
+ def directory_mutex(
549
+ directory: Path, *, blocking: bool = True, timeout: Optional[float] = None
550
+ ) -> Iterator[None]:
551
+ """Windows-only exclusive lock keyed on a directory's resolved path.
552
+
553
+ POSIX flocks the directory itself. Windows cannot lock a directory, and a
554
+ lock file inside it would be committed, so this holds a named kernel mutex
555
+ in the session namespace. A holder that dies releases it (WAIT_ABANDONED).
556
+ Contention raises BlockingIOError (non-blocking) or TimeoutError.
557
+ """
558
+ import ctypes
559
+ import hashlib
560
+ from ctypes import wintypes
561
+
562
+ identity = os.path.normcase(str(directory.resolve()))
563
+ name = "Local\\gsd-path-" + hashlib.sha256(identity.encode("utf-8")).hexdigest()
564
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
565
+ kernel32.CreateMutexW.restype = wintypes.HANDLE
566
+ kernel32.CreateMutexW.argtypes = (ctypes.c_void_p, wintypes.BOOL, wintypes.LPCWSTR)
567
+ kernel32.WaitForSingleObject.restype = wintypes.DWORD
568
+ kernel32.WaitForSingleObject.argtypes = (wintypes.HANDLE, wintypes.DWORD)
569
+ kernel32.ReleaseMutex.argtypes = (wintypes.HANDLE,)
570
+ kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
571
+ handle = kernel32.CreateMutexW(None, False, name)
572
+ if not handle:
573
+ raise ctypes.WinError(ctypes.get_last_error())
574
+ try:
575
+ deadline = None if timeout is None else time.monotonic() + timeout
576
+ while True:
577
+ # Wait in short slices so Ctrl+C still interrupts a blocked caller.
578
+ result = kernel32.WaitForSingleObject(handle, 0 if not blocking else 200)
579
+ if result in (_WAIT_OBJECT_0, _WAIT_ABANDONED):
580
+ break
581
+ if result != _WAIT_TIMEOUT:
582
+ raise ctypes.WinError(ctypes.get_last_error())
583
+ if not blocking:
584
+ raise BlockingIOError(errno.EAGAIN, f"lock is held: {directory}")
585
+ if deadline is not None and time.monotonic() >= deadline:
586
+ raise TimeoutError(f"timed out waiting for lock: {directory}")
587
+ try:
588
+ yield
589
+ finally:
590
+ kernel32.ReleaseMutex(handle)
591
+ finally:
592
+ kernel32.CloseHandle(handle)
593
+
594
+
595
+ def process_alive(pid: int) -> bool:
596
+ """Whether pid names a running process, without signalling it.
597
+
598
+ Windows os.kill(pid, 0) sends CTRL_C_EVENT, so Windows asks the kernel.
599
+ status_runtime and install carry verbatim copies (they cannot import this
600
+ module); tests/test_common_platform.py keeps them identical.
601
+ """
602
+ if pid <= 0:
603
+ return False
604
+ if os.name != "nt":
605
+ try:
606
+ os.kill(pid, 0)
607
+ except OSError as error:
608
+ return error.errno == errno.EPERM
609
+ return True
610
+ import ctypes
611
+ from ctypes import wintypes
612
+
613
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
614
+ kernel32.OpenProcess.restype = wintypes.HANDLE
615
+ kernel32.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
616
+ kernel32.GetExitCodeProcess.argtypes = (wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD))
617
+ kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
618
+ handle = kernel32.OpenProcess(0x1000, False, pid) # PROCESS_QUERY_LIMITED_INFORMATION
619
+ if not handle:
620
+ # ERROR_ACCESS_DENIED: a protected process exists but refuses the query.
621
+ return ctypes.get_last_error() == 5
622
+ try:
623
+ code = wintypes.DWORD()
624
+ if not kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
625
+ return True
626
+ return code.value == 259 # STILL_ACTIVE
627
+ finally:
628
+ kernel32.CloseHandle(handle)
629
+
630
+
631
+ def popen_group(argv: Sequence[str], **kwargs) -> subprocess.Popen:
632
+ """Start argv as the leader of a new process group that kill_tree can end."""
633
+ if os.name == "nt":
634
+ kwargs["creationflags"] = (kwargs.get("creationflags", 0)
635
+ | subprocess.CREATE_NEW_PROCESS_GROUP)
636
+ else:
637
+ kwargs["start_new_session"] = True
638
+ return subprocess.Popen(list(argv), **kwargs)
639
+
640
+
641
+ # Windows creation flags for a background process that outlives its caller.
642
+ _CREATE_NO_WINDOW = 0x08000000
643
+ _CREATE_BREAKAWAY_FROM_JOB = 0x01000000
644
+
645
+
646
+ def popen_detached(argv: Sequence[str], **kwargs) -> subprocess.Popen:
647
+ """Start a background process that survives its caller's exit.
648
+
649
+ POSIX starts a new session. Windows hosts may run commands in a job object
650
+ that kills its members when the command returns, so this breaks away when
651
+ the job allows it, and gives the process a hidden console that its console
652
+ children inherit instead of flashing new windows.
653
+ """
654
+ if os.name != "nt":
655
+ return subprocess.Popen(list(argv), start_new_session=True, **kwargs)
656
+ flags = subprocess.CREATE_NEW_PROCESS_GROUP | _CREATE_NO_WINDOW
657
+ try:
658
+ return subprocess.Popen(list(argv), creationflags=flags | _CREATE_BREAKAWAY_FROM_JOB, **kwargs)
659
+ except PermissionError:
660
+ # The job forbids breakaway; the process still runs, bound to that job.
661
+ return subprocess.Popen(list(argv), creationflags=flags, **kwargs)
662
+
663
+
664
+ def kill_tree(process: subprocess.Popen) -> None:
665
+ """Forcibly end a popen_group process and every descendant it started."""
666
+ if os.name == "nt":
667
+ subprocess.run(
668
+ ("taskkill", "/T", "/F", "/PID", str(process.pid)),
669
+ capture_output=True,
670
+ check=False,
671
+ )
672
+ if process.poll() is None:
673
+ process.kill()
674
+ return
675
+ try:
676
+ os.killpg(process.pid, signal.SIGKILL)
677
+ except ProcessLookupError:
678
+ pass
679
+
680
+
681
+ class ShellNotFound(RuntimeError):
682
+ pass
683
+
684
+
685
+ def _is_wsl_launcher(path: str) -> bool:
686
+ folded = os.path.normcase(os.path.abspath(path))
687
+ system_root = os.path.normcase(os.environ.get("SystemRoot", r"C:\Windows"))
688
+ windows_apps = os.path.normcase(os.path.join(
689
+ os.environ.get("LOCALAPPDATA", ""), "Microsoft", "WindowsApps"))
690
+ return folded.startswith(system_root + os.sep) or (
691
+ bool(os.environ.get("LOCALAPPDATA")) and folded.startswith(windows_apps + os.sep))
692
+
693
+
694
+ def find_bash() -> str:
695
+ """The bash that runs Verify commands; on Windows, Git for Windows' bash.exe.
696
+
697
+ Windows puts the WSL launcher at System32\\bash.exe, ahead of PATH, so a
698
+ bare "bash" would run the command in another OS. GSD_PATH_BASH overrides.
699
+ """
700
+ if os.name != "nt":
701
+ return "bash"
702
+ override = os.environ.get("GSD_PATH_BASH")
703
+ if override:
704
+ if not os.path.isfile(override):
705
+ raise ShellNotFound(f"GSD_PATH_BASH is not a file: {override}")
706
+ return override
707
+ candidates = []
708
+ found = shutil.which("bash")
709
+ if found:
710
+ candidates.append(found)
711
+ git = shutil.which("git")
712
+ if git:
713
+ # <Git>\cmd\git.exe or <Git>\bin\git.exe -> <Git>\bin\bash.exe
714
+ candidates.append(os.path.join(os.path.dirname(os.path.dirname(git)), "bin", "bash.exe"))
715
+ for variable in ("ProgramFiles", "ProgramW6432", "LOCALAPPDATA"):
716
+ base = os.environ.get(variable)
717
+ if base:
718
+ parts = ("Programs", "Git") if variable == "LOCALAPPDATA" else ("Git",)
719
+ candidates.append(os.path.join(base, *parts, "bin", "bash.exe"))
720
+ for candidate in candidates:
721
+ if os.path.isfile(candidate) and not _is_wsl_launcher(candidate):
722
+ return candidate
723
+ raise ShellNotFound(
724
+ "Verify commands need bash; install Git for Windows or set GSD_PATH_BASH "
725
+ "to its bin\\bash.exe (the System32 WSL launcher is never used)"
726
+ )
727
+
728
+
729
+ def bash_argv(command: str) -> List[str]:
730
+ return [find_bash(), "-c", command]
731
+
732
+
733
+ # cmd.exe re-parses the arguments of a .cmd or .bat target, so these would
734
+ # let an argument escape its quoting.
735
+ _CMD_METACHARACTERS = frozenset('%^&|<>"!\r\n')
736
+
737
+
738
+ def resolve_argv(argv: Sequence[str]) -> List[str]:
739
+ """argv with its program resolved the way a shell would find it.
740
+
741
+ Windows CreateProcess only appends .exe, so npm-installed CLIs such as
742
+ claude.cmd or codex.cmd are found through PATHEXT here. POSIX is unchanged.
743
+ """
744
+ arguments = list(argv)
745
+ if os.name != "nt" or not arguments:
746
+ return arguments
747
+ program = shutil.which(arguments[0])
748
+ if program is None:
749
+ raise FileNotFoundError(f"command not found: {arguments[0]}")
750
+ if program.lower().endswith((".cmd", ".bat")):
751
+ for argument in arguments[1:]:
752
+ if _CMD_METACHARACTERS.intersection(argument):
753
+ raise ValueError(
754
+ f"argument {argument!r} is unsafe to pass to {program}; "
755
+ "cmd.exe would reinterpret it"
756
+ )
757
+ return [program, *arguments[1:]]
758
+
759
+
760
+ def split_command(text: str) -> List[str]:
761
+ """Split a command string with POSIX quoting.
762
+
763
+ Agents write these in a POSIX shell (Git Bash on Windows), so single and
764
+ double quotes group as in sh. On Windows backslash is not an escape, so a
765
+ path such as C:\\Python312\\python.exe keeps its separators.
766
+ """
767
+ if os.name != "nt":
768
+ return shlex.split(text)
769
+ lexer = shlex.shlex(text, posix=True)
770
+ lexer.whitespace_split = True
771
+ lexer.escape = ""
772
+ lexer.commenters = ""
773
+ return list(lexer)
774
+
775
+
776
+ def rmtree_force(path: Path) -> None:
777
+ """shutil.rmtree that also removes read-only files, such as git objects on Windows."""
778
+
779
+ def clear_readonly(function, target, _error) -> None:
780
+ os.chmod(target, 0o700)
781
+ function(target)
782
+
783
+ if sys.version_info >= (3, 12):
784
+ shutil.rmtree(path, onexc=clear_readonly)
785
+ else:
786
+ shutil.rmtree(path, onerror=clear_readonly)