@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
@@ -12,10 +12,13 @@ sys.dont_write_bytecode = True
12
12
  from pathlib import Path, PurePosixPath
13
13
  from typing import Dict, List, Optional, Sequence, Set, Tuple
14
14
 
15
- try:
16
- import _common
17
- except ImportError: # pragma: no cover - package import used by tests
18
- from scripts import _common
15
+ if __package__:
16
+ from . import _common
17
+ else:
18
+ try:
19
+ import _common
20
+ except ImportError: # pragma: no cover - package import used by tests
21
+ from scripts import _common
19
22
 
20
23
 
21
24
  DEFAULT_TASKS_DIR = ".project/tasks"
@@ -183,10 +186,47 @@ def _verify_tokens(block: str) -> List[str]:
183
186
  return tokens
184
187
 
185
188
 
189
+ def _member_bases(root: Path):
190
+ """Resolve a member name to (checkout, origin/main SHA), reading MEMBERS.md once."""
191
+ cache: Dict[str, Optional[Tuple[Path, str]]] = {}
192
+
193
+ def locate(name: str) -> Optional[Tuple[Path, str]]:
194
+ if name not in cache:
195
+ if __package__:
196
+ from . import members
197
+ else:
198
+ try:
199
+ import members
200
+ except ImportError: # pragma: no cover - package import used by tests
201
+ from scripts import members
202
+ try:
203
+ listed = {member["name"]: Path(member["checkout"]) for member in members.read_members(root)}
204
+ checkout = listed.get(name)
205
+ if checkout is None:
206
+ cache[name] = None
207
+ else:
208
+ # A build lock selects the bound tip, including during Plan recovery;
209
+ # without the lock, use origin/main before the first build start.
210
+ lock = root / members.LOCK_PATH
211
+ locked = [entry for entry in (json.loads(lock.read_text(encoding="utf-8"))["members"]
212
+ if lock.is_file() else []) if entry["name"] == name]
213
+ if locked:
214
+ cache[name] = (checkout, _resolve_base(checkout, f"refs/heads/{locked[0]['branch']}"))
215
+ else:
216
+ members.require_origin_main(checkout)
217
+ cache[name] = (checkout, _resolve_base(checkout, "refs/remotes/origin/main"))
218
+ except (members.MembersError, BriefError) as error:
219
+ raise BriefError(f"member {name} is unavailable: {error}") from error
220
+ return cache[name]
221
+
222
+ return locate
223
+
224
+
186
225
  def _lint_task(
187
226
  repo: Path, base: str, path: Path,
188
227
  dependency_files: Optional[Dict[str, Set[str]]] = None,
189
228
  landed_bases: Optional[Dict[str, str]] = None,
229
+ member_base=None,
190
230
  ) -> Tuple[str, List[str], Optional[str], int]:
191
231
  text = path.read_text(encoding="utf-8")
192
232
  fields, error = _frontmatter(text)
@@ -198,6 +238,15 @@ def _lint_task(
198
238
  checked = 0
199
239
  if fields is None:
200
240
  return task_id, [error or "invalid frontmatter"], None, checked
241
+ # A member task's paths resolve in the member at its origin/main, where the
242
+ # member bound branch starts; `repo:` absent means the coordinator.
243
+ git_root = repo
244
+ member = fields.get("repo")
245
+ if member is not None:
246
+ located = member_base(member) if isinstance(member, str) and member and member_base else None
247
+ if located is None:
248
+ return task_id, [f"repo: names no member in MEMBERS.md: {member}"], None, checked
249
+ git_root, base = located[0], (landed_bases or {}).get(task_id, located[1])
201
250
 
202
251
  for field in REQUIRED_FIELDS:
203
252
  if field not in fields:
@@ -237,7 +286,7 @@ def _lint_task(
237
286
  for parent in candidate.parents:
238
287
  if str(parent) == ".":
239
288
  break
240
- kind = _run_git(repo, "cat-file", "-t", f"{base}:{parent}")
289
+ kind = _run_git(git_root, "cat-file", "-t", f"{base}:{parent}")
241
290
  if kind.returncode == 0 and kind.stdout.strip() != "tree":
242
291
  problems.append(
243
292
  f"files entry {normalized} has non-directory ancestor "
@@ -255,7 +304,7 @@ def _lint_task(
255
304
  token not in declared
256
305
  and token not in supplied
257
306
  and not _supplied_contract(repo, path, token)
258
- and not _base_exists(repo, base, token)
307
+ and not _base_exists(git_root, base, token)
259
308
  ):
260
309
  problems.append(f"## {name} names a path missing at the layer base: {token}")
261
310
 
@@ -267,7 +316,7 @@ def _lint_task(
267
316
  else:
268
317
  for token in _verify_tokens(block.group("block")):
269
318
  checked += 1
270
- if token not in declared and token not in supplied and not _base_exists(repo, base, token):
319
+ if token not in declared and token not in supplied and not _base_exists(git_root, base, token):
271
320
  problems.append(f"## Verify names a path missing at the layer base: {token}")
272
321
 
273
322
  contract = None
@@ -284,6 +333,7 @@ def validate_task_briefs(
284
333
  root: Path, base: str, tasks_dir: str = DEFAULT_TASKS_DIR,
285
334
  *, dependency_files: Optional[Dict[str, Set[str]]] = None,
286
335
  landed_bases: Optional[Dict[str, str]] = None,
336
+ member_base=None,
287
337
  ) -> Dict[str, object]:
288
338
  """Lint task briefs using the caller's validation bases.
289
339
 
@@ -305,9 +355,11 @@ def validate_task_briefs(
305
355
  task_files = sorted(tasks_path.glob("*.md"))
306
356
  if not task_files:
307
357
  raise BriefError(f"no task briefs found in {tasks_dir}")
358
+ if member_base is None:
359
+ member_base = _member_bases(root)
308
360
  for path in task_files:
309
361
  task_id, task_problems, contract, task_checked = _lint_task(
310
- root, resolved_base, path, dependency_files, landed_bases
362
+ root, resolved_base, path, dependency_files, landed_bases, member_base
311
363
  )
312
364
  problems.extend(f"{task_id}: {problem}" for problem in task_problems)
313
365
  if contract is not None:
@@ -12,12 +12,14 @@ Verdicts:
12
12
  - owned — `.project/` is a real directory with a regular `STATE.md` and no
13
13
  unsafe project paths; route by that file, not detection
14
14
  - orphan — `.project/` is unsafe, or it contains an entry without a safe
15
- regular `STATE.md`; the initializer's regular temporary state file is
16
- recoverable and does not make the project orphaned
17
- - brownfield — no owned state, `.project/` empty or absent, and at least one
18
- in-scope signal (package/build manifest, source file, tracked signal in
19
- git, or substantive system documentation)
20
- - greenfield — no owned state, `.project/` empty or absent, and no signal
15
+ regular `STATE.md`; the initializer's regular temporary state file and an
16
+ ignored, untracked regular `.DS_Store` are exceptions
17
+ - brownfield — no owned state, `.project/` empty, absent, or containing only
18
+ an ignored `.DS_Store`, and at least one in-scope signal (package/build
19
+ manifest, source file, tracked signal in git, or substantive system
20
+ documentation)
21
+ - greenfield — no owned state, `.project/` empty, absent, or containing only
22
+ an ignored `.DS_Store`, and no signal
21
23
 
22
24
  Ignored path components: `.git`, `node_modules`, vendored/generated trees,
23
25
  build output, and managed GSD Path installation artifacts. `.project/` is
@@ -41,6 +43,11 @@ from datetime import date
41
43
  from pathlib import Path, PurePosixPath, PureWindowsPath
42
44
  from typing import Iterable, NamedTuple, Optional, Sequence
43
45
 
46
+ try:
47
+ from . import _common
48
+ except ImportError:
49
+ import _common
50
+
44
51
  try:
45
52
  import fcntl
46
53
 
@@ -961,6 +968,26 @@ def is_staged_skill_bundle_artifact(
961
968
  )
962
969
 
963
970
 
971
+ def ignored_junk(directory: Path, name: str, status: os.stat_result) -> bool:
972
+ """Ignored, untracked OS junk. The caller's no-follow status decides the file type."""
973
+ if (
974
+ name not in _common.OS_JUNK_NAMES
975
+ or is_link_like_status(status)
976
+ or not stat.S_ISREG(status.st_mode)
977
+ ):
978
+ return False
979
+ try:
980
+ result = subprocess.run(
981
+ ["git", "-C", str(directory), "check-ignore", "-q", "--", name],
982
+ capture_output=True,
983
+ check=False,
984
+ env=git_environment(),
985
+ )
986
+ except OSError:
987
+ return False
988
+ return result.returncode == 0
989
+
990
+
964
991
  def project_path_inventory(
965
992
  project: Path, root: Path
966
993
  ) -> tuple[tuple[str, ...], tuple[str, ...]]:
@@ -1001,14 +1028,18 @@ def project_path_inventory(
1001
1028
  posix_relative(current / name, root) for name in linked_directories
1002
1029
  )
1003
1030
  dirnames[:] = [name for name in dirnames if name not in linked_directories]
1031
+ kept = []
1004
1032
  for name in filenames:
1005
1033
  path = current / name
1006
- relative = posix_relative(path, root)
1007
1034
  status = lstat_evidence(path, missing_ok=False)
1035
+ if ignored_junk(current, name, status):
1036
+ continue
1037
+ kept.append(name)
1038
+ relative = posix_relative(path, root)
1008
1039
  paths.append(relative)
1009
1040
  if is_link_like(path, status) or not stat.S_ISREG(status.st_mode):
1010
1041
  unsafe_paths.append(relative)
1011
- if not filenames and not dirnames and current != project:
1042
+ if not kept and not dirnames and current != project:
1012
1043
  paths.append(posix_relative(current, root))
1013
1044
  if not paths:
1014
1045
  try:
@@ -1017,7 +1048,11 @@ def project_path_inventory(
1017
1048
  raise DetectError(
1018
1049
  f"cannot list project evidence: {project}: {error}"
1019
1050
  ) from error
1020
- paths = [posix_relative(project / name, root) for name in names]
1051
+ paths = [
1052
+ posix_relative(project / name, root)
1053
+ for name in names
1054
+ if not ignored_junk(project, name, lstat_evidence(project / name, missing_ok=False))
1055
+ ]
1021
1056
  return tuple(sorted(paths)), tuple(sorted(unsafe_paths))
1022
1057
 
1023
1058
 
@@ -1093,7 +1128,7 @@ def classify(repo: Path) -> dict:
1093
1128
  missing_ok=True,
1094
1129
  )
1095
1130
  if (
1096
- ANCHORED_STATE_CREATE_SUPPORTED
1131
+ STATE_CREATE_SUPPORTED
1097
1132
  and orphan_paths == (f".project/{STATE_TEMP_NAME}",)
1098
1133
  and not unsafe_paths
1099
1134
  and temporary_state_status is not None
@@ -1265,12 +1300,28 @@ def close_file_descriptors(
1265
1300
  pass
1266
1301
 
1267
1302
 
1303
+ def project_entries(project_fd: int, project: Path) -> set:
1304
+ """Names in .project without ignored OS junk (Finder may add .DS_Store at any time)."""
1305
+ entries = set(os.listdir(project_fd))
1306
+ for name in entries & _common.OS_JUNK_NAMES:
1307
+ try:
1308
+ status = os.stat(name, dir_fd=project_fd, follow_symlinks=False)
1309
+ except FileNotFoundError:
1310
+ continue # Keep the name: the caller's comparison then fails closed.
1311
+ if ignored_junk(project, name, status):
1312
+ entries.discard(name)
1313
+ return entries
1314
+
1315
+
1268
1316
  def write_state_anchored(
1269
1317
  root: Path,
1270
1318
  content: str,
1271
1319
  expected_root: os.stat_result,
1272
1320
  expected_project: Optional[os.stat_result],
1273
1321
  ) -> None:
1322
+ if WINDOWS_STATE_CREATE_SUPPORTED:
1323
+ write_state_windows(root, content, expected_root, expected_project)
1324
+ return
1274
1325
  if not ANCHORED_STATE_CREATE_SUPPORTED:
1275
1326
  raise DetectError("anchored no-follow STATE.md creation is unavailable")
1276
1327
  payload = content.encode("utf-8")
@@ -1333,7 +1384,7 @@ def write_state_anchored(
1333
1384
  ):
1334
1385
  raise DetectError(".project changed after classification")
1335
1386
  lock_exclusive(project_fd)
1336
- entries = set(os.listdir(project_fd))
1387
+ entries = project_entries(project_fd, root / ".project")
1337
1388
  if entries not in (set(), {STATE_TEMP_NAME}):
1338
1389
  raise DetectError(".project changed after classification")
1339
1390
  create = (
@@ -1400,7 +1451,7 @@ def write_state_anchored(
1400
1451
  or not os.path.samestat(created_status, current_state)
1401
1452
  ):
1402
1453
  raise DetectError("temporary STATE.md changed while writing")
1403
- if set(os.listdir(project_fd)) != {STATE_TEMP_NAME}:
1454
+ if project_entries(project_fd, root / ".project") != {STATE_TEMP_NAME}:
1404
1455
  raise DetectError(".project contents changed while creating STATE.md")
1405
1456
  try:
1406
1457
  os.link(
@@ -1420,7 +1471,7 @@ def write_state_anchored(
1420
1471
  published = os.stat("STATE.md", dir_fd=project_fd, follow_symlinks=False)
1421
1472
  if published.st_nlink != 1 or not os.path.samestat(created_status, published):
1422
1473
  raise DetectError("STATE.md changed while publishing")
1423
- if set(os.listdir(project_fd)) != {"STATE.md"}:
1474
+ if project_entries(project_fd, root / ".project") != {"STATE.md"}:
1424
1475
  raise DetectError(".project contents changed while publishing STATE.md")
1425
1476
  os.fsync(project_fd)
1426
1477
  closing_state_fd = state_fd
@@ -1448,6 +1499,285 @@ def write_state_anchored(
1448
1499
  close_file_descriptors(state_fd, project_fd, root_fd)
1449
1500
 
1450
1501
 
1502
+ # --- Windows anchored creation ------------------------------------------------
1503
+ # Windows has no dir_fd, O_NOFOLLOW, or O_DIRECTORY. Handles give the same
1504
+ # guarantee (docs/adr/0004-windows-anchored-state.md):
1505
+ # - each directory is opened without following a reparse point and held
1506
+ # without FILE_SHARE_DELETE, so it cannot be renamed or replaced while held;
1507
+ # - children are opened relative to the held parent handle (NtCreateFile with
1508
+ # a RootDirectory), so no path is resolved again after it was checked;
1509
+ # - STATE.md is published by renaming the temporary file's own handle without
1510
+ # replacing an existing name, and rollback deletes through that handle.
1511
+
1512
+ _FILE_LIST_DIRECTORY = 0x0001
1513
+ _FILE_TRAVERSE = 0x0020
1514
+ _FILE_READ_ATTRIBUTES = 0x0080
1515
+ _DELETE = 0x00010000
1516
+ _SYNCHRONIZE = 0x00100000
1517
+ _GENERIC_WRITE = 0x40000000
1518
+ _GENERIC_READ = 0x80000000
1519
+ _FILE_SHARE_READ = 0x1
1520
+ _FILE_SHARE_WRITE = 0x2
1521
+ _OPEN_EXISTING = 3
1522
+ _FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
1523
+ _FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
1524
+ _FILE_OPEN = 1
1525
+ _FILE_CREATE = 2
1526
+ _FILE_OPEN_IF = 3
1527
+ _FILE_DIRECTORY_FILE = 0x00000001
1528
+ _FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020
1529
+ _FILE_NON_DIRECTORY_FILE = 0x00000040
1530
+ _FILE_OPEN_REPARSE_POINT = 0x00200000
1531
+ _OBJ_CASE_INSENSITIVE = 0x00000040
1532
+ _FILE_RENAME_INFORMATION = 10
1533
+ _FILE_DISPOSITION_INFO = 4
1534
+ _DIRECTORY_ACCESS = _FILE_LIST_DIRECTORY | _FILE_TRAVERSE | _FILE_READ_ATTRIBUTES | _SYNCHRONIZE
1535
+ # Readers and writers may share a held directory; nobody may delete or rename it.
1536
+ _PIN_SHARE = _FILE_SHARE_READ | _FILE_SHARE_WRITE
1537
+
1538
+ WINDOWS_STATE_CREATE_SUPPORTED = os.name == "nt"
1539
+ STATE_CREATE_SUPPORTED = ANCHORED_STATE_CREATE_SUPPORTED or WINDOWS_STATE_CREATE_SUPPORTED
1540
+
1541
+
1542
+ class _WindowsHandles:
1543
+ """kernel32/ntdll calls for the Windows anchored create, bound on first use."""
1544
+
1545
+ def __init__(self) -> None:
1546
+ import ctypes
1547
+ import msvcrt
1548
+ from ctypes import wintypes
1549
+
1550
+ class UnicodeString(ctypes.Structure):
1551
+ _fields_ = [("Length", wintypes.USHORT), ("MaximumLength", wintypes.USHORT),
1552
+ ("Buffer", wintypes.LPWSTR)]
1553
+
1554
+ class ObjectAttributes(ctypes.Structure):
1555
+ _fields_ = [("Length", wintypes.ULONG), ("RootDirectory", wintypes.HANDLE),
1556
+ ("ObjectName", ctypes.POINTER(UnicodeString)),
1557
+ ("Attributes", wintypes.ULONG), ("SecurityDescriptor", ctypes.c_void_p),
1558
+ ("SecurityQualityOfService", ctypes.c_void_p)]
1559
+
1560
+ class IoStatusBlock(ctypes.Structure):
1561
+ _fields_ = [("Status", ctypes.c_void_p), ("Information", ctypes.c_size_t)]
1562
+
1563
+ self.ctypes, self.wintypes, self.msvcrt = ctypes, wintypes, msvcrt
1564
+ self.UnicodeString, self.ObjectAttributes, self.IoStatusBlock = (
1565
+ UnicodeString, ObjectAttributes, IoStatusBlock)
1566
+ self.kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
1567
+ self.ntdll = ctypes.WinDLL("ntdll")
1568
+ self.kernel32.CreateFileW.restype = wintypes.HANDLE
1569
+ self.kernel32.CreateFileW.argtypes = (
1570
+ wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p,
1571
+ wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE)
1572
+ self.kernel32.SetFileInformationByHandle.argtypes = (
1573
+ wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD)
1574
+ self.ntdll.NtCreateFile.restype = ctypes.c_long
1575
+ self.ntdll.NtCreateFile.argtypes = (
1576
+ ctypes.POINTER(wintypes.HANDLE), wintypes.DWORD, ctypes.POINTER(ObjectAttributes),
1577
+ ctypes.POINTER(IoStatusBlock), ctypes.c_void_p, wintypes.ULONG, wintypes.ULONG,
1578
+ wintypes.ULONG, wintypes.ULONG, ctypes.c_void_p, wintypes.ULONG)
1579
+ self.ntdll.RtlNtStatusToDosError.restype = wintypes.ULONG
1580
+ self.ntdll.RtlNtStatusToDosError.argtypes = (ctypes.c_long,)
1581
+ self.ntdll.NtSetInformationFile.restype = ctypes.c_long
1582
+ self.ntdll.NtSetInformationFile.argtypes = (
1583
+ wintypes.HANDLE, ctypes.POINTER(IoStatusBlock), ctypes.c_void_p, wintypes.ULONG,
1584
+ ctypes.c_int)
1585
+
1586
+ def adopt(self, handle: int) -> int:
1587
+ """A CRT descriptor that owns handle, so os.fstat, os.write, and os.close apply."""
1588
+ try:
1589
+ descriptor = self.msvcrt.open_osfhandle(handle, 0)
1590
+ except OSError:
1591
+ self.kernel32.CloseHandle(self.wintypes.HANDLE(handle))
1592
+ raise
1593
+ # The CRT adopts handles in text mode, where os.write turns "\n" into "\r\n".
1594
+ self.msvcrt.setmode(descriptor, os.O_BINARY)
1595
+ return descriptor
1596
+
1597
+ def open_root(self, path: Path) -> int:
1598
+ handle = self.kernel32.CreateFileW(
1599
+ str(path), _DIRECTORY_ACCESS, _PIN_SHARE, None, _OPEN_EXISTING,
1600
+ _FILE_FLAG_BACKUP_SEMANTICS | _FILE_FLAG_OPEN_REPARSE_POINT, None)
1601
+ if handle is None or handle == self.wintypes.HANDLE(-1).value:
1602
+ raise self.ctypes.WinError(self.ctypes.get_last_error())
1603
+ return self.adopt(handle)
1604
+
1605
+ def open_relative(
1606
+ self, parent_fd: int, name: str, access: int, share: int, disposition: int, options: int
1607
+ ) -> int:
1608
+ """Open name inside the directory held by parent_fd, never following a reparse point."""
1609
+ ctypes = self.ctypes
1610
+ buffer = ctypes.create_unicode_buffer(name)
1611
+ length = len(name) * ctypes.sizeof(ctypes.c_wchar)
1612
+ object_name = self.UnicodeString(length, length + ctypes.sizeof(ctypes.c_wchar),
1613
+ ctypes.cast(buffer, self.wintypes.LPWSTR))
1614
+ attributes = self.ObjectAttributes(
1615
+ ctypes.sizeof(self.ObjectAttributes), self.msvcrt.get_osfhandle(parent_fd),
1616
+ ctypes.pointer(object_name), _OBJ_CASE_INSENSITIVE, None, None)
1617
+ handle = self.wintypes.HANDLE()
1618
+ status = self.ntdll.NtCreateFile(
1619
+ ctypes.byref(handle), access | _SYNCHRONIZE, ctypes.byref(attributes),
1620
+ ctypes.byref(self.IoStatusBlock()), None, 0, share, disposition,
1621
+ options | _FILE_SYNCHRONOUS_IO_NONALERT | _FILE_OPEN_REPARSE_POINT, None, 0)
1622
+ if status < 0:
1623
+ raise ctypes.WinError(self.ntdll.RtlNtStatusToDosError(status))
1624
+ return self.adopt(handle.value)
1625
+
1626
+ def rename_no_replace(self, fd: int, directory_fd: int, name: str) -> None:
1627
+ """Rename the file held by fd to name inside the directory held by directory_fd.
1628
+
1629
+ The renameat equivalent; fails when name already exists.
1630
+ """
1631
+ ctypes, wintypes = self.ctypes, self.wintypes
1632
+
1633
+ class RenameInformation(ctypes.Structure):
1634
+ _fields_ = [("ReplaceIfExists", wintypes.BOOLEAN), ("RootDirectory", wintypes.HANDLE),
1635
+ ("FileNameLength", wintypes.ULONG),
1636
+ ("FileName", wintypes.WCHAR * (len(name) + 1))]
1637
+
1638
+ info = RenameInformation(False, self.msvcrt.get_osfhandle(directory_fd),
1639
+ len(name) * ctypes.sizeof(ctypes.c_wchar), name)
1640
+ status = self.ntdll.NtSetInformationFile(
1641
+ self.msvcrt.get_osfhandle(fd), ctypes.byref(self.IoStatusBlock()),
1642
+ ctypes.byref(info), ctypes.sizeof(info), _FILE_RENAME_INFORMATION)
1643
+ if status < 0:
1644
+ raise ctypes.WinError(self.ntdll.RtlNtStatusToDosError(status))
1645
+
1646
+ def delete_on_close(self, fd: int) -> None:
1647
+ """Delete exactly the file held by fd when it closes, whatever its name is now."""
1648
+ ctypes = self.ctypes
1649
+ flag = ctypes.c_ubyte(1)
1650
+ if not self.kernel32.SetFileInformationByHandle(
1651
+ self.msvcrt.get_osfhandle(fd), _FILE_DISPOSITION_INFO,
1652
+ ctypes.byref(flag), ctypes.sizeof(flag)):
1653
+ raise ctypes.WinError(ctypes.get_last_error())
1654
+
1655
+
1656
+ def windows_project_entries(project: Path) -> set:
1657
+ """project_entries for a .project held open without delete sharing."""
1658
+ entries = set(os.listdir(project))
1659
+ for name in entries & _common.OS_JUNK_NAMES:
1660
+ try:
1661
+ status = os.stat(project / name, follow_symlinks=False)
1662
+ except FileNotFoundError:
1663
+ continue
1664
+ if ignored_junk(project, name, status):
1665
+ entries.discard(name)
1666
+ return entries
1667
+
1668
+
1669
+ def write_state_windows(
1670
+ root: Path,
1671
+ content: str,
1672
+ expected_root: os.stat_result,
1673
+ expected_project: Optional[os.stat_result],
1674
+ ) -> None:
1675
+ win = _WindowsHandles()
1676
+ payload = content.encode("utf-8")
1677
+ project = root / ".project"
1678
+ root_fd = project_fd = state_fd = None
1679
+ delete_on_failure = False
1680
+ # The same lock pipeline_state takes for this track.
1681
+ with _common.directory_mutex(project):
1682
+ try:
1683
+ try:
1684
+ root_fd = win.open_root(root)
1685
+ except OSError as error:
1686
+ raise DetectError(f"cannot open repo for STATE.md creation: {error}") from error
1687
+ opened_root = os.fstat(root_fd)
1688
+ if (
1689
+ is_link_like(root, opened_root)
1690
+ or not stat.S_ISDIR(opened_root.st_mode)
1691
+ or not os.path.samestat(expected_root, opened_root)
1692
+ ):
1693
+ raise DetectError("repo changed after classification")
1694
+ try:
1695
+ project_fd = win.open_relative(
1696
+ root_fd, ".project", _DIRECTORY_ACCESS, _PIN_SHARE,
1697
+ _FILE_CREATE if expected_project is None else _FILE_OPEN,
1698
+ _FILE_DIRECTORY_FILE)
1699
+ except FileExistsError as error:
1700
+ raise DetectError(".project changed after classification") from error
1701
+ except OSError as error:
1702
+ raise DetectError(f"cannot open .project for STATE.md creation: {error}") from error
1703
+ opened_project = os.fstat(project_fd)
1704
+ if is_link_like(project, opened_project) or not stat.S_ISDIR(opened_project.st_mode):
1705
+ raise DetectError("cannot create state through a non-directory .project")
1706
+ if expected_project is not None and not os.path.samestat(expected_project, opened_project):
1707
+ raise DetectError(".project changed after classification")
1708
+ if windows_project_entries(project) not in (set(), {STATE_TEMP_NAME}):
1709
+ raise DetectError(".project changed after classification")
1710
+ try:
1711
+ # Share mode 0: nothing else may open the temporary file while it is held.
1712
+ state_fd = win.open_relative(
1713
+ project_fd, STATE_TEMP_NAME, _GENERIC_READ | _GENERIC_WRITE | _DELETE,
1714
+ 0, _FILE_OPEN_IF, _FILE_NON_DIRECTORY_FILE)
1715
+ except OSError as error:
1716
+ raise DetectError(f"cannot reserve temporary STATE.md: {error}") from error
1717
+ created_status = os.fstat(state_fd)
1718
+ if is_link_like(project / STATE_TEMP_NAME, created_status) or not stat.S_ISREG(
1719
+ created_status.st_mode
1720
+ ):
1721
+ raise DetectError("temporary STATE.md is not a regular file")
1722
+ if created_status.st_nlink != 1:
1723
+ raise DetectError("temporary STATE.md has an unexpected link count")
1724
+ delete_on_failure = True
1725
+ if "STATE.md" in os.listdir(project):
1726
+ raise DetectError("STATE.md already exists")
1727
+ os.ftruncate(state_fd, 0)
1728
+ write_all(state_fd, payload)
1729
+ os.fsync(state_fd)
1730
+ current_root = lstat_evidence(root, missing_ok=False)
1731
+ current_project = lstat_evidence(project, missing_ok=False)
1732
+ if (
1733
+ is_link_like(root, current_root)
1734
+ or not os.path.samestat(opened_root, current_root)
1735
+ or is_link_like(project, current_project)
1736
+ or not os.path.samestat(opened_project, current_project)
1737
+ ):
1738
+ raise DetectError("project identity changed while creating STATE.md")
1739
+ current_state = os.fstat(state_fd)
1740
+ if current_state.st_nlink != 1 or not os.path.samestat(created_status, current_state):
1741
+ raise DetectError("temporary STATE.md changed while writing")
1742
+ if windows_project_entries(project) != {STATE_TEMP_NAME}:
1743
+ raise DetectError(".project contents changed while creating STATE.md")
1744
+ try:
1745
+ win.rename_no_replace(state_fd, project_fd, "STATE.md")
1746
+ except OSError as error:
1747
+ if "STATE.md" in os.listdir(project):
1748
+ raise DetectError("STATE.md already exists") from error
1749
+ raise DetectError(f"cannot publish STATE.md: {error}") from error
1750
+ published = lstat_evidence(project / "STATE.md", missing_ok=False)
1751
+ if (
1752
+ is_link_like(project / "STATE.md", published)
1753
+ or published.st_nlink != 1
1754
+ or not os.path.samestat(created_status, published)
1755
+ ):
1756
+ raise DetectError("STATE.md changed while publishing")
1757
+ if windows_project_entries(project) != {"STATE.md"}:
1758
+ raise DetectError(".project contents changed while publishing STATE.md")
1759
+ delete_on_failure = False
1760
+ closing_state_fd, state_fd = state_fd, None
1761
+ try:
1762
+ os.close(closing_state_fd)
1763
+ except OSError as error:
1764
+ raise DetectError(f"cannot close STATE.md: {error}") from error
1765
+ except BaseException as error:
1766
+ if delete_on_failure and state_fd is not None:
1767
+ try:
1768
+ win.delete_on_close(state_fd)
1769
+ except OSError as rollback_error:
1770
+ raise DetectError(
1771
+ f"cannot roll back STATE.md: {rollback_error}") from error
1772
+ if isinstance(error, DetectError):
1773
+ raise
1774
+ if isinstance(error, OSError):
1775
+ raise DetectError(f"cannot create STATE.md: {error}") from error
1776
+ raise
1777
+ finally:
1778
+ close_file_descriptors(state_fd, project_fd, root_fd)
1779
+
1780
+
1451
1781
  def initialize(
1452
1782
  repo: Path, template: Path, phase: Optional[str] = None, *, require_git: bool = False
1453
1783
  ) -> dict:
@@ -1471,7 +1801,7 @@ def initialize(
1471
1801
  if require_git:
1472
1802
  git = subprocess.run(
1473
1803
  ["git", "-C", str(root), "rev-parse", "--is-inside-work-tree"],
1474
- env=git_environment(), capture_output=True, text=True, check=False,
1804
+ env=git_environment(), capture_output=True, encoding="utf-8", errors="replace", check=False,
1475
1805
  )
1476
1806
  if git.returncode != 0 or git.stdout.strip() != "true":
1477
1807
  return {**payload, "route": "setup-repository", "wrote_state": False}
@@ -1482,7 +1812,7 @@ def initialize(
1482
1812
  raise DetectError(
1483
1813
  f"initialize phase {phase} does not match verdict {payload['verdict']}"
1484
1814
  )
1485
- if not ANCHORED_STATE_CREATE_SUPPORTED:
1815
+ if not STATE_CREATE_SUPPORTED:
1486
1816
  payload["wrote_state"] = False
1487
1817
  payload["error"] = "anchored no-follow STATE.md creation is unavailable"
1488
1818
  return payload
@@ -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: