@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
@@ -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,})$")
@@ -33,6 +66,58 @@ 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
 
35
68
 
69
+ # Sample paths the pipeline must commit. A product rule such as an unanchored
70
+ # `build/` would otherwise drop them silently from every commit.
71
+ PROJECT_PROBE_PATHS = (
72
+ ".project/STATE.md",
73
+ ".project/LESSONS.md",
74
+ ".project/REPOSITORY.md",
75
+ ".project/MEMBERS.md",
76
+ ".project/CHARTER.md",
77
+ ".project/ROADMAP.md",
78
+ ".project/SYNTHESIS.md",
79
+ VERIFY_LEDGER_PATH,
80
+ ".project/build/members.json",
81
+ ".project/build/evidence.json",
82
+ ".project/archive/001-probe/build/evidence.json",
83
+ ".project/archive/001-probe/build/verify-ledger.jsonl",
84
+ ".project/intent/probe.md",
85
+ ".project/research/probe.md",
86
+ ".project/plan/probe.md",
87
+ ".project/plan/PLAN.md",
88
+ ".project/tasks/probe.md",
89
+ ".project/review/probe.md",
90
+ ".project/discuss/probe.md",
91
+ ".project/next/probe.md",
92
+ )
93
+
94
+
95
+ def project_ignore_error(repo: Path) -> Optional[str]:
96
+ """Why ignore rules would drop pipeline state from commits, or None."""
97
+ result = subprocess.run(
98
+ ("git", "-C", str(repo), "check-ignore", "-v", "-n", "-z", "--no-index", "--stdin"),
99
+ input="\0".join(PROJECT_PROBE_PATHS) + "\0",
100
+ encoding="utf-8",
101
+ errors="surrogateescape",
102
+ capture_output=True,
103
+ check=False,
104
+ )
105
+ if result.returncode not in (0, 1):
106
+ return f"could not check ignore rules: {result.stderr.strip()}"
107
+ fields = result.stdout.split("\0")
108
+ hits = []
109
+ for index in range(0, len(fields) - 3, 4):
110
+ source, line, pattern, path = fields[index:index + 4]
111
+ # A `!` match re-includes the path; only a plain match excludes it.
112
+ if pattern and not pattern.startswith("!"):
113
+ hits.append(f"{source}:{line}:{pattern} excludes {path}")
114
+ if not hits:
115
+ return None
116
+ return ("ignore rules exclude GSD Path state that must be committed: "
117
+ + "; ".join(hits)
118
+ + ". Anchor the product rule (for example `/build/`) in a task that owns it.")
119
+
120
+
36
121
  def section_body(text: str, heading: str) -> Optional[str]:
37
122
  """The text under `## <heading>` up to the next `## `, or None when absent."""
38
123
  match = re.search(
@@ -48,9 +133,11 @@ def task_verify_command(task_text: str) -> str:
48
133
  return block.group("block").removesuffix("\n") if block else ""
49
134
 
50
135
 
51
- def latest_verify_entry(entries: list, command: str, commit: str) -> Optional[dict]:
136
+ def latest_verify_entry(entries: list, command: str, commit: str,
137
+ repo: Optional[str] = None) -> Optional[dict]:
138
+ """Rows are keyed by (command, repo, commit); a row without `repo` is a coordinator row."""
52
139
  for entry in reversed(entries):
53
- if (entry.get("schema") == VERIFY_LEDGER_SCHEMA
140
+ if (entry.get("schema") == VERIFY_LEDGER_SCHEMA and entry.get("repo") == repo
54
141
  and entry["command"] == command and entry["commit"] == commit):
55
142
  return entry
56
143
  return None
@@ -78,34 +165,66 @@ def parse_verify_ledger(text: str) -> list:
78
165
  or not isinstance(entry.get("commit"), str)
79
166
  or entry.get("result") not in VERIFY_RESULTS
80
167
  or not isinstance(entry.get("recorded_at"), str)
168
+ or ("repo" in entry and not isinstance(entry["repo"], str))
81
169
  ):
82
170
  raise ValueError(f"{VERIFY_LEDGER_PATH} line {number} has invalid fields")
83
171
  entries.append(entry)
84
172
  return entries
85
173
 
86
174
 
87
- def run_command(
88
- *arguments: str, cwd: Optional[Path] = None
175
+ def _run_exact(
176
+ argv: Sequence[str], *, cwd: Optional[Path] = None, input: Optional[str] = None
89
177
  ) -> subprocess.CompletedProcess[str]:
90
- return subprocess.run(
91
- arguments,
178
+ """subprocess.run with UTF-8 text and no newline translation.
179
+
180
+ Text-mode pipes on Windows turn "\\n" into "\\r\\n" on the way in, so text
181
+ handed to git hash-object would no longer match the committed blob.
182
+ """
183
+ completed = subprocess.run(
184
+ argv,
92
185
  cwd=cwd,
93
- text=True,
186
+ input=None if input is None else input.encode("utf-8", "surrogateescape"),
94
187
  capture_output=True,
95
188
  check=False,
96
189
  )
190
+ return subprocess.CompletedProcess(
191
+ completed.args,
192
+ completed.returncode,
193
+ completed.stdout.decode("utf-8", "surrogateescape"),
194
+ completed.stderr.decode("utf-8", "surrogateescape"),
195
+ )
196
+
197
+
198
+ def run_command(
199
+ *arguments: str, cwd: Optional[Path] = None
200
+ ) -> subprocess.CompletedProcess[str]:
201
+ return _run_exact(resolve_argv(arguments), cwd=cwd)
97
202
 
98
203
 
99
204
  def run_git(
100
205
  repo: Path, *arguments: str, input: Optional[str] = None
101
206
  ) -> 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,
108
- )
207
+ return _run_exact(("git", "-C", str(repo), *arguments), input=input)
208
+
209
+
210
+ # OS files that are never pipeline artifacts when git ignores them.
211
+ # ponytail: .DS_Store only (#149); add Thumbs.db and the like when reported.
212
+ OS_JUNK_NAMES = frozenset({".DS_Store"})
213
+
214
+
215
+ def is_ignored_junk(path: Path) -> bool:
216
+ """True for an OS junk file that git ignores and does not track."""
217
+ if path.name not in OS_JUNK_NAMES or path.is_symlink() or not path.is_file():
218
+ return False
219
+ return run_git(path.parent, "check-ignore", "-q", "--", path.name).returncode == 0
220
+
221
+
222
+ def git_visible_entries(directory: Path) -> set:
223
+ """Top-level names under directory that git tracks or would add (not ignored)."""
224
+ result = run_git(directory, "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ".")
225
+ if result.returncode != 0:
226
+ raise RuntimeError(f"git ls-files failed in {directory}: {result.stderr.strip()}")
227
+ return {path.split("/", 1)[0] for path in result.stdout.split("\0") if path}
109
228
 
110
229
 
111
230
  def atomic_replace(path: Path, temporary_path: Path, content: str) -> None:
@@ -116,14 +235,15 @@ def atomic_replace(path: Path, temporary_path: Path, content: str) -> None:
116
235
  temporary_path.unlink()
117
236
  descriptor = os.open(
118
237
  temporary_path,
119
- os.O_WRONLY | os.O_CREAT | os.O_EXCL,
238
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | O_BINARY,
120
239
  0o666,
121
240
  )
122
- handle = os.fdopen(descriptor, "w", encoding="utf-8")
241
+ # Bytes, so Windows text mode never turns "\n" into "\r\n".
242
+ handle = os.fdopen(descriptor, "wb")
123
243
  descriptor = None
124
244
  with handle:
125
- handle.write(content)
126
- os.replace(temporary_path, path)
245
+ handle.write(content.encode("utf-8"))
246
+ replace(temporary_path, path)
127
247
  finally:
128
248
  if descriptor is not None:
129
249
  os.close(descriptor)
@@ -183,3 +303,336 @@ def unquote(value: str) -> str:
183
303
  ):
184
304
  return cleaned[1:-1]
185
305
  return cleaned
306
+
307
+
308
+ # --- Platform helpers --------------------------------------------------------
309
+ # Each helper keeps the POSIX behavior the scripts had before and adds the
310
+ # native Windows equivalent. Windows has no fcntl, cannot open a directory as
311
+ # a descriptor, and treats os.kill(pid, 0) as a Ctrl+C, not a probe.
312
+
313
+ # Sharing violation and access denied: another process briefly holds the file.
314
+ _WINDOWS_BUSY_ERRORS = (5, 32)
315
+ _REPLACE_ATTEMPTS = 20
316
+
317
+
318
+ def replace(source: Path, destination: Path) -> None:
319
+ """os.replace, retried while a Windows reader briefly holds the destination."""
320
+ if os.name != "nt":
321
+ os.replace(source, destination)
322
+ return
323
+ for attempt in range(_REPLACE_ATTEMPTS):
324
+ try:
325
+ os.replace(source, destination)
326
+ return
327
+ except PermissionError as error:
328
+ if (getattr(error, "winerror", None) not in _WINDOWS_BUSY_ERRORS
329
+ or attempt == _REPLACE_ATTEMPTS - 1):
330
+ raise
331
+ time.sleep(min(0.01 * 2 ** attempt, 0.25))
332
+
333
+
334
+ def fsync_directory(path: Path) -> None:
335
+ """Persist a directory entry change; Windows cannot open a directory to fsync it."""
336
+ if os.name == "nt":
337
+ return
338
+ descriptor = os.open(path, os.O_RDONLY)
339
+ try:
340
+ os.fsync(descriptor)
341
+ finally:
342
+ os.close(descriptor)
343
+
344
+
345
+ @contextlib.contextmanager
346
+ def exclusive_lock(
347
+ path: Path, *, blocking: bool = True, timeout: Optional[float] = None
348
+ ) -> Iterator[BinaryIO]:
349
+ """Hold an exclusive lock on the file at path, creating it when absent.
350
+
351
+ A non-blocking attempt that loses raises BlockingIOError; a blocking one
352
+ that outlives timeout raises TimeoutError. Windows msvcrt LK_LOCK gives up
353
+ after ten seconds, so both platforms poll a non-blocking lock instead.
354
+ """
355
+ path.parent.mkdir(parents=True, exist_ok=True)
356
+ deadline = None if timeout is None else time.monotonic() + timeout
357
+ with open(path, "a+b") as handle:
358
+ if fcntl is None:
359
+ # msvcrt locks bytes from the current position; keep one to lock.
360
+ handle.seek(0, os.SEEK_END)
361
+ if handle.tell() == 0:
362
+ handle.write(b"\0")
363
+ handle.flush()
364
+ delay = 0.01
365
+ while True:
366
+ try:
367
+ if fcntl is None:
368
+ handle.seek(0)
369
+ msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
370
+ elif blocking and deadline is None:
371
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
372
+ else:
373
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
374
+ break
375
+ except OSError as error:
376
+ if error.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
377
+ raise
378
+ if not blocking:
379
+ raise BlockingIOError(errno.EAGAIN, f"lock is held: {path}") from error
380
+ if deadline is not None and time.monotonic() >= deadline:
381
+ raise TimeoutError(f"timed out waiting for lock: {path}") from error
382
+ time.sleep(delay)
383
+ delay = min(delay * 2, 0.25)
384
+ try:
385
+ yield handle
386
+ finally:
387
+ if fcntl is None:
388
+ handle.seek(0)
389
+ msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
390
+ else:
391
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
392
+
393
+
394
+ _WAIT_OBJECT_0 = 0x0
395
+ _WAIT_ABANDONED = 0x80
396
+ _WAIT_TIMEOUT = 0x102
397
+
398
+
399
+ @contextlib.contextmanager
400
+ def directory_mutex(
401
+ directory: Path, *, blocking: bool = True, timeout: Optional[float] = None
402
+ ) -> Iterator[None]:
403
+ """Windows-only exclusive lock keyed on a directory's resolved path.
404
+
405
+ POSIX flocks the directory itself. Windows cannot lock a directory, and a
406
+ lock file inside it would be committed, so this holds a named kernel mutex
407
+ in the session namespace. A holder that dies releases it (WAIT_ABANDONED).
408
+ Contention raises BlockingIOError (non-blocking) or TimeoutError.
409
+ """
410
+ import ctypes
411
+ import hashlib
412
+ from ctypes import wintypes
413
+
414
+ identity = os.path.normcase(str(directory.resolve()))
415
+ name = "Local\\gsd-path-" + hashlib.sha256(identity.encode("utf-8")).hexdigest()
416
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
417
+ kernel32.CreateMutexW.restype = wintypes.HANDLE
418
+ kernel32.CreateMutexW.argtypes = (ctypes.c_void_p, wintypes.BOOL, wintypes.LPCWSTR)
419
+ kernel32.WaitForSingleObject.restype = wintypes.DWORD
420
+ kernel32.WaitForSingleObject.argtypes = (wintypes.HANDLE, wintypes.DWORD)
421
+ kernel32.ReleaseMutex.argtypes = (wintypes.HANDLE,)
422
+ kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
423
+ handle = kernel32.CreateMutexW(None, False, name)
424
+ if not handle:
425
+ raise ctypes.WinError(ctypes.get_last_error())
426
+ try:
427
+ deadline = None if timeout is None else time.monotonic() + timeout
428
+ while True:
429
+ # Wait in short slices so Ctrl+C still interrupts a blocked caller.
430
+ result = kernel32.WaitForSingleObject(handle, 0 if not blocking else 200)
431
+ if result in (_WAIT_OBJECT_0, _WAIT_ABANDONED):
432
+ break
433
+ if result != _WAIT_TIMEOUT:
434
+ raise ctypes.WinError(ctypes.get_last_error())
435
+ if not blocking:
436
+ raise BlockingIOError(errno.EAGAIN, f"lock is held: {directory}")
437
+ if deadline is not None and time.monotonic() >= deadline:
438
+ raise TimeoutError(f"timed out waiting for lock: {directory}")
439
+ try:
440
+ yield
441
+ finally:
442
+ kernel32.ReleaseMutex(handle)
443
+ finally:
444
+ kernel32.CloseHandle(handle)
445
+
446
+
447
+ def process_alive(pid: int) -> bool:
448
+ """Whether pid names a running process, without signalling it.
449
+
450
+ Windows os.kill(pid, 0) sends CTRL_C_EVENT, so Windows asks the kernel.
451
+ status_runtime and install carry verbatim copies (they cannot import this
452
+ module); tests/test_common_platform.py keeps them identical.
453
+ """
454
+ if pid <= 0:
455
+ return False
456
+ if os.name != "nt":
457
+ try:
458
+ os.kill(pid, 0)
459
+ except OSError as error:
460
+ return error.errno == errno.EPERM
461
+ return True
462
+ import ctypes
463
+ from ctypes import wintypes
464
+
465
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
466
+ kernel32.OpenProcess.restype = wintypes.HANDLE
467
+ kernel32.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
468
+ kernel32.GetExitCodeProcess.argtypes = (wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD))
469
+ kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
470
+ handle = kernel32.OpenProcess(0x1000, False, pid) # PROCESS_QUERY_LIMITED_INFORMATION
471
+ if not handle:
472
+ # ERROR_ACCESS_DENIED: a protected process exists but refuses the query.
473
+ return ctypes.get_last_error() == 5
474
+ try:
475
+ code = wintypes.DWORD()
476
+ if not kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
477
+ return True
478
+ return code.value == 259 # STILL_ACTIVE
479
+ finally:
480
+ kernel32.CloseHandle(handle)
481
+
482
+
483
+ def popen_group(argv: Sequence[str], **kwargs) -> subprocess.Popen:
484
+ """Start argv as the leader of a new process group that kill_tree can end."""
485
+ if os.name == "nt":
486
+ kwargs["creationflags"] = (kwargs.get("creationflags", 0)
487
+ | subprocess.CREATE_NEW_PROCESS_GROUP)
488
+ else:
489
+ kwargs["start_new_session"] = True
490
+ return subprocess.Popen(list(argv), **kwargs)
491
+
492
+
493
+ # Windows creation flags for a background process that outlives its caller.
494
+ _CREATE_NO_WINDOW = 0x08000000
495
+ _CREATE_BREAKAWAY_FROM_JOB = 0x01000000
496
+
497
+
498
+ def popen_detached(argv: Sequence[str], **kwargs) -> subprocess.Popen:
499
+ """Start a background process that survives its caller's exit.
500
+
501
+ POSIX starts a new session. Windows hosts may run commands in a job object
502
+ that kills its members when the command returns, so this breaks away when
503
+ the job allows it, and gives the process a hidden console that its console
504
+ children inherit instead of flashing new windows.
505
+ """
506
+ if os.name != "nt":
507
+ return subprocess.Popen(list(argv), start_new_session=True, **kwargs)
508
+ flags = subprocess.CREATE_NEW_PROCESS_GROUP | _CREATE_NO_WINDOW
509
+ try:
510
+ return subprocess.Popen(list(argv), creationflags=flags | _CREATE_BREAKAWAY_FROM_JOB, **kwargs)
511
+ except PermissionError:
512
+ # The job forbids breakaway; the process still runs, bound to that job.
513
+ return subprocess.Popen(list(argv), creationflags=flags, **kwargs)
514
+
515
+
516
+ def kill_tree(process: subprocess.Popen) -> None:
517
+ """Forcibly end a popen_group process and every descendant it started."""
518
+ if os.name == "nt":
519
+ subprocess.run(
520
+ ("taskkill", "/T", "/F", "/PID", str(process.pid)),
521
+ capture_output=True,
522
+ check=False,
523
+ )
524
+ if process.poll() is None:
525
+ process.kill()
526
+ return
527
+ try:
528
+ os.killpg(process.pid, signal.SIGKILL)
529
+ except ProcessLookupError:
530
+ pass
531
+
532
+
533
+ class ShellNotFound(RuntimeError):
534
+ pass
535
+
536
+
537
+ def _is_wsl_launcher(path: str) -> bool:
538
+ folded = os.path.normcase(os.path.abspath(path))
539
+ system_root = os.path.normcase(os.environ.get("SystemRoot", r"C:\Windows"))
540
+ windows_apps = os.path.normcase(os.path.join(
541
+ os.environ.get("LOCALAPPDATA", ""), "Microsoft", "WindowsApps"))
542
+ return folded.startswith(system_root + os.sep) or (
543
+ bool(os.environ.get("LOCALAPPDATA")) and folded.startswith(windows_apps + os.sep))
544
+
545
+
546
+ def find_bash() -> str:
547
+ """The bash that runs Verify commands; on Windows, Git for Windows' bash.exe.
548
+
549
+ Windows puts the WSL launcher at System32\\bash.exe, ahead of PATH, so a
550
+ bare "bash" would run the command in another OS. GSD_PATH_BASH overrides.
551
+ """
552
+ if os.name != "nt":
553
+ return "bash"
554
+ override = os.environ.get("GSD_PATH_BASH")
555
+ if override:
556
+ if not os.path.isfile(override):
557
+ raise ShellNotFound(f"GSD_PATH_BASH is not a file: {override}")
558
+ return override
559
+ candidates = []
560
+ found = shutil.which("bash")
561
+ if found:
562
+ candidates.append(found)
563
+ git = shutil.which("git")
564
+ if git:
565
+ # <Git>\cmd\git.exe or <Git>\bin\git.exe -> <Git>\bin\bash.exe
566
+ candidates.append(os.path.join(os.path.dirname(os.path.dirname(git)), "bin", "bash.exe"))
567
+ for variable in ("ProgramFiles", "ProgramW6432", "LOCALAPPDATA"):
568
+ base = os.environ.get(variable)
569
+ if base:
570
+ parts = ("Programs", "Git") if variable == "LOCALAPPDATA" else ("Git",)
571
+ candidates.append(os.path.join(base, *parts, "bin", "bash.exe"))
572
+ for candidate in candidates:
573
+ if os.path.isfile(candidate) and not _is_wsl_launcher(candidate):
574
+ return candidate
575
+ raise ShellNotFound(
576
+ "Verify commands need bash; install Git for Windows or set GSD_PATH_BASH "
577
+ "to its bin\\bash.exe (the System32 WSL launcher is never used)"
578
+ )
579
+
580
+
581
+ def bash_argv(command: str) -> List[str]:
582
+ return [find_bash(), "-c", command]
583
+
584
+
585
+ # cmd.exe re-parses the arguments of a .cmd or .bat target, so these would
586
+ # let an argument escape its quoting.
587
+ _CMD_METACHARACTERS = frozenset('%^&|<>"!\r\n')
588
+
589
+
590
+ def resolve_argv(argv: Sequence[str]) -> List[str]:
591
+ """argv with its program resolved the way a shell would find it.
592
+
593
+ Windows CreateProcess only appends .exe, so npm-installed CLIs such as
594
+ claude.cmd or codex.cmd are found through PATHEXT here. POSIX is unchanged.
595
+ """
596
+ arguments = list(argv)
597
+ if os.name != "nt" or not arguments:
598
+ return arguments
599
+ program = shutil.which(arguments[0])
600
+ if program is None:
601
+ raise FileNotFoundError(f"command not found: {arguments[0]}")
602
+ if program.lower().endswith((".cmd", ".bat")):
603
+ for argument in arguments[1:]:
604
+ if _CMD_METACHARACTERS.intersection(argument):
605
+ raise ValueError(
606
+ f"argument {argument!r} is unsafe to pass to {program}; "
607
+ "cmd.exe would reinterpret it"
608
+ )
609
+ return [program, *arguments[1:]]
610
+
611
+
612
+ def split_command(text: str) -> List[str]:
613
+ """Split a command string with POSIX quoting.
614
+
615
+ Agents write these in a POSIX shell (Git Bash on Windows), so single and
616
+ double quotes group as in sh. On Windows backslash is not an escape, so a
617
+ path such as C:\\Python312\\python.exe keeps its separators.
618
+ """
619
+ if os.name != "nt":
620
+ return shlex.split(text)
621
+ lexer = shlex.shlex(text, posix=True)
622
+ lexer.whitespace_split = True
623
+ lexer.escape = ""
624
+ lexer.commenters = ""
625
+ return list(lexer)
626
+
627
+
628
+ def rmtree_force(path: Path) -> None:
629
+ """shutil.rmtree that also removes read-only files, such as git objects on Windows."""
630
+
631
+ def clear_readonly(function, target, _error) -> None:
632
+ os.chmod(target, 0o700)
633
+ function(target)
634
+
635
+ if sys.version_info >= (3, 12):
636
+ shutil.rmtree(path, onexc=clear_readonly)
637
+ else:
638
+ shutil.rmtree(path, onerror=clear_readonly)
@@ -16,14 +16,20 @@ from pathlib import Path
16
16
  from typing import Iterator, Optional, Sequence
17
17
 
18
18
  def _load_pipeline_modules():
19
- try:
20
- import archive_milestone
21
- import pipeline_state
22
- import _common
19
+ if __package__:
20
+ from . import archive_milestone
21
+ from . import pipeline_state
22
+ from . import _common
23
23
  return _common, archive_milestone, pipeline_state
24
- except ModuleNotFoundError as error:
25
- if error.name not in {"archive_milestone", "pipeline_state", "_common"}:
26
- raise
24
+ else:
25
+ try:
26
+ import archive_milestone
27
+ import pipeline_state
28
+ import _common
29
+ return _common, archive_milestone, pipeline_state
30
+ except ModuleNotFoundError as error:
31
+ if error.name not in {"archive_milestone", "pipeline_state", "_common"}:
32
+ raise
27
33
  try:
28
34
  from scripts import archive_milestone, pipeline_state, _common
29
35
  return _common, archive_milestone, pipeline_state
@@ -111,7 +117,7 @@ def record_count(directory: Path) -> int:
111
117
  def validate_or_empty(directory: Path) -> None:
112
118
  if directory.is_symlink() or not directory.is_dir():
113
119
  raise DiscussionError("discussion records must be a real directory")
114
- if sorted(path.name for path in directory.iterdir()) != sorted(FILES):
120
+ if sorted(path.name for path in directory.iterdir() if not _common.is_ignored_junk(path)) != sorted(FILES):
115
121
  raise DiscussionError("discussion directory must contain exactly DIALOGUE.md and ANSWERS.md")
116
122
  if record_count(directory) == 0:
117
123
  if "### D" in (directory / "DIALOGUE.md").read_text(encoding="utf-8"):
@@ -170,7 +176,7 @@ def validate_contents(project: Path, files: dict) -> None:
170
176
  staging.mkdir()
171
177
  try:
172
178
  for name in FILES:
173
- (staging / name).write_text(files[name], encoding="utf-8")
179
+ (staging / name).write_bytes(files[name].encode("utf-8"))
174
180
  validate_or_empty(staging)
175
181
  finally:
176
182
  shutil.rmtree(staging)
@@ -187,11 +193,11 @@ def initialize_records(discussion: Path, dialogue_template: Path, answers_templa
187
193
  shutil.rmtree(staging)
188
194
  staging.mkdir()
189
195
  try:
190
- (staging / "DIALOGUE.md").write_text(
191
- template_header(dialogue_template, "### D001"), encoding="utf-8"
196
+ (staging / "DIALOGUE.md").write_bytes(
197
+ template_header(dialogue_template, "### D001").encode("utf-8")
192
198
  )
193
- (staging / "ANSWERS.md").write_text(
194
- template_header(answers_template, "## Answer A001"), encoding="utf-8"
199
+ (staging / "ANSWERS.md").write_bytes(
200
+ template_header(answers_template, "## Answer A001").encode("utf-8")
195
201
  )
196
202
  os.replace(staging, discussion)
197
203
  finally:
@@ -32,10 +32,10 @@ from datetime import datetime, timedelta, timezone
32
32
  from pathlib import Path
33
33
  from typing import Iterator, Optional, Sequence
34
34
 
35
- if sys.platform == "win32":
36
- import msvcrt
37
- else:
38
- import fcntl
35
+ try:
36
+ from . import _common
37
+ except ImportError:
38
+ import _common
39
39
 
40
40
 
41
41
  class LoopError(RuntimeError):
@@ -119,25 +119,8 @@ def log_path(fields: dict) -> Path:
119
119
  def log_lock(fields: dict) -> Iterator[None]:
120
120
  path = log_path(fields)
121
121
  path.parent.mkdir(parents=True, exist_ok=True)
122
- lock_path = path.with_name(f".{path.name}.lock")
123
- with lock_path.open("a+b") as handle:
124
- if sys.platform == "win32":
125
- handle.seek(0, os.SEEK_END)
126
- if handle.tell() == 0:
127
- handle.write(b"\0")
128
- handle.flush()
129
- handle.seek(0)
130
- msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
131
- else:
132
- fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
133
- try:
134
- yield
135
- finally:
136
- if sys.platform == "win32":
137
- handle.seek(0)
138
- msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
139
- else:
140
- fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
122
+ with _common.exclusive_lock(path.with_name(f".{path.name}.lock")):
123
+ yield
141
124
 
142
125
 
143
126
  def load_log(fields: dict) -> list[dict]:
@@ -528,15 +511,22 @@ def command_claim(fields: dict) -> dict:
528
511
 
529
512
 
530
513
  def run_shell(command: str, timeout: float) -> tuple[Optional[int], str]:
531
- """Run one spec command; a hung command counts as failed (exit None)."""
514
+ """Run one spec command; a hung command counts as failed (exit None).
515
+
516
+ POSIX runs /bin/sh as shell=True did; Windows runs Git for Windows bash, not
517
+ cmd.exe. A timeout kills the whole process group, so a grandchild cannot
518
+ keep the output pipes open.
519
+ """
520
+ argv = _common.bash_argv(command) if os.name == "nt" else ["/bin/sh", "-c", command]
521
+ process = _common.popen_group(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
532
522
  try:
533
- completed = subprocess.run(
534
- command, shell=True, capture_output=True, text=True, check=False, timeout=timeout
535
- )
536
- except subprocess.TimeoutExpired as expired:
537
- output = (expired.stdout or b"").decode(errors="replace") + (expired.stderr or b"").decode(errors="replace")
523
+ stdout, stderr = process.communicate(timeout=timeout)
524
+ except subprocess.TimeoutExpired:
525
+ _common.kill_tree(process)
526
+ stdout, stderr = process.communicate()
527
+ output = stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace")
538
528
  return None, f"{output}\ntimed out at the admitted wall-clock deadline"
539
- return completed.returncode, completed.stdout + completed.stderr
529
+ return process.returncode, (stdout + stderr).decode("utf-8", errors="replace")
540
530
 
541
531
 
542
532
  def command_verify(fields: dict, claim_identifier: Optional[str]) -> dict: