@opengsd/gsd-path 1.3.1 → 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 +281 -0
  3. package/DOCS.md +13 -7
  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 +57 -8
  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 +54 -38
  31. package/scripts/install.py +562 -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 +35 -9
  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,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:
@@ -14,12 +14,16 @@ from datetime import date
14
14
  from pathlib import Path, PurePosixPath
15
15
  from typing import Optional, Sequence
16
16
 
17
- try:
18
- from pipeline_state import PipelineState
19
- import _common
20
- except ImportError: # pragma: no cover - package import used by tests
21
- from scripts.pipeline_state import PipelineState
22
- from scripts import _common
17
+ if __package__:
18
+ from .pipeline_state import PipelineState
19
+ from . import _common
20
+ else:
21
+ try:
22
+ from pipeline_state import PipelineState
23
+ import _common
24
+ except ImportError: # pragma: no cover - package import used by tests
25
+ from scripts.pipeline_state import PipelineState
26
+ from scripts import _common
23
27
 
24
28
 
25
29
  atomic_replace = _common.atomic_replace
@@ -105,7 +109,7 @@ def validate_discussion_directory(
105
109
  ) -> None:
106
110
  if directory.is_symlink() or not directory.is_dir():
107
111
  raise ArchiveError("discussion archive must be a real directory")
108
- entries = sorted(path.name for path in directory.iterdir())
112
+ entries = sorted(path.name for path in directory.iterdir() if not _common.is_ignored_junk(path))
109
113
  if entries != sorted(DISCUSSION_FILES):
110
114
  raise ArchiveError("discussion archive must contain exactly DIALOGUE.md and ANSWERS.md")
111
115
  dialogue, answers = (directory / name for name in DISCUSSION_FILES)
@@ -490,10 +494,13 @@ def archived_intent_criteria(archive: Path) -> dict[int, str]:
490
494
  if not archive_milestone.is_real_file(intent):
491
495
  raise ArchiveError("archived intent must be a real INTENT.md file")
492
496
  text = HTML_COMMENT_PATTERN.sub("", intent.read_text(encoding="utf-8"))
493
- try:
494
- import check_handoffs
495
- except ImportError:
496
- from scripts import check_handoffs
497
+ if __package__:
498
+ from . import check_handoffs
499
+ else:
500
+ try:
501
+ import check_handoffs
502
+ except ImportError:
503
+ from scripts import check_handoffs
497
504
  try:
498
505
  criteria = check_handoffs._success_criteria(text)
499
506
  except check_handoffs.HandoffError as error:
@@ -502,10 +509,13 @@ def archived_intent_criteria(archive: Path) -> dict[int, str]:
502
509
 
503
510
 
504
511
  def parse_final_review(archive: Path) -> tuple:
505
- try:
506
- import check_handoffs
507
- except ImportError: # pragma: no cover - package import used by tests
508
- from scripts import check_handoffs
512
+ if __package__:
513
+ from . import check_handoffs
514
+ else:
515
+ try:
516
+ import check_handoffs
517
+ except ImportError: # pragma: no cover - package import used by tests
518
+ from scripts import check_handoffs
509
519
 
510
520
  final = archive / "review" / "FINAL.md"
511
521
  if not archive_milestone.is_real_file(final):
@@ -597,8 +607,29 @@ def parse_final_review(archive: Path) -> tuple:
597
607
  return reviewed_head.lower(), criteria
598
608
 
599
609
 
610
+ def _member_heads(text: str, source: str) -> dict:
611
+ if __package__:
612
+ from . import check_handoffs
613
+ else:
614
+ try:
615
+ import check_handoffs
616
+ except ImportError: # pragma: no cover - package import used by tests
617
+ from scripts import check_handoffs
618
+ try:
619
+ return check_handoffs.member_reviewed_heads(text, source)
620
+ except check_handoffs.HandoffError as error:
621
+ raise ArchiveError(str(error)) from error
622
+
623
+
600
624
  def validate_gap_reviews(archive: Path, reviewed_head: str) -> None:
601
625
  review = archive / "review"
626
+ member_heads = _member_heads((review / "FINAL.md").read_text(encoding="utf-8"), "FINAL.md")
627
+ lock = archive / "build" / "members.json"
628
+ locked = [item["name"] for item in json.loads(lock.read_text(encoding="utf-8"))["members"]] if lock.is_file() else []
629
+ if sorted(member_heads) != sorted(locked):
630
+ raise ArchiveError(
631
+ "FINAL.md Member reviewed HEAD lines must name each locked member: " + (", ".join(locked) or "none")
632
+ )
602
633
  candidates = sorted(path for path in review.iterdir() if path.name.startswith("final-gap-"))
603
634
  if not candidates:
604
635
  raise ArchiveError("final review requires at least one final-gap-N.md artifact")
@@ -626,6 +657,8 @@ def validate_gap_reviews(archive: Path, reviewed_head: str) -> None:
626
657
  gap_head = completed_field(lines, "Reviewed HEAD:", path.name).lower()
627
658
  if gap_head != reviewed_head:
628
659
  raise ArchiveError(f"{path.name} Reviewed HEAD does not match FINAL.md")
660
+ if _member_heads("\n".join(lines), path.name) != member_heads:
661
+ raise ArchiveError(f"{path.name} Member reviewed HEAD lines do not match FINAL.md")
629
662
  if completed_field(lines, "Gap verdict:", path.name) != "pass":
630
663
  raise ArchiveError(f"{path.name} gap review did not pass")
631
664
  risk = completed_field(lines, "Risk:", path.name)
@@ -655,10 +688,13 @@ def validate_gap_reviews(archive: Path, reviewed_head: str) -> None:
655
688
 
656
689
 
657
690
  def meaningful_review_evidence(lines: Sequence[str], marker: str, task_text: str = "") -> bool:
658
- try:
659
- from check_handoffs import task_review_observation
660
- except ImportError: # pragma: no cover - package import used by tests
661
- from scripts.check_handoffs import task_review_observation
691
+ if __package__:
692
+ from .check_handoffs import task_review_observation
693
+ else:
694
+ try:
695
+ from check_handoffs import task_review_observation
696
+ except ImportError: # pragma: no cover - package import used by tests
697
+ from scripts.check_handoffs import task_review_observation
662
698
 
663
699
  prefix = f"- {marker} "
664
700
  evidence = []
@@ -1071,8 +1107,17 @@ def archive_file_inventory(archive: Path) -> Sequence[str]:
1071
1107
  for path in archive.rglob("*"):
1072
1108
  if path.is_symlink():
1073
1109
  raise ArchiveError(f"archive contents must not be symlinks: {path}")
1074
- if path.is_file() and path.name != "MANIFEST.md":
1075
- contents.append(path.relative_to(archive).as_posix())
1110
+ if not path.is_file() or path.name == "MANIFEST.md":
1111
+ continue
1112
+ relative = path.relative_to(archive)
1113
+ if _common.is_ignored_junk(path):
1114
+ if len(relative.parts) == 1:
1115
+ continue
1116
+ original = archive.parent.parent / relative
1117
+ project = archive.parent.parent.parent
1118
+ if run_git(project, "check-ignore", "-q", "--", original.relative_to(project).as_posix()).returncode == 0:
1119
+ continue
1120
+ contents.append(relative.as_posix())
1076
1121
  return sorted(contents)
1077
1122
 
1078
1123