@opengsd/gsd-path 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. package/AGENTS.md +2 -207
  2. package/CHANGELOG.md +331 -69
  3. package/DOCS.md +25 -9
  4. package/FULL.md +12 -5
  5. package/GUIDE.md +1 -1
  6. package/HOOKS.md +104 -14
  7. package/QUICK.md +5 -2
  8. package/README.md +131 -111
  9. package/RUNTIME.md +2 -2
  10. package/UPDATE.md +61 -11
  11. package/WORKFLOW.md +44 -7
  12. package/package.json +13 -9
  13. package/platforms/shared-agents/dispatch.md +13 -0
  14. package/scripts/_common.py +619 -18
  15. package/scripts/archive_milestone.py +160 -86
  16. package/scripts/bootstrap_repository.py +8 -5
  17. package/scripts/build_recovery.py +100 -32
  18. package/scripts/build_state.py +143 -24
  19. package/scripts/check_docs_audit.py +91 -7
  20. package/scripts/check_handoffs.py +662 -50
  21. package/scripts/check_task_briefs.py +338 -30
  22. package/scripts/check_update.py +2 -2
  23. package/scripts/core_hook_gate.py +10 -1
  24. package/scripts/core_hook_settings.py +11 -4
  25. package/scripts/detect_project.py +587 -32
  26. package/scripts/discussion_records.py +19 -13
  27. package/scripts/discussion_validate.py +152 -50
  28. package/scripts/dispatch_driver.py +501 -46
  29. package/scripts/git_guard.py +118 -31
  30. package/scripts/guard_hook.py +1567 -194
  31. package/scripts/install.mjs +77 -46
  32. package/scripts/install.py +611 -50
  33. package/scripts/integration.py +580 -25
  34. package/scripts/isolation.py +1438 -57
  35. package/scripts/lean_verification.py +99 -19
  36. package/scripts/loop_run.py +31 -29
  37. package/scripts/members.py +706 -0
  38. package/scripts/migrate_core.py +2 -2
  39. package/scripts/path_config.py +5 -5
  40. package/scripts/pipeline_diagnose.py +82 -31
  41. package/scripts/pipeline_git.py +124 -34
  42. package/scripts/pipeline_state.py +287 -87
  43. package/scripts/pipeline_undo.py +218 -30
  44. package/scripts/promote_lookahead.py +1 -1
  45. package/scripts/review_panel.py +1 -1
  46. package/scripts/runtime_store.py +55 -11
  47. package/scripts/skill-resources.json +37 -5
  48. package/scripts/state_checkpoint.py +135 -44
  49. package/scripts/state_promote.py +2 -0
  50. package/scripts/status_runtime.py +58 -10
  51. package/scripts/sync_skill_resources.py +8 -7
  52. package/scripts/task_context.py +12 -3
  53. package/scripts/token_budget.py +12 -8
  54. package/scripts/wizard.mjs +1 -0
  55. package/scripts/workflow_run.py +107 -30
  56. package/scripts/worktree_paths.py +72 -10
  57. package/skills/gsd-path/BUILD.md +7 -1
  58. package/skills/gsd-path/DECIDE.md +11 -4
  59. package/skills/gsd-path/DEFINE.md +36 -4
  60. package/skills/gsd-path/DOCS-AUDIT.md +6 -4
  61. package/skills/gsd-path/INSPECT.md +5 -4
  62. package/skills/gsd-path/PLAN.md +18 -4
  63. package/skills/gsd-path/RESEARCH.md +13 -1
  64. package/skills/gsd-path/ROADMAP.md +2 -1
  65. package/skills/gsd-path/SHIP.md +33 -11
  66. package/skills/gsd-path/SKILL.md +45 -1
  67. package/skills/gsd-path/references/build-native.md +63 -15
  68. package/skills/gsd-path/references/codebase-mapper.md +2 -1
  69. package/skills/gsd-path/references/dispatch.md +13 -0
  70. package/skills/gsd-path/references/docs-auditor.md +7 -1
  71. package/skills/gsd-path/references/operating-rules.md +214 -0
  72. package/skills/gsd-path/references/planner.md +22 -4
  73. package/skills/gsd-path/references/reviewer-final.md +20 -2
  74. package/skills/gsd-path/references/reviewer-wave.md +18 -5
  75. package/skills/gsd-path/references/reviewer.md +3 -1
  76. package/skills/gsd-path/references/spec-probes.md +150 -0
  77. package/skills/gsd-path/scripts/_common.py +619 -18
  78. package/skills/gsd-path/scripts/archive_milestone.py +160 -86
  79. package/skills/gsd-path/scripts/bootstrap_repository.py +8 -5
  80. package/skills/gsd-path/scripts/build_recovery.py +100 -32
  81. package/skills/gsd-path/scripts/build_state.py +143 -24
  82. package/skills/gsd-path/scripts/check_docs_audit.py +91 -7
  83. package/skills/gsd-path/scripts/check_handoffs.py +662 -50
  84. package/skills/gsd-path/scripts/check_task_briefs.py +338 -30
  85. package/skills/gsd-path/scripts/check_update.py +2 -2
  86. package/skills/gsd-path/scripts/detect_project.py +587 -32
  87. package/skills/gsd-path/scripts/discussion_records.py +19 -13
  88. package/skills/gsd-path/scripts/discussion_validate.py +152 -50
  89. package/skills/gsd-path/scripts/dispatch_driver.py +501 -46
  90. package/skills/gsd-path/scripts/git_guard.py +118 -31
  91. package/skills/gsd-path/scripts/integration.py +580 -25
  92. package/skills/gsd-path/scripts/isolation.py +1438 -57
  93. package/skills/gsd-path/scripts/lean_verification.py +99 -19
  94. package/skills/gsd-path/scripts/loop_run.py +31 -29
  95. package/skills/gsd-path/scripts/members.py +706 -0
  96. package/skills/gsd-path/scripts/path_config.py +5 -5
  97. package/skills/gsd-path/scripts/pipeline_git.py +124 -34
  98. package/skills/gsd-path/scripts/pipeline_state.py +287 -87
  99. package/skills/gsd-path/scripts/promote_lookahead.py +1 -1
  100. package/skills/gsd-path/scripts/review_panel.py +1 -1
  101. package/skills/gsd-path/scripts/state_checkpoint.py +135 -44
  102. package/skills/gsd-path/scripts/state_promote.py +2 -0
  103. package/skills/gsd-path/scripts/status_runtime.py +58 -10
  104. package/skills/gsd-path/scripts/task_context.py +12 -3
  105. package/skills/gsd-path/scripts/token_budget.py +12 -8
  106. package/skills/gsd-path/scripts/workflow_run.py +107 -30
  107. package/skills/gsd-path/scripts/worktree_paths.py +72 -10
  108. package/skills/gsd-path/templates/codebase.md +1 -1
  109. package/skills/gsd-path/templates/docs-audit.md +6 -2
  110. package/skills/gsd-path/templates/final-review.md +16 -4
  111. package/skills/gsd-path/templates/gap-review.md +2 -1
  112. package/skills/gsd-path/templates/intent.md +30 -0
  113. package/skills/gsd-path/templates/patch-findings.md +3 -3
  114. package/skills/gsd-path/templates/plan.md +17 -2
  115. package/skills/gsd-path/templates/research-handoff.md +14 -3
  116. package/skills/gsd-path/templates/state.md +1 -1
  117. package/skills/gsd-path/templates/task.md +1 -0
  118. package/skills/gsd-path/templates/wave-review.md +16 -4
  119. package/skills/gsd-path-build/SKILL.md +7 -1
  120. package/skills/gsd-path-build/references/build-native.md +63 -15
  121. package/skills/gsd-path-build/references/dispatch.md +13 -0
  122. package/skills/gsd-path-build/references/operating-rules.md +214 -0
  123. package/skills/gsd-path-build/references/reviewer-final.md +20 -2
  124. package/skills/gsd-path-build/references/reviewer-wave.md +18 -5
  125. package/skills/gsd-path-build/references/reviewer.md +3 -1
  126. package/skills/gsd-path-build/references/spec-probes.md +150 -0
  127. package/skills/gsd-path-build/scripts/_common.py +619 -18
  128. package/skills/gsd-path-build/scripts/archive_milestone.py +160 -86
  129. package/skills/gsd-path-build/scripts/build_recovery.py +100 -32
  130. package/skills/gsd-path-build/scripts/build_state.py +143 -24
  131. package/skills/gsd-path-build/scripts/check_handoffs.py +662 -50
  132. package/skills/gsd-path-build/scripts/check_task_briefs.py +338 -30
  133. package/skills/gsd-path-build/scripts/detect_project.py +587 -32
  134. package/skills/gsd-path-build/scripts/discussion_records.py +19 -13
  135. package/skills/gsd-path-build/scripts/discussion_validate.py +152 -50
  136. package/skills/gsd-path-build/scripts/dispatch_driver.py +501 -46
  137. package/skills/gsd-path-build/scripts/integration.py +580 -25
  138. package/skills/gsd-path-build/scripts/isolation.py +1438 -57
  139. package/skills/gsd-path-build/scripts/loop_run.py +31 -29
  140. package/skills/gsd-path-build/scripts/members.py +706 -0
  141. package/skills/gsd-path-build/scripts/path_config.py +5 -5
  142. package/skills/gsd-path-build/scripts/pipeline_git.py +124 -34
  143. package/skills/gsd-path-build/scripts/pipeline_state.py +287 -87
  144. package/skills/gsd-path-build/scripts/promote_lookahead.py +1 -1
  145. package/skills/gsd-path-build/scripts/review_panel.py +1 -1
  146. package/skills/gsd-path-build/scripts/state_checkpoint.py +135 -44
  147. package/skills/gsd-path-build/scripts/state_promote.py +2 -0
  148. package/skills/gsd-path-build/scripts/task_context.py +12 -3
  149. package/skills/gsd-path-build/scripts/token_budget.py +12 -8
  150. package/skills/gsd-path-build/scripts/workflow_run.py +107 -30
  151. package/skills/gsd-path-build/scripts/worktree_paths.py +72 -10
  152. package/skills/gsd-path-build/templates/task.md +1 -0
  153. package/skills/gsd-path-build/templates/wave-review.md +16 -4
  154. package/skills/gsd-path-decide/SKILL.md +11 -4
  155. package/skills/gsd-path-decide/references/dispatch.md +13 -0
  156. package/skills/gsd-path-decide/references/operating-rules.md +214 -0
  157. package/skills/gsd-path-decide/scripts/_common.py +619 -18
  158. package/skills/gsd-path-decide/scripts/archive_milestone.py +160 -86
  159. package/skills/gsd-path-decide/scripts/build_recovery.py +100 -32
  160. package/skills/gsd-path-decide/scripts/check_handoffs.py +662 -50
  161. package/skills/gsd-path-decide/scripts/check_task_briefs.py +338 -30
  162. package/skills/gsd-path-decide/scripts/detect_project.py +587 -32
  163. package/skills/gsd-path-decide/scripts/discussion_records.py +19 -13
  164. package/skills/gsd-path-decide/scripts/discussion_validate.py +152 -50
  165. package/skills/gsd-path-decide/scripts/integration.py +580 -25
  166. package/skills/gsd-path-decide/scripts/isolation.py +1438 -57
  167. package/skills/gsd-path-decide/scripts/loop_run.py +31 -29
  168. package/skills/gsd-path-decide/scripts/members.py +706 -0
  169. package/skills/gsd-path-decide/scripts/path_config.py +5 -5
  170. package/skills/gsd-path-decide/scripts/pipeline_git.py +124 -34
  171. package/skills/gsd-path-decide/scripts/pipeline_state.py +287 -87
  172. package/skills/gsd-path-decide/scripts/promote_lookahead.py +1 -1
  173. package/skills/gsd-path-decide/scripts/review_panel.py +1 -1
  174. package/skills/gsd-path-decide/scripts/state_checkpoint.py +135 -44
  175. package/skills/gsd-path-decide/scripts/state_promote.py +2 -0
  176. package/skills/gsd-path-decide/scripts/worktree_paths.py +72 -10
  177. package/skills/gsd-path-define/SKILL.md +36 -4
  178. package/skills/gsd-path-define/references/operating-rules.md +214 -0
  179. package/skills/gsd-path-define/references/spec-probes.md +150 -0
  180. package/skills/gsd-path-define/scripts/_common.py +619 -18
  181. package/skills/gsd-path-define/scripts/archive_milestone.py +160 -86
  182. package/skills/gsd-path-define/scripts/build_recovery.py +100 -32
  183. package/skills/gsd-path-define/scripts/check_handoffs.py +662 -50
  184. package/skills/gsd-path-define/scripts/check_task_briefs.py +338 -30
  185. package/skills/gsd-path-define/scripts/detect_project.py +587 -32
  186. package/skills/gsd-path-define/scripts/discussion_records.py +19 -13
  187. package/skills/gsd-path-define/scripts/discussion_validate.py +152 -50
  188. package/skills/gsd-path-define/scripts/integration.py +580 -25
  189. package/skills/gsd-path-define/scripts/isolation.py +1438 -57
  190. package/skills/gsd-path-define/scripts/loop_run.py +31 -29
  191. package/skills/gsd-path-define/scripts/members.py +706 -0
  192. package/skills/gsd-path-define/scripts/path_config.py +5 -5
  193. package/skills/gsd-path-define/scripts/pipeline_git.py +124 -34
  194. package/skills/gsd-path-define/scripts/pipeline_state.py +287 -87
  195. package/skills/gsd-path-define/scripts/promote_lookahead.py +1 -1
  196. package/skills/gsd-path-define/scripts/review_panel.py +1 -1
  197. package/skills/gsd-path-define/scripts/state_checkpoint.py +135 -44
  198. package/skills/gsd-path-define/scripts/state_promote.py +2 -0
  199. package/skills/gsd-path-define/scripts/worktree_paths.py +72 -10
  200. package/skills/gsd-path-define/templates/intent.md +30 -0
  201. package/skills/gsd-path-define/templates/state.md +1 -1
  202. package/skills/gsd-path-discuss/SKILL.md +2 -1
  203. package/skills/gsd-path-discuss/references/operating-rules.md +214 -0
  204. package/skills/gsd-path-discuss/scripts/_common.py +619 -18
  205. package/skills/gsd-path-discuss/scripts/discussion_records.py +19 -13
  206. package/skills/gsd-path-discuss/scripts/loop_run.py +31 -29
  207. package/skills/gsd-path-discuss/scripts/path_config.py +5 -5
  208. package/skills/gsd-path-discuss/scripts/review_panel.py +1 -1
  209. package/skills/gsd-path-discuss/scripts/worktree_paths.py +72 -10
  210. package/skills/gsd-path-docs-audit/SKILL.md +6 -4
  211. package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
  212. package/skills/gsd-path-docs-audit/references/docs-auditor.md +7 -1
  213. package/skills/gsd-path-docs-audit/references/operating-rules.md +214 -0
  214. package/skills/gsd-path-docs-audit/scripts/_common.py +619 -18
  215. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +160 -86
  216. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +100 -32
  217. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +91 -7
  218. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +662 -50
  219. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +338 -30
  220. package/skills/gsd-path-docs-audit/scripts/detect_project.py +587 -32
  221. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +19 -13
  222. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +152 -50
  223. package/skills/gsd-path-docs-audit/scripts/integration.py +580 -25
  224. package/skills/gsd-path-docs-audit/scripts/isolation.py +1438 -57
  225. package/skills/gsd-path-docs-audit/scripts/loop_run.py +31 -29
  226. package/skills/gsd-path-docs-audit/scripts/members.py +706 -0
  227. package/skills/gsd-path-docs-audit/scripts/path_config.py +5 -5
  228. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +124 -34
  229. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +287 -87
  230. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +1 -1
  231. package/skills/gsd-path-docs-audit/scripts/review_panel.py +1 -1
  232. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +135 -44
  233. package/skills/gsd-path-docs-audit/scripts/state_promote.py +2 -0
  234. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +72 -10
  235. package/skills/gsd-path-docs-audit/templates/docs-audit.md +6 -2
  236. package/skills/gsd-path-forensics/SKILL.md +2 -1
  237. package/skills/gsd-path-forensics/references/operating-rules.md +214 -0
  238. package/skills/gsd-path-forensics/scripts/_common.py +619 -18
  239. package/skills/gsd-path-forensics/scripts/discussion_records.py +19 -13
  240. package/skills/gsd-path-forensics/scripts/loop_run.py +31 -29
  241. package/skills/gsd-path-forensics/scripts/path_config.py +5 -5
  242. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +82 -31
  243. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +218 -30
  244. package/skills/gsd-path-forensics/scripts/review_panel.py +1 -1
  245. package/skills/gsd-path-forensics/scripts/worktree_paths.py +72 -10
  246. package/skills/gsd-path-inspect/SKILL.md +5 -4
  247. package/skills/gsd-path-inspect/references/codebase-mapper.md +2 -1
  248. package/skills/gsd-path-inspect/references/dispatch.md +13 -0
  249. package/skills/gsd-path-inspect/references/docs-auditor.md +7 -1
  250. package/skills/gsd-path-inspect/references/operating-rules.md +214 -0
  251. package/skills/gsd-path-inspect/scripts/_common.py +619 -18
  252. package/skills/gsd-path-inspect/scripts/archive_milestone.py +160 -86
  253. package/skills/gsd-path-inspect/scripts/build_recovery.py +100 -32
  254. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +91 -7
  255. package/skills/gsd-path-inspect/scripts/check_handoffs.py +662 -50
  256. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +338 -30
  257. package/skills/gsd-path-inspect/scripts/detect_project.py +587 -32
  258. package/skills/gsd-path-inspect/scripts/discussion_records.py +19 -13
  259. package/skills/gsd-path-inspect/scripts/discussion_validate.py +152 -50
  260. package/skills/gsd-path-inspect/scripts/integration.py +580 -25
  261. package/skills/gsd-path-inspect/scripts/isolation.py +1438 -57
  262. package/skills/gsd-path-inspect/scripts/loop_run.py +31 -29
  263. package/skills/gsd-path-inspect/scripts/members.py +706 -0
  264. package/skills/gsd-path-inspect/scripts/path_config.py +5 -5
  265. package/skills/gsd-path-inspect/scripts/pipeline_git.py +124 -34
  266. package/skills/gsd-path-inspect/scripts/pipeline_state.py +287 -87
  267. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +1 -1
  268. package/skills/gsd-path-inspect/scripts/review_panel.py +1 -1
  269. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +135 -44
  270. package/skills/gsd-path-inspect/scripts/state_promote.py +2 -0
  271. package/skills/gsd-path-inspect/scripts/workflow_run.py +107 -30
  272. package/skills/gsd-path-inspect/scripts/worktree_paths.py +72 -10
  273. package/skills/gsd-path-inspect/templates/codebase.md +1 -1
  274. package/skills/gsd-path-inspect/templates/docs-audit.md +6 -2
  275. package/skills/gsd-path-inspect/templates/state.md +1 -1
  276. package/skills/gsd-path-loop/SKILL.md +2 -1
  277. package/skills/gsd-path-loop/references/operating-rules.md +214 -0
  278. package/skills/gsd-path-loop/scripts/_common.py +619 -18
  279. package/skills/gsd-path-loop/scripts/discussion_records.py +19 -13
  280. package/skills/gsd-path-loop/scripts/loop_run.py +31 -29
  281. package/skills/gsd-path-loop/scripts/path_config.py +5 -5
  282. package/skills/gsd-path-loop/scripts/review_panel.py +1 -1
  283. package/skills/gsd-path-loop/scripts/worktree_paths.py +72 -10
  284. package/skills/gsd-path-migrate/SKILL.md +2 -1
  285. package/skills/gsd-path-migrate/references/operating-rules.md +214 -0
  286. package/skills/gsd-path-migrate/scripts/_common.py +619 -18
  287. package/skills/gsd-path-migrate/scripts/core_hook_gate.py +10 -1
  288. package/skills/gsd-path-migrate/scripts/core_hook_settings.py +11 -4
  289. package/skills/gsd-path-migrate/scripts/discussion_records.py +19 -13
  290. package/skills/gsd-path-migrate/scripts/loop_run.py +31 -29
  291. package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -2
  292. package/skills/gsd-path-migrate/scripts/path_config.py +5 -5
  293. package/skills/gsd-path-migrate/scripts/review_panel.py +1 -1
  294. package/skills/gsd-path-plan/SKILL.md +18 -4
  295. package/skills/gsd-path-plan/references/dispatch.md +13 -0
  296. package/skills/gsd-path-plan/references/operating-rules.md +214 -0
  297. package/skills/gsd-path-plan/references/planner.md +22 -4
  298. package/skills/gsd-path-plan/references/spec-probes.md +150 -0
  299. package/skills/gsd-path-plan/scripts/_common.py +619 -18
  300. package/skills/gsd-path-plan/scripts/archive_milestone.py +160 -86
  301. package/skills/gsd-path-plan/scripts/build_recovery.py +100 -32
  302. package/skills/gsd-path-plan/scripts/check_handoffs.py +662 -50
  303. package/skills/gsd-path-plan/scripts/check_task_briefs.py +338 -30
  304. package/skills/gsd-path-plan/scripts/detect_project.py +587 -32
  305. package/skills/gsd-path-plan/scripts/discussion_records.py +19 -13
  306. package/skills/gsd-path-plan/scripts/discussion_validate.py +152 -50
  307. package/skills/gsd-path-plan/scripts/integration.py +580 -25
  308. package/skills/gsd-path-plan/scripts/isolation.py +1438 -57
  309. package/skills/gsd-path-plan/scripts/loop_run.py +31 -29
  310. package/skills/gsd-path-plan/scripts/members.py +706 -0
  311. package/skills/gsd-path-plan/scripts/path_config.py +5 -5
  312. package/skills/gsd-path-plan/scripts/pipeline_git.py +124 -34
  313. package/skills/gsd-path-plan/scripts/pipeline_state.py +287 -87
  314. package/skills/gsd-path-plan/scripts/promote_lookahead.py +1 -1
  315. package/skills/gsd-path-plan/scripts/review_panel.py +1 -1
  316. package/skills/gsd-path-plan/scripts/state_checkpoint.py +135 -44
  317. package/skills/gsd-path-plan/scripts/state_promote.py +2 -0
  318. package/skills/gsd-path-plan/scripts/workflow_run.py +107 -30
  319. package/skills/gsd-path-plan/scripts/worktree_paths.py +72 -10
  320. package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
  321. package/skills/gsd-path-plan/templates/plan.md +17 -2
  322. package/skills/gsd-path-plan/templates/task.md +1 -0
  323. package/skills/gsd-path-research/SKILL.md +13 -1
  324. package/skills/gsd-path-research/references/dispatch.md +13 -0
  325. package/skills/gsd-path-research/references/operating-rules.md +214 -0
  326. package/skills/gsd-path-research/scripts/_common.py +619 -18
  327. package/skills/gsd-path-research/scripts/archive_milestone.py +160 -86
  328. package/skills/gsd-path-research/scripts/build_recovery.py +100 -32
  329. package/skills/gsd-path-research/scripts/check_handoffs.py +662 -50
  330. package/skills/gsd-path-research/scripts/check_task_briefs.py +338 -30
  331. package/skills/gsd-path-research/scripts/detect_project.py +587 -32
  332. package/skills/gsd-path-research/scripts/discussion_records.py +19 -13
  333. package/skills/gsd-path-research/scripts/discussion_validate.py +152 -50
  334. package/skills/gsd-path-research/scripts/integration.py +580 -25
  335. package/skills/gsd-path-research/scripts/isolation.py +1438 -57
  336. package/skills/gsd-path-research/scripts/loop_run.py +31 -29
  337. package/skills/gsd-path-research/scripts/members.py +706 -0
  338. package/skills/gsd-path-research/scripts/path_config.py +5 -5
  339. package/skills/gsd-path-research/scripts/pipeline_git.py +124 -34
  340. package/skills/gsd-path-research/scripts/pipeline_state.py +287 -87
  341. package/skills/gsd-path-research/scripts/promote_lookahead.py +1 -1
  342. package/skills/gsd-path-research/scripts/review_panel.py +1 -1
  343. package/skills/gsd-path-research/scripts/state_checkpoint.py +135 -44
  344. package/skills/gsd-path-research/scripts/state_promote.py +2 -0
  345. package/skills/gsd-path-research/scripts/worktree_paths.py +72 -10
  346. package/skills/gsd-path-research/templates/research-handoff.md +14 -3
  347. package/skills/gsd-path-roadmap/SKILL.md +2 -1
  348. package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
  349. package/skills/gsd-path-roadmap/references/operating-rules.md +214 -0
  350. package/skills/gsd-path-roadmap/scripts/_common.py +619 -18
  351. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +160 -86
  352. package/skills/gsd-path-roadmap/scripts/build_recovery.py +100 -32
  353. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +662 -50
  354. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +338 -30
  355. package/skills/gsd-path-roadmap/scripts/detect_project.py +587 -32
  356. package/skills/gsd-path-roadmap/scripts/discussion_records.py +19 -13
  357. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +152 -50
  358. package/skills/gsd-path-roadmap/scripts/integration.py +580 -25
  359. package/skills/gsd-path-roadmap/scripts/isolation.py +1438 -57
  360. package/skills/gsd-path-roadmap/scripts/loop_run.py +31 -29
  361. package/skills/gsd-path-roadmap/scripts/members.py +706 -0
  362. package/skills/gsd-path-roadmap/scripts/path_config.py +5 -5
  363. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +124 -34
  364. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +287 -87
  365. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +1 -1
  366. package/skills/gsd-path-roadmap/scripts/review_panel.py +1 -1
  367. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +135 -44
  368. package/skills/gsd-path-roadmap/scripts/state_promote.py +2 -0
  369. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +72 -10
  370. package/skills/gsd-path-ship/SKILL.md +33 -11
  371. package/skills/gsd-path-ship/references/dispatch.md +13 -0
  372. package/skills/gsd-path-ship/references/operating-rules.md +214 -0
  373. package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
  374. package/skills/gsd-path-ship/references/reviewer-wave.md +18 -5
  375. package/skills/gsd-path-ship/references/reviewer.md +3 -1
  376. package/skills/gsd-path-ship/references/spec-probes.md +150 -0
  377. package/skills/gsd-path-ship/scripts/_common.py +619 -18
  378. package/skills/gsd-path-ship/scripts/archive_milestone.py +160 -86
  379. package/skills/gsd-path-ship/scripts/build_recovery.py +100 -32
  380. package/skills/gsd-path-ship/scripts/build_state.py +143 -24
  381. package/skills/gsd-path-ship/scripts/check_handoffs.py +662 -50
  382. package/skills/gsd-path-ship/scripts/check_task_briefs.py +338 -30
  383. package/skills/gsd-path-ship/scripts/detect_project.py +587 -32
  384. package/skills/gsd-path-ship/scripts/discussion_records.py +19 -13
  385. package/skills/gsd-path-ship/scripts/discussion_validate.py +152 -50
  386. package/skills/gsd-path-ship/scripts/integration.py +580 -25
  387. package/skills/gsd-path-ship/scripts/isolation.py +1438 -57
  388. package/skills/gsd-path-ship/scripts/lean_verification.py +99 -19
  389. package/skills/gsd-path-ship/scripts/loop_run.py +31 -29
  390. package/skills/gsd-path-ship/scripts/members.py +706 -0
  391. package/skills/gsd-path-ship/scripts/path_config.py +5 -5
  392. package/skills/gsd-path-ship/scripts/pipeline_git.py +124 -34
  393. package/skills/gsd-path-ship/scripts/pipeline_state.py +287 -87
  394. package/skills/gsd-path-ship/scripts/promote_lookahead.py +1 -1
  395. package/skills/gsd-path-ship/scripts/review_panel.py +1 -1
  396. package/skills/gsd-path-ship/scripts/state_checkpoint.py +135 -44
  397. package/skills/gsd-path-ship/scripts/state_promote.py +2 -0
  398. package/skills/gsd-path-ship/scripts/workflow_run.py +107 -30
  399. package/skills/gsd-path-ship/scripts/worktree_paths.py +72 -10
  400. package/skills/gsd-path-ship/templates/final-review.md +16 -4
  401. package/skills/gsd-path-ship/templates/gap-review.md +2 -1
  402. package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
  403. package/skills/gsd-path-ship/templates/wave-review.md +16 -4
  404. package/skills/gsd-path-undo/SKILL.md +2 -1
  405. package/skills/gsd-path-undo/references/operating-rules.md +214 -0
  406. package/skills/gsd-path-undo/scripts/_common.py +619 -18
  407. package/skills/gsd-path-undo/scripts/discussion_records.py +19 -13
  408. package/skills/gsd-path-undo/scripts/loop_run.py +31 -29
  409. package/skills/gsd-path-undo/scripts/path_config.py +5 -5
  410. package/skills/gsd-path-undo/scripts/pipeline_undo.py +218 -30
  411. package/skills/gsd-path-undo/scripts/review_panel.py +1 -1
  412. package/skills/gsd-path-undo/scripts/worktree_paths.py +72 -10
  413. package/skills/path/BUILD.md +7 -1
  414. package/skills/path/DECIDE.md +11 -4
  415. package/skills/path/DEFINE.md +36 -4
  416. package/skills/path/DOCS-AUDIT.md +6 -4
  417. package/skills/path/INSPECT.md +5 -4
  418. package/skills/path/PLAN.md +18 -4
  419. package/skills/path/RESEARCH.md +13 -1
  420. package/skills/path/ROADMAP.md +2 -1
  421. package/skills/path/SHIP.md +33 -11
  422. package/skills/path/SKILL.md +45 -1
  423. package/skills/path/references/build-native.md +63 -15
  424. package/skills/path/references/codebase-mapper.md +2 -1
  425. package/skills/path/references/dispatch.md +13 -0
  426. package/skills/path/references/docs-auditor.md +7 -1
  427. package/skills/path/references/operating-rules.md +214 -0
  428. package/skills/path/references/planner.md +22 -4
  429. package/skills/path/references/reviewer-final.md +20 -2
  430. package/skills/path/references/reviewer-wave.md +18 -5
  431. package/skills/path/references/reviewer.md +3 -1
  432. package/skills/path/references/spec-probes.md +150 -0
  433. package/skills/path/scripts/_common.py +619 -18
  434. package/skills/path/scripts/archive_milestone.py +160 -86
  435. package/skills/path/scripts/bootstrap_repository.py +8 -5
  436. package/skills/path/scripts/build_recovery.py +100 -32
  437. package/skills/path/scripts/build_state.py +143 -24
  438. package/skills/path/scripts/check_docs_audit.py +91 -7
  439. package/skills/path/scripts/check_handoffs.py +662 -50
  440. package/skills/path/scripts/check_task_briefs.py +338 -30
  441. package/skills/path/scripts/check_update.py +2 -2
  442. package/skills/path/scripts/detect_project.py +587 -32
  443. package/skills/path/scripts/discussion_records.py +19 -13
  444. package/skills/path/scripts/discussion_validate.py +152 -50
  445. package/skills/path/scripts/dispatch_driver.py +501 -46
  446. package/skills/path/scripts/git_guard.py +118 -31
  447. package/skills/path/scripts/integration.py +580 -25
  448. package/skills/path/scripts/isolation.py +1438 -57
  449. package/skills/path/scripts/lean_verification.py +99 -19
  450. package/skills/path/scripts/loop_run.py +31 -29
  451. package/skills/path/scripts/members.py +706 -0
  452. package/skills/path/scripts/path_config.py +5 -5
  453. package/skills/path/scripts/pipeline_git.py +124 -34
  454. package/skills/path/scripts/pipeline_state.py +287 -87
  455. package/skills/path/scripts/promote_lookahead.py +1 -1
  456. package/skills/path/scripts/review_panel.py +1 -1
  457. package/skills/path/scripts/state_checkpoint.py +135 -44
  458. package/skills/path/scripts/state_promote.py +2 -0
  459. package/skills/path/scripts/status_runtime.py +58 -10
  460. package/skills/path/scripts/task_context.py +12 -3
  461. package/skills/path/scripts/token_budget.py +12 -8
  462. package/skills/path/scripts/workflow_run.py +107 -30
  463. package/skills/path/scripts/worktree_paths.py +72 -10
  464. package/skills/path/templates/codebase.md +1 -1
  465. package/skills/path/templates/docs-audit.md +6 -2
  466. package/skills/path/templates/final-review.md +16 -4
  467. package/skills/path/templates/gap-review.md +2 -1
  468. package/skills/path/templates/intent.md +30 -0
  469. package/skills/path/templates/patch-findings.md +3 -3
  470. package/skills/path/templates/plan.md +17 -2
  471. package/skills/path/templates/research-handoff.md +14 -3
  472. package/skills/path/templates/state.md +1 -1
  473. package/skills/path/templates/task.md +1 -0
  474. package/skills/path/templates/wave-review.md +16 -4
  475. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  476. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  477. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  478. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  479. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  480. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  481. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
@@ -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
 
@@ -48,10 +55,22 @@ try:
48
55
  fcntl.flock(fd, fcntl.LOCK_EX)
49
56
 
50
57
  except ImportError: # Windows does not provide POSIX file locks.
58
+ import errno
51
59
  import msvcrt
60
+ import time
52
61
 
53
62
  def lock_exclusive(fd: int) -> None:
54
- msvcrt.locking(fd, msvcrt.LK_LOCK, 1)
63
+ delay = 0.01
64
+ while True:
65
+ try:
66
+ os.lseek(fd, 0, os.SEEK_SET)
67
+ msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
68
+ return
69
+ except OSError as error:
70
+ if error.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
71
+ raise
72
+ time.sleep(delay)
73
+ delay = min(delay * 2, 0.25)
55
74
 
56
75
 
57
76
  IGNORE_DIRS = {
@@ -210,16 +229,20 @@ HTML_BLOCK_START = re.compile(
210
229
  re.IGNORECASE,
211
230
  )
212
231
  REPARSE_NAME_SURROGATE = 0x20000000
213
- ANCHORED_EVIDENCE_SUPPORTED = (
232
+ _POSIX_ANCHORED_EVIDENCE_SUPPORTED = (
214
233
  hasattr(os, "O_DIRECTORY")
215
234
  and hasattr(os, "O_NOFOLLOW")
216
235
  and os.open in os.supports_dir_fd
217
236
  and os.stat in os.supports_dir_fd
218
237
  and os.stat in os.supports_follow_symlinks
219
238
  )
239
+ WINDOWS_ANCHORED_EVIDENCE_SUPPORTED = os.name == "nt"
240
+ ANCHORED_EVIDENCE_SUPPORTED = (
241
+ _POSIX_ANCHORED_EVIDENCE_SUPPORTED or WINDOWS_ANCHORED_EVIDENCE_SUPPORTED
242
+ )
220
243
  LISTDIR_DIR_FD_SUPPORTED = os.listdir in getattr(os, "supports_fd", ())
221
244
  ANCHORED_STATE_CREATE_SUPPORTED = (
222
- ANCHORED_EVIDENCE_SUPPORTED
245
+ _POSIX_ANCHORED_EVIDENCE_SUPPORTED
223
246
  and LISTDIR_DIR_FD_SUPPORTED
224
247
  and os.mkdir in getattr(os, "supports_dir_fd", ())
225
248
  and os.unlink in getattr(os, "supports_dir_fd", ())
@@ -262,11 +285,12 @@ def is_link_like_status(status: os.stat_result) -> bool:
262
285
  def is_link_like(path: Path, status: os.stat_result) -> bool:
263
286
  if is_link_like_status(status):
264
287
  return True
265
- isjunction = getattr(os.path, "isjunction", None)
266
- if os.name != "nt" or isjunction is None:
288
+ if os.name != "nt":
267
289
  return False
268
290
  try:
269
- return bool(isjunction(path))
291
+ return is_link_like_status(os.lstat(path))
292
+ except FileNotFoundError:
293
+ return False
270
294
  except OSError as error:
271
295
  raise DetectError(f"cannot inspect link-like evidence: {path}: {error}") from error
272
296
 
@@ -325,7 +349,7 @@ def is_managed_pipeline_directory(relative: str, root: Path) -> bool:
325
349
  return is_verified_skill_bundle(relative, root)
326
350
 
327
351
 
328
- def is_verified_skill_bundle_at(relative: str, directory_fd: int) -> bool:
352
+ def is_verified_skill_bundle_at(relative: str, directory_fd: int, root: Path) -> bool:
329
353
  parts = PurePosixPath(relative).parts
330
354
  if not parts:
331
355
  return False
@@ -335,19 +359,45 @@ def is_verified_skill_bundle_at(relative: str, directory_fd: int) -> bool:
335
359
  return False
336
360
  if is_verified_installer_bundle(parts):
337
361
  return True
362
+ if _POSIX_ANCHORED_EVIDENCE_SUPPORTED:
363
+ try:
364
+ status = os.stat(
365
+ "SKILL.md",
366
+ dir_fd=directory_fd,
367
+ follow_symlinks=False,
368
+ )
369
+ except FileNotFoundError:
370
+ return False
371
+ except OSError as error:
372
+ raise DetectError(
373
+ f"cannot inspect managed skill bundle: {relative}: {error}"
374
+ ) from error
375
+ return not is_link_like_status(status) and stat.S_ISREG(status.st_mode)
376
+ skill_fd = None
338
377
  try:
339
- status = os.stat(
378
+ win = get_windows_handles()
379
+ skill_fd = win.open_relative(
380
+ directory_fd,
340
381
  "SKILL.md",
341
- dir_fd=directory_fd,
342
- follow_symlinks=False,
382
+ _FILE_READ_ATTRIBUTES,
383
+ _PIN_SHARE,
384
+ _FILE_OPEN,
385
+ 0,
343
386
  )
387
+ status = os.fstat(skill_fd)
344
388
  except FileNotFoundError:
345
389
  return False
346
390
  except OSError as error:
391
+ if getattr(error, "winerror", None) == 2:
392
+ return False
347
393
  raise DetectError(
348
394
  f"cannot inspect managed skill bundle: {relative}: {error}"
349
395
  ) from error
350
- return not is_link_like_status(status) and stat.S_ISREG(status.st_mode)
396
+ finally:
397
+ if skill_fd is not None:
398
+ os.close(skill_fd)
399
+ skill = root.joinpath(*parts) / "SKILL.md"
400
+ return not is_link_like(skill, status) and stat.S_ISREG(status.st_mode)
351
401
 
352
402
 
353
403
  def is_fixed_managed_pipeline_artifact(relative: str) -> bool:
@@ -416,13 +466,20 @@ def read_regular_evidence(
416
466
  ) from error
417
467
  if not relative.parts:
418
468
  raise DetectError(f"{evidence_name} evidence path is empty: {path}")
419
- if ANCHORED_EVIDENCE_SUPPORTED:
469
+ if _POSIX_ANCHORED_EVIDENCE_SUPPORTED:
420
470
  return read_anchored_evidence(
421
471
  relative,
422
472
  root,
423
473
  missing_ok=missing_ok,
424
474
  evidence_name=evidence_name,
425
475
  )
476
+ if WINDOWS_ANCHORED_EVIDENCE_SUPPORTED:
477
+ return read_windows_anchored_evidence(
478
+ relative,
479
+ root,
480
+ missing_ok=missing_ok,
481
+ evidence_name=evidence_name,
482
+ )
426
483
  return read_lstat_open_evidence(
427
484
  relative,
428
485
  root,
@@ -440,6 +497,17 @@ def directory_flags() -> int:
440
497
  )
441
498
 
442
499
 
500
+ def listdir_anchored(dir_fd: int) -> list[str]:
501
+ """List names through a fresh directory descriptor under dir_fd."""
502
+ fresh = os.open(".", directory_flags(), dir_fd=dir_fd)
503
+ try:
504
+ if not os.path.samestat(os.fstat(fresh), os.fstat(dir_fd)):
505
+ raise DetectError("anchored directory listing lost its identity")
506
+ return os.listdir(fresh)
507
+ finally:
508
+ os.close(fresh)
509
+
510
+
443
511
  def file_flags() -> int:
444
512
  return (
445
513
  os.O_RDONLY
@@ -759,9 +827,12 @@ def raise_walk_error(error: OSError) -> None:
759
827
 
760
828
 
761
829
  def iter_worktree_files(root: Path) -> Iterable[str]:
762
- if ANCHORED_EVIDENCE_SUPPORTED and LISTDIR_DIR_FD_SUPPORTED:
830
+ if _POSIX_ANCHORED_EVIDENCE_SUPPORTED and LISTDIR_DIR_FD_SUPPORTED:
763
831
  yield from iter_worktree_files_anchored(root)
764
832
  return
833
+ if WINDOWS_ANCHORED_EVIDENCE_SUPPORTED:
834
+ yield from iter_worktree_files_windows_anchored(root)
835
+ return
765
836
  for dirpath, dirnames, filenames in os.walk(
766
837
  root,
767
838
  followlinks=False,
@@ -819,7 +890,7 @@ def iter_from_dir_fd(
819
890
  dir_fd: int, relative_dir: str, root: Path
820
891
  ) -> Iterable[str]:
821
892
  try:
822
- names = os.listdir(dir_fd)
893
+ names = listdir_anchored(dir_fd)
823
894
  except OSError as error:
824
895
  raise DetectError(f"cannot traverse filesystem evidence: {error}") from error
825
896
  for name in names:
@@ -855,7 +926,7 @@ def iter_from_dir_fd(
855
926
  raise DetectError(
856
927
  f"filesystem evidence changed while traversing: {relative}"
857
928
  )
858
- if is_verified_skill_bundle_at(relative, child):
929
+ if is_verified_skill_bundle_at(relative, child, root):
859
930
  continue
860
931
  yield from iter_from_dir_fd(child, relative, root)
861
932
  finally:
@@ -961,6 +1032,26 @@ def is_staged_skill_bundle_artifact(
961
1032
  )
962
1033
 
963
1034
 
1035
+ def ignored_junk(directory: Path, name: str, status: os.stat_result) -> bool:
1036
+ """Ignored, untracked OS junk. The caller's no-follow status decides the file type."""
1037
+ if (
1038
+ name not in _common.OS_JUNK_NAMES
1039
+ or is_link_like_status(status)
1040
+ or not stat.S_ISREG(status.st_mode)
1041
+ ):
1042
+ return False
1043
+ try:
1044
+ result = subprocess.run(
1045
+ ["git", "-C", str(directory), "check-ignore", "-q", "--", name],
1046
+ capture_output=True,
1047
+ check=False,
1048
+ env=git_environment(),
1049
+ )
1050
+ except OSError:
1051
+ return False
1052
+ return result.returncode == 0
1053
+
1054
+
964
1055
  def project_path_inventory(
965
1056
  project: Path, root: Path
966
1057
  ) -> tuple[tuple[str, ...], tuple[str, ...]]:
@@ -1001,14 +1092,18 @@ def project_path_inventory(
1001
1092
  posix_relative(current / name, root) for name in linked_directories
1002
1093
  )
1003
1094
  dirnames[:] = [name for name in dirnames if name not in linked_directories]
1095
+ kept = []
1004
1096
  for name in filenames:
1005
1097
  path = current / name
1006
- relative = posix_relative(path, root)
1007
1098
  status = lstat_evidence(path, missing_ok=False)
1099
+ if ignored_junk(current, name, status):
1100
+ continue
1101
+ kept.append(name)
1102
+ relative = posix_relative(path, root)
1008
1103
  paths.append(relative)
1009
1104
  if is_link_like(path, status) or not stat.S_ISREG(status.st_mode):
1010
1105
  unsafe_paths.append(relative)
1011
- if not filenames and not dirnames and current != project:
1106
+ if not kept and not dirnames and current != project:
1012
1107
  paths.append(posix_relative(current, root))
1013
1108
  if not paths:
1014
1109
  try:
@@ -1017,7 +1112,11 @@ def project_path_inventory(
1017
1112
  raise DetectError(
1018
1113
  f"cannot list project evidence: {project}: {error}"
1019
1114
  ) from error
1020
- paths = [posix_relative(project / name, root) for name in names]
1115
+ paths = [
1116
+ posix_relative(project / name, root)
1117
+ for name in names
1118
+ if not ignored_junk(project, name, lstat_evidence(project / name, missing_ok=False))
1119
+ ]
1021
1120
  return tuple(sorted(paths)), tuple(sorted(unsafe_paths))
1022
1121
 
1023
1122
 
@@ -1093,7 +1192,7 @@ def classify(repo: Path) -> dict:
1093
1192
  missing_ok=True,
1094
1193
  )
1095
1194
  if (
1096
- ANCHORED_STATE_CREATE_SUPPORTED
1195
+ STATE_CREATE_SUPPORTED
1097
1196
  and orphan_paths == (f".project/{STATE_TEMP_NAME}",)
1098
1197
  and not unsafe_paths
1099
1198
  and temporary_state_status is not None
@@ -1265,12 +1364,28 @@ def close_file_descriptors(
1265
1364
  pass
1266
1365
 
1267
1366
 
1367
+ def project_entries(project_fd: int, project: Path) -> set:
1368
+ """Names in .project without ignored OS junk (Finder may add .DS_Store at any time)."""
1369
+ entries = set(listdir_anchored(project_fd))
1370
+ for name in entries & _common.OS_JUNK_NAMES:
1371
+ try:
1372
+ status = os.stat(name, dir_fd=project_fd, follow_symlinks=False)
1373
+ except FileNotFoundError:
1374
+ continue # Keep the name: the caller's comparison then fails closed.
1375
+ if ignored_junk(project, name, status):
1376
+ entries.discard(name)
1377
+ return entries
1378
+
1379
+
1268
1380
  def write_state_anchored(
1269
1381
  root: Path,
1270
1382
  content: str,
1271
1383
  expected_root: os.stat_result,
1272
1384
  expected_project: Optional[os.stat_result],
1273
1385
  ) -> None:
1386
+ if WINDOWS_STATE_CREATE_SUPPORTED:
1387
+ write_state_windows(root, content, expected_root, expected_project)
1388
+ return
1274
1389
  if not ANCHORED_STATE_CREATE_SUPPORTED:
1275
1390
  raise DetectError("anchored no-follow STATE.md creation is unavailable")
1276
1391
  payload = content.encode("utf-8")
@@ -1333,7 +1448,7 @@ def write_state_anchored(
1333
1448
  ):
1334
1449
  raise DetectError(".project changed after classification")
1335
1450
  lock_exclusive(project_fd)
1336
- entries = set(os.listdir(project_fd))
1451
+ entries = project_entries(project_fd, root / ".project")
1337
1452
  if entries not in (set(), {STATE_TEMP_NAME}):
1338
1453
  raise DetectError(".project changed after classification")
1339
1454
  create = (
@@ -1362,12 +1477,12 @@ def write_state_anchored(
1362
1477
  follow_symlinks=False,
1363
1478
  )
1364
1479
  except FileNotFoundError as error:
1365
- if "STATE.md" in os.listdir(project_fd):
1480
+ if "STATE.md" in listdir_anchored(project_fd):
1366
1481
  raise DetectError("STATE.md already exists") from error
1367
1482
  raise DetectError("temporary STATE.md disappeared") from error
1368
1483
  if not os.path.samestat(created_status, named_temporary):
1369
1484
  raise DetectError("temporary STATE.md changed before writing")
1370
- if "STATE.md" in os.listdir(project_fd):
1485
+ if "STATE.md" in listdir_anchored(project_fd):
1371
1486
  raise DetectError("STATE.md already exists")
1372
1487
  os.ftruncate(state_fd, 0)
1373
1488
  write_all(state_fd, payload)
@@ -1400,7 +1515,7 @@ def write_state_anchored(
1400
1515
  or not os.path.samestat(created_status, current_state)
1401
1516
  ):
1402
1517
  raise DetectError("temporary STATE.md changed while writing")
1403
- if set(os.listdir(project_fd)) != {STATE_TEMP_NAME}:
1518
+ if project_entries(project_fd, root / ".project") != {STATE_TEMP_NAME}:
1404
1519
  raise DetectError(".project contents changed while creating STATE.md")
1405
1520
  try:
1406
1521
  os.link(
@@ -1420,7 +1535,7 @@ def write_state_anchored(
1420
1535
  published = os.stat("STATE.md", dir_fd=project_fd, follow_symlinks=False)
1421
1536
  if published.st_nlink != 1 or not os.path.samestat(created_status, published):
1422
1537
  raise DetectError("STATE.md changed while publishing")
1423
- if set(os.listdir(project_fd)) != {"STATE.md"}:
1538
+ if project_entries(project_fd, root / ".project") != {"STATE.md"}:
1424
1539
  raise DetectError(".project contents changed while publishing STATE.md")
1425
1540
  os.fsync(project_fd)
1426
1541
  closing_state_fd = state_fd
@@ -1448,6 +1563,446 @@ def write_state_anchored(
1448
1563
  close_file_descriptors(state_fd, project_fd, root_fd)
1449
1564
 
1450
1565
 
1566
+ # --- Windows anchored creation ------------------------------------------------
1567
+ # Windows has no dir_fd, O_NOFOLLOW, or O_DIRECTORY. Handles give the same
1568
+ # guarantee (docs/adr/0004-windows-anchored-state.md):
1569
+ # - each directory is opened without following a reparse point and held
1570
+ # without FILE_SHARE_DELETE, so it cannot be renamed or replaced while held;
1571
+ # - children are opened relative to the held parent handle (NtCreateFile with
1572
+ # a RootDirectory), so no path is resolved again after it was checked;
1573
+ # - STATE.md is published by renaming the temporary file's own handle without
1574
+ # replacing an existing name, and rollback deletes through that handle.
1575
+
1576
+ _FILE_LIST_DIRECTORY = 0x0001
1577
+ _FILE_TRAVERSE = 0x0020
1578
+ _FILE_READ_ATTRIBUTES = 0x0080
1579
+ _DELETE = 0x00010000
1580
+ _SYNCHRONIZE = 0x00100000
1581
+ _GENERIC_WRITE = 0x40000000
1582
+ _GENERIC_READ = 0x80000000
1583
+ _FILE_SHARE_READ = 0x1
1584
+ _FILE_SHARE_WRITE = 0x2
1585
+ _OPEN_EXISTING = 3
1586
+ _FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
1587
+ _FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
1588
+ _FILE_OPEN = 1
1589
+ _FILE_CREATE = 2
1590
+ _FILE_OPEN_IF = 3
1591
+ _FILE_DIRECTORY_FILE = 0x00000001
1592
+ _FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020
1593
+ _FILE_NON_DIRECTORY_FILE = 0x00000040
1594
+ _FILE_OPEN_REPARSE_POINT = 0x00200000
1595
+ _OBJ_CASE_INSENSITIVE = 0x00000040
1596
+ _FILE_RENAME_INFORMATION = 10
1597
+ _FILE_DISPOSITION_INFO = 4
1598
+ _DIRECTORY_ACCESS = _FILE_LIST_DIRECTORY | _FILE_TRAVERSE | _FILE_READ_ATTRIBUTES | _SYNCHRONIZE
1599
+ # Readers and writers may share a held directory; nobody may delete or rename it.
1600
+ _PIN_SHARE = _FILE_SHARE_READ | _FILE_SHARE_WRITE
1601
+
1602
+ WINDOWS_STATE_CREATE_SUPPORTED = os.name == "nt"
1603
+ STATE_CREATE_SUPPORTED = ANCHORED_STATE_CREATE_SUPPORTED or WINDOWS_STATE_CREATE_SUPPORTED
1604
+
1605
+
1606
+ class _WindowsHandles:
1607
+ """kernel32/ntdll calls for the Windows anchored create, bound on first use."""
1608
+
1609
+ def __init__(self) -> None:
1610
+ import ctypes
1611
+ import msvcrt
1612
+ from ctypes import wintypes
1613
+
1614
+ class UnicodeString(ctypes.Structure):
1615
+ _fields_ = [("Length", wintypes.USHORT), ("MaximumLength", wintypes.USHORT),
1616
+ ("Buffer", wintypes.LPWSTR)]
1617
+
1618
+ class ObjectAttributes(ctypes.Structure):
1619
+ _fields_ = [("Length", wintypes.ULONG), ("RootDirectory", wintypes.HANDLE),
1620
+ ("ObjectName", ctypes.POINTER(UnicodeString)),
1621
+ ("Attributes", wintypes.ULONG), ("SecurityDescriptor", ctypes.c_void_p),
1622
+ ("SecurityQualityOfService", ctypes.c_void_p)]
1623
+
1624
+ class IoStatusBlock(ctypes.Structure):
1625
+ _fields_ = [("Status", ctypes.c_void_p), ("Information", ctypes.c_size_t)]
1626
+
1627
+ self.ctypes, self.wintypes, self.msvcrt = ctypes, wintypes, msvcrt
1628
+ self.UnicodeString, self.ObjectAttributes, self.IoStatusBlock = (
1629
+ UnicodeString, ObjectAttributes, IoStatusBlock)
1630
+ self.kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
1631
+ self.ntdll = ctypes.WinDLL("ntdll")
1632
+ self.kernel32.CreateFileW.restype = wintypes.HANDLE
1633
+ self.kernel32.CreateFileW.argtypes = (
1634
+ wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p,
1635
+ wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE)
1636
+ self.kernel32.SetFileInformationByHandle.argtypes = (
1637
+ wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD)
1638
+ self.ntdll.NtCreateFile.restype = ctypes.c_long
1639
+ self.ntdll.NtCreateFile.argtypes = (
1640
+ ctypes.POINTER(wintypes.HANDLE), wintypes.DWORD, ctypes.POINTER(ObjectAttributes),
1641
+ ctypes.POINTER(IoStatusBlock), ctypes.c_void_p, wintypes.ULONG, wintypes.ULONG,
1642
+ wintypes.ULONG, wintypes.ULONG, ctypes.c_void_p, wintypes.ULONG)
1643
+ self.ntdll.RtlNtStatusToDosError.restype = wintypes.ULONG
1644
+ self.ntdll.RtlNtStatusToDosError.argtypes = (ctypes.c_long,)
1645
+ self.ntdll.NtSetInformationFile.restype = ctypes.c_long
1646
+ self.ntdll.NtSetInformationFile.argtypes = (
1647
+ wintypes.HANDLE, ctypes.POINTER(IoStatusBlock), ctypes.c_void_p, wintypes.ULONG,
1648
+ ctypes.c_int)
1649
+
1650
+ def adopt(self, handle: int) -> int:
1651
+ """A CRT descriptor that owns handle, so os.fstat, os.write, and os.close apply."""
1652
+ try:
1653
+ descriptor = self.msvcrt.open_osfhandle(handle, 0)
1654
+ except OSError:
1655
+ self.kernel32.CloseHandle(self.wintypes.HANDLE(handle))
1656
+ raise
1657
+ # The CRT adopts handles in text mode, where os.write turns "\n" into "\r\n".
1658
+ self.msvcrt.setmode(descriptor, os.O_BINARY)
1659
+ return descriptor
1660
+
1661
+ def open_root(self, path: Path) -> int:
1662
+ handle = self.kernel32.CreateFileW(
1663
+ str(path), _DIRECTORY_ACCESS, _PIN_SHARE, None, _OPEN_EXISTING,
1664
+ _FILE_FLAG_BACKUP_SEMANTICS | _FILE_FLAG_OPEN_REPARSE_POINT, None)
1665
+ if handle is None or handle == self.wintypes.HANDLE(-1).value:
1666
+ raise self.ctypes.WinError(self.ctypes.get_last_error())
1667
+ return self.adopt(handle)
1668
+
1669
+ def open_relative(
1670
+ self, parent_fd: int, name: str, access: int, share: int, disposition: int, options: int
1671
+ ) -> int:
1672
+ """Open name inside the directory held by parent_fd, never following a reparse point."""
1673
+ ctypes = self.ctypes
1674
+ buffer = ctypes.create_unicode_buffer(name)
1675
+ length = len(name) * ctypes.sizeof(ctypes.c_wchar)
1676
+ object_name = self.UnicodeString(length, length + ctypes.sizeof(ctypes.c_wchar),
1677
+ ctypes.cast(buffer, self.wintypes.LPWSTR))
1678
+ attributes = self.ObjectAttributes(
1679
+ ctypes.sizeof(self.ObjectAttributes), self.msvcrt.get_osfhandle(parent_fd),
1680
+ ctypes.pointer(object_name), _OBJ_CASE_INSENSITIVE, None, None)
1681
+ handle = self.wintypes.HANDLE()
1682
+ status = self.ntdll.NtCreateFile(
1683
+ ctypes.byref(handle), access | _SYNCHRONIZE, ctypes.byref(attributes),
1684
+ ctypes.byref(self.IoStatusBlock()), None, 0, share, disposition,
1685
+ options | _FILE_SYNCHRONOUS_IO_NONALERT | _FILE_OPEN_REPARSE_POINT, None, 0)
1686
+ if status < 0:
1687
+ raise ctypes.WinError(self.ntdll.RtlNtStatusToDosError(status))
1688
+ return self.adopt(handle.value)
1689
+
1690
+ def rename_no_replace(self, fd: int, directory_fd: int, name: str) -> None:
1691
+ """Rename the file held by fd to name inside the directory held by directory_fd.
1692
+
1693
+ The renameat equivalent; fails when name already exists.
1694
+ """
1695
+ ctypes, wintypes = self.ctypes, self.wintypes
1696
+
1697
+ class RenameInformation(ctypes.Structure):
1698
+ _fields_ = [("ReplaceIfExists", wintypes.BOOLEAN), ("RootDirectory", wintypes.HANDLE),
1699
+ ("FileNameLength", wintypes.ULONG),
1700
+ ("FileName", wintypes.WCHAR * (len(name) + 1))]
1701
+
1702
+ info = RenameInformation(False, self.msvcrt.get_osfhandle(directory_fd),
1703
+ len(name) * ctypes.sizeof(ctypes.c_wchar), name)
1704
+ status = self.ntdll.NtSetInformationFile(
1705
+ self.msvcrt.get_osfhandle(fd), ctypes.byref(self.IoStatusBlock()),
1706
+ ctypes.byref(info), ctypes.sizeof(info), _FILE_RENAME_INFORMATION)
1707
+ if status < 0:
1708
+ raise ctypes.WinError(self.ntdll.RtlNtStatusToDosError(status))
1709
+
1710
+ def delete_on_close(self, fd: int) -> None:
1711
+ """Delete exactly the file held by fd when it closes, whatever its name is now."""
1712
+ ctypes = self.ctypes
1713
+ flag = ctypes.c_ubyte(1)
1714
+ if not self.kernel32.SetFileInformationByHandle(
1715
+ self.msvcrt.get_osfhandle(fd), _FILE_DISPOSITION_INFO,
1716
+ ctypes.byref(flag), ctypes.sizeof(flag)):
1717
+ raise ctypes.WinError(ctypes.get_last_error())
1718
+
1719
+
1720
+ _WINDOWS_HANDLES: Optional[_WindowsHandles] = None
1721
+
1722
+
1723
+ def get_windows_handles() -> _WindowsHandles:
1724
+ global _WINDOWS_HANDLES
1725
+ if _WINDOWS_HANDLES is None:
1726
+ _WINDOWS_HANDLES = _WindowsHandles()
1727
+ return _WINDOWS_HANDLES
1728
+
1729
+
1730
+ def _windows_missing_ok(error: OSError) -> bool:
1731
+ return getattr(error, "winerror", None) in {2, 3}
1732
+
1733
+
1734
+ def read_windows_anchored_evidence(
1735
+ relative: Path,
1736
+ root: Path,
1737
+ *,
1738
+ missing_ok: bool,
1739
+ evidence_name: str,
1740
+ ) -> Optional[str]:
1741
+ win = get_windows_handles()
1742
+ descriptors: list[int] = []
1743
+ try:
1744
+ descriptors.append(win.open_root(root))
1745
+ opened_root = os.fstat(descriptors[-1])
1746
+ if not stat.S_ISDIR(opened_root.st_mode):
1747
+ raise DetectError(f"repo evidence is not a directory: {root}")
1748
+ for index, part in enumerate(relative.parts):
1749
+ current = root.joinpath(*relative.parts[: index + 1])
1750
+ final = index == len(relative.parts) - 1
1751
+ access = _GENERIC_READ if final else _DIRECTORY_ACCESS
1752
+ options = _FILE_NON_DIRECTORY_FILE if final else _FILE_DIRECTORY_FILE
1753
+ try:
1754
+ descriptor = win.open_relative(
1755
+ descriptors[-1],
1756
+ part,
1757
+ access,
1758
+ _PIN_SHARE,
1759
+ _FILE_OPEN,
1760
+ options,
1761
+ )
1762
+ except OSError as error:
1763
+ if missing_ok and _windows_missing_ok(error):
1764
+ return None
1765
+ raise DetectError(
1766
+ f"filesystem evidence disappeared: {root / relative}"
1767
+ ) from error
1768
+ descriptors.append(descriptor)
1769
+ actual = os.fstat(descriptor)
1770
+ if is_link_like(current, actual):
1771
+ raise DetectError(
1772
+ f"link-like {evidence_name} evidence is not allowed: {current}"
1773
+ )
1774
+ if final and not stat.S_ISREG(actual.st_mode):
1775
+ raise DetectError(
1776
+ f"{evidence_name} evidence is not a regular file: {current}"
1777
+ )
1778
+ if not final and not stat.S_ISDIR(actual.st_mode):
1779
+ raise DetectError(
1780
+ f"{evidence_name} evidence parent is not a directory: {current}"
1781
+ )
1782
+ with os.fdopen(descriptors.pop(), "rb") as stream:
1783
+ data = stream.read()
1784
+ except DetectError:
1785
+ raise
1786
+ except OSError as error:
1787
+ raise DetectError(
1788
+ f"cannot read {evidence_name} evidence: {root / relative}"
1789
+ ) from error
1790
+ finally:
1791
+ for descriptor in reversed(descriptors):
1792
+ os.close(descriptor)
1793
+ try:
1794
+ return data.decode("utf-8")
1795
+ except UnicodeDecodeError as error:
1796
+ raise DetectError(
1797
+ f"cannot read {evidence_name} evidence: {root / relative}"
1798
+ ) from error
1799
+
1800
+
1801
+ def iter_worktree_files_windows_anchored(root: Path) -> Iterable[str]:
1802
+ win = get_windows_handles()
1803
+ try:
1804
+ root_fd = win.open_root(root)
1805
+ except OSError as error:
1806
+ raise DetectError(f"cannot traverse filesystem evidence: {error}") from error
1807
+ try:
1808
+ root_status = os.fstat(root_fd)
1809
+ if not stat.S_ISDIR(root_status.st_mode):
1810
+ raise DetectError(f"repo evidence is not a directory: {root}")
1811
+ yield from iter_from_windows_dir_fd(root_fd, "", root, win)
1812
+ finally:
1813
+ os.close(root_fd)
1814
+
1815
+
1816
+ def iter_from_windows_dir_fd(
1817
+ dir_fd: int, relative_dir: str, root: Path, win: _WindowsHandles
1818
+ ) -> Iterable[str]:
1819
+ scan_dir = root if not relative_dir else root.joinpath(*PurePosixPath(relative_dir).parts)
1820
+ try:
1821
+ # Windows CPython cannot listdir(dir_fd); paths come from the pinned walk.
1822
+ names = os.listdir(scan_dir)
1823
+ except OSError as error:
1824
+ raise DetectError(f"cannot traverse filesystem evidence: {error}") from error
1825
+ for name in names:
1826
+ relative = f"{relative_dir}/{name}" if relative_dir else name
1827
+ if is_ignored(relative) or name == ".project":
1828
+ continue
1829
+ path = root.joinpath(*PurePosixPath(relative).parts)
1830
+ try:
1831
+ child_fd = win.open_relative(
1832
+ dir_fd,
1833
+ name,
1834
+ _DIRECTORY_ACCESS,
1835
+ _PIN_SHARE,
1836
+ _FILE_OPEN,
1837
+ _FILE_DIRECTORY_FILE,
1838
+ )
1839
+ except OSError:
1840
+ child_fd = None
1841
+ else:
1842
+ status = os.fstat(child_fd)
1843
+ if is_link_like(path, status):
1844
+ os.close(child_fd)
1845
+ continue
1846
+ if stat.S_ISDIR(status.st_mode):
1847
+ try:
1848
+ if is_verified_skill_bundle_at(relative, child_fd, root):
1849
+ continue
1850
+ yield from iter_from_windows_dir_fd(
1851
+ child_fd, relative, root, win
1852
+ )
1853
+ finally:
1854
+ os.close(child_fd)
1855
+ continue
1856
+ os.close(child_fd)
1857
+ try:
1858
+ file_fd = win.open_relative(
1859
+ dir_fd,
1860
+ name,
1861
+ _FILE_READ_ATTRIBUTES,
1862
+ _PIN_SHARE,
1863
+ _FILE_OPEN,
1864
+ _FILE_NON_DIRECTORY_FILE,
1865
+ )
1866
+ except OSError as error:
1867
+ raise DetectError(
1868
+ f"cannot inspect filesystem evidence: {relative}: {error}"
1869
+ ) from error
1870
+ try:
1871
+ status = os.fstat(file_fd)
1872
+ if is_link_like(path, status) or not stat.S_ISREG(status.st_mode):
1873
+ continue
1874
+ if is_fixed_managed_pipeline_artifact(relative):
1875
+ continue
1876
+ yield relative
1877
+ finally:
1878
+ os.close(file_fd)
1879
+
1880
+
1881
+ def windows_project_entries(project: Path) -> set:
1882
+ """project_entries for a .project held open without delete sharing."""
1883
+ entries = set(os.listdir(project))
1884
+ for name in entries & _common.OS_JUNK_NAMES:
1885
+ try:
1886
+ status = os.stat(project / name, follow_symlinks=False)
1887
+ except FileNotFoundError:
1888
+ continue
1889
+ if ignored_junk(project, name, status):
1890
+ entries.discard(name)
1891
+ return entries
1892
+
1893
+
1894
+ def write_state_windows(
1895
+ root: Path,
1896
+ content: str,
1897
+ expected_root: os.stat_result,
1898
+ expected_project: Optional[os.stat_result],
1899
+ ) -> None:
1900
+ win = _WindowsHandles()
1901
+ payload = content.encode("utf-8")
1902
+ project = root / ".project"
1903
+ root_fd = project_fd = state_fd = None
1904
+ delete_on_failure = False
1905
+ # The same lock pipeline_state takes for this track.
1906
+ with _common.directory_mutex(project):
1907
+ try:
1908
+ try:
1909
+ root_fd = win.open_root(root)
1910
+ except OSError as error:
1911
+ raise DetectError(f"cannot open repo for STATE.md creation: {error}") from error
1912
+ opened_root = os.fstat(root_fd)
1913
+ if (
1914
+ is_link_like(root, opened_root)
1915
+ or not stat.S_ISDIR(opened_root.st_mode)
1916
+ or not os.path.samestat(expected_root, opened_root)
1917
+ ):
1918
+ raise DetectError("repo changed after classification")
1919
+ try:
1920
+ project_fd = win.open_relative(
1921
+ root_fd, ".project", _DIRECTORY_ACCESS, _PIN_SHARE,
1922
+ _FILE_CREATE if expected_project is None else _FILE_OPEN,
1923
+ _FILE_DIRECTORY_FILE)
1924
+ except FileExistsError as error:
1925
+ raise DetectError(".project changed after classification") from error
1926
+ except OSError as error:
1927
+ raise DetectError(f"cannot open .project for STATE.md creation: {error}") from error
1928
+ opened_project = os.fstat(project_fd)
1929
+ if is_link_like(project, opened_project) or not stat.S_ISDIR(opened_project.st_mode):
1930
+ raise DetectError("cannot create state through a non-directory .project")
1931
+ if expected_project is not None and not os.path.samestat(expected_project, opened_project):
1932
+ raise DetectError(".project changed after classification")
1933
+ if windows_project_entries(project) not in (set(), {STATE_TEMP_NAME}):
1934
+ raise DetectError(".project changed after classification")
1935
+ try:
1936
+ # Share mode 0: nothing else may open the temporary file while it is held.
1937
+ state_fd = win.open_relative(
1938
+ project_fd, STATE_TEMP_NAME, _GENERIC_READ | _GENERIC_WRITE | _DELETE,
1939
+ 0, _FILE_OPEN_IF, _FILE_NON_DIRECTORY_FILE)
1940
+ except OSError as error:
1941
+ raise DetectError(f"cannot reserve temporary STATE.md: {error}") from error
1942
+ created_status = os.fstat(state_fd)
1943
+ if is_link_like(project / STATE_TEMP_NAME, created_status) or not stat.S_ISREG(
1944
+ created_status.st_mode
1945
+ ):
1946
+ raise DetectError("temporary STATE.md is not a regular file")
1947
+ if created_status.st_nlink != 1:
1948
+ raise DetectError("temporary STATE.md has an unexpected link count")
1949
+ delete_on_failure = True
1950
+ if "STATE.md" in os.listdir(project):
1951
+ raise DetectError("STATE.md already exists")
1952
+ os.ftruncate(state_fd, 0)
1953
+ write_all(state_fd, payload)
1954
+ os.fsync(state_fd)
1955
+ current_root = lstat_evidence(root, missing_ok=False)
1956
+ current_project = lstat_evidence(project, missing_ok=False)
1957
+ if (
1958
+ is_link_like(root, current_root)
1959
+ or not os.path.samestat(opened_root, current_root)
1960
+ or is_link_like(project, current_project)
1961
+ or not os.path.samestat(opened_project, current_project)
1962
+ ):
1963
+ raise DetectError("project identity changed while creating STATE.md")
1964
+ current_state = os.fstat(state_fd)
1965
+ if current_state.st_nlink != 1 or not os.path.samestat(created_status, current_state):
1966
+ raise DetectError("temporary STATE.md changed while writing")
1967
+ if windows_project_entries(project) != {STATE_TEMP_NAME}:
1968
+ raise DetectError(".project contents changed while creating STATE.md")
1969
+ try:
1970
+ win.rename_no_replace(state_fd, project_fd, "STATE.md")
1971
+ except OSError as error:
1972
+ if "STATE.md" in os.listdir(project):
1973
+ raise DetectError("STATE.md already exists") from error
1974
+ raise DetectError(f"cannot publish STATE.md: {error}") from error
1975
+ published = lstat_evidence(project / "STATE.md", missing_ok=False)
1976
+ if (
1977
+ is_link_like(project / "STATE.md", published)
1978
+ or published.st_nlink != 1
1979
+ or not os.path.samestat(created_status, published)
1980
+ ):
1981
+ raise DetectError("STATE.md changed while publishing")
1982
+ if windows_project_entries(project) != {"STATE.md"}:
1983
+ raise DetectError(".project contents changed while publishing STATE.md")
1984
+ delete_on_failure = False
1985
+ closing_state_fd, state_fd = state_fd, None
1986
+ try:
1987
+ os.close(closing_state_fd)
1988
+ except OSError as error:
1989
+ raise DetectError(f"cannot close STATE.md: {error}") from error
1990
+ except BaseException as error:
1991
+ if delete_on_failure and state_fd is not None:
1992
+ try:
1993
+ win.delete_on_close(state_fd)
1994
+ except OSError as rollback_error:
1995
+ raise DetectError(
1996
+ f"cannot roll back STATE.md: {rollback_error}") from error
1997
+ if isinstance(error, DetectError):
1998
+ raise
1999
+ if isinstance(error, OSError):
2000
+ raise DetectError(f"cannot create STATE.md: {error}") from error
2001
+ raise
2002
+ finally:
2003
+ close_file_descriptors(state_fd, project_fd, root_fd)
2004
+
2005
+
1451
2006
  def initialize(
1452
2007
  repo: Path, template: Path, phase: Optional[str] = None, *, require_git: bool = False
1453
2008
  ) -> dict:
@@ -1471,7 +2026,7 @@ def initialize(
1471
2026
  if require_git:
1472
2027
  git = subprocess.run(
1473
2028
  ["git", "-C", str(root), "rev-parse", "--is-inside-work-tree"],
1474
- env=git_environment(), capture_output=True, text=True, check=False,
2029
+ env=git_environment(), capture_output=True, encoding="utf-8", errors="replace", check=False,
1475
2030
  )
1476
2031
  if git.returncode != 0 or git.stdout.strip() != "true":
1477
2032
  return {**payload, "route": "setup-repository", "wrote_state": False}
@@ -1482,7 +2037,7 @@ def initialize(
1482
2037
  raise DetectError(
1483
2038
  f"initialize phase {phase} does not match verdict {payload['verdict']}"
1484
2039
  )
1485
- if not ANCHORED_STATE_CREATE_SUPPORTED:
2040
+ if not STATE_CREATE_SUPPORTED:
1486
2041
  payload["wrote_state"] = False
1487
2042
  payload["error"] = "anchored no-follow STATE.md creation is unavailable"
1488
2043
  return payload