@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
@@ -2,15 +2,18 @@
2
2
  """Install GSD Path skills for supported coding agents."""
3
3
 
4
4
  import argparse
5
+ import difflib
5
6
  import errno
6
7
  import hashlib
7
8
  import json
8
9
  import os
9
10
  import re
11
+ import shlex
10
12
  import shutil
11
13
  import subprocess
12
14
  import sys
13
15
  import tempfile
16
+ import time
14
17
  from dataclasses import dataclass, field
15
18
  from pathlib import Path
16
19
  from typing import Callable, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
@@ -45,6 +48,10 @@ LOCAL_ROOTS = local_roots_for_manifest(sync_skill_resources.RESOURCE_MANIFEST)
45
48
  SKILL_NAMES = skill_names_for_manifest(sync_skill_resources.RESOURCE_MANIFEST)
46
49
  SKILL_ALIASES = dict(sync_skill_resources.RESOURCE_MANIFEST["skill_aliases"])
47
50
  ROUTER_ALIASES = dict(sync_skill_resources.RESOURCE_MANIFEST["router_aliases"])
51
+ LOCAL_EDITS_NOTE = (
52
+ "; local edits inside these skills are not carried forward "
53
+ "(see UPDATE.md#local-additions)"
54
+ )
48
55
  CLAUDE_BRIDGE = "@../AGENTS.md\n@../WORKFLOW.md\n"
49
56
  HOOKS_DIRECTORY = ".gsd-path"
50
57
  GUARD_SCRIPTS = ("guard_hook.py", "git_guard.py")
@@ -56,6 +63,21 @@ PROJECT_STATUS_MARKER = "gsd-path project status launcher"
56
63
  INSTALL_LOCK_NAME = ".gsd-path-install-lock"
57
64
  INSTALL_LOCK_OWNER = "owner.json"
58
65
  INSTALL_LOCK_SCHEMA = "gsd-path/install-lock/v2"
66
+ AGENTS_BEGIN = "<!-- gsd-path:begin -->"
67
+ AGENTS_END = "<!-- gsd-path:end -->"
68
+ # Codex default project_doc_max_bytes; it silently cuts the rest of AGENTS.md.
69
+ CODEX_DOC_LIMIT = 32768
70
+ # sha256 of each released whole-file AGENTS.md (v1.0.0, v1.1.0, v1.2.0-v1.3.1).
71
+ RELEASED_AGENTS = frozenset(
72
+ {
73
+ "dfc969299e10c4430742c66c273436136a813d75e332cecf9403f94c904b330b",
74
+ "3a5c4ed62f397725acce962a7fdfafd5a3296f0a784acafc55454b5304a4eb7c",
75
+ "831de61d4b4b624319930162d45ac88c49cc13ca22894f37733ba68e6503f972",
76
+ }
77
+ )
78
+ LEGACY_AGENTS_TITLE = "# AGENTS.md — Operating Rules for the GSD Path Pipeline"
79
+ # Every released whole-file AGENTS.md carries this marker.
80
+ LEGACY_AGENTS_MARKER = "<!-- gsd-path/plain-prompt-reentry/v1 -->"
59
81
  PROJECT_CONTRACTS = (
60
82
  ("AGENTS.md", "## Plain-prompt re-entry"),
61
83
  ("WORKFLOW.md", "### Plain-prompt re-entry"),
@@ -102,7 +124,7 @@ STATUS_STATE_FIELDS = frozenset(
102
124
  "integration_source",
103
125
  }
104
126
  )
105
- STATUS_INTEGRATION_MODES = frozenset({"direct", "pull-request"})
127
+ STATUS_INTEGRATION_MODES = frozenset({"direct", "pull-request", "external-landing"})
106
128
  STATUS_INTEGRATION_SOURCES = frozenset({"default", "milestone"})
107
129
  STATUS_SLUG = re.compile(r"^[a-z0-9][a-z0-9-]*$")
108
130
  STATUS_BRANCH = re.compile(r"^gsd-path/M(\d{3,})$")
@@ -231,19 +253,149 @@ GIT_HOOKS = (
231
253
  ("pre-push", pre_push_hook),
232
254
  )
233
255
  GIT_HOOK_NAMES = tuple(name for name, _ in GIT_HOOKS)
256
+ MEMBER_HOOK_MARKER = "gsd-path member guard"
257
+ CHAINED_HOOK_SUFFIX = ".gsd-path-chained"
258
+
259
+
260
+ def member_hook(interpreter: str, name: str) -> str:
261
+ """A member hook finds its coordinator through the member marker, so a
262
+ moved coordinator needs only `members.py repair`, then runs any chained hook."""
263
+ guard = f'{interpreter} "$coordinator/{HOOKS_DIRECTORY}/git_guard.py" {name} "$@"'
264
+ lines = [
265
+ "#!/bin/sh",
266
+ f"# {MEMBER_HOOK_MARKER}: runs the coordinator's git_guard.py named by this",
267
+ "# repository's member marker, then any chained hook.",
268
+ "common=$(git rev-parse --path-format=absolute --git-common-dir) || exit 1",
269
+ f"coordinator=$({interpreter} -c 'import json, sys; print(json.load(open(sys.argv[1]))[\"coordinator\"])' "
270
+ '"$common/gsd-path/member.json") || {',
271
+ ' echo "gsd-path guard: member marker is unreadable; run members.py repair --repo <coordinator>" >&2',
272
+ " exit 1",
273
+ "}",
274
+ f'chained="$(dirname "$0")/{name}{CHAINED_HOOK_SUFFIX}"',
275
+ ]
276
+ if name == "pre-push":
277
+ # Git gives pre-push its ref updates on stdin; both hooks need them.
278
+ lines += [
279
+ "input=$(cat)",
280
+ '[ -n "$input" ] && input="$input\n"',
281
+ f'printf %s "$input" | {guard} || exit 1',
282
+ 'if [ -x "$chained" ]; then printf %s "$input" | exec "$chained" "$@"; fi',
283
+ ]
284
+ else:
285
+ lines += [f"{guard} || exit 1", 'if [ -x "$chained" ]; then exec "$chained" "$@"; fi']
286
+ return "\n".join(lines) + "\n"
287
+
288
+
289
+ def install_member_hooks(coordinator: Path, member: Path, dry_run: bool = False) -> List[str]:
290
+ """Install git hooks that hold a member repo to its coordinator's guard.
291
+
292
+ Unmanaged hooks are kept as `<hook>.gsd-path-chained` and run after the
293
+ guard passes; the member's own single-repo Path hooks are replaced.
294
+ """
295
+ _validate_project_git_root(member)
296
+ # Imported here so other installer commands keep their small import closure.
297
+ try:
298
+ from . import members
299
+ except ImportError:
300
+ import members
301
+ try:
302
+ role = members.member_role(member)
303
+ except members.MembersError as error:
304
+ raise InstallerError(str(error)) from error
305
+ if role is None or not _same_path(Path(role["coordinator"]), coordinator):
306
+ raise InstallerError(f"{member} is not a member of {coordinator}; run members.py add first")
307
+ guard = coordinator / HOOKS_DIRECTORY / "git_guard.py"
308
+ if guard.is_symlink() or not _is_managed_guard_script(guard):
309
+ raise InstallerError(f"coordinator guard is not installed: {guard}")
310
+ hooks_dir = _resolve_git_hooks_path(member)
311
+ if hooks_dir is None:
312
+ raise InstallerError(f"cannot resolve the git hooks directory of {member}")
313
+ common = Path(subprocess.run(
314
+ ["git", "rev-parse", "--path-format=absolute", "--git-common-dir"],
315
+ cwd=os.fspath(member), capture_output=True, encoding="utf-8", errors="replace", check=True,
316
+ ).stdout.strip()).resolve()
317
+ resolved = hooks_dir.resolve()
318
+ worktree_output = subprocess.run(
319
+ ["git", "worktree", "list", "--porcelain", "-z"],
320
+ cwd=os.fspath(member), capture_output=True, check=True,
321
+ ).stdout
322
+ worktrees = [
323
+ Path(os.fsdecode(field[len(b"worktree "):])).resolve()
324
+ for field in worktree_output.split(b"\0") if field.startswith(b"worktree ")
325
+ ]
326
+ if not worktrees or (
327
+ any(resolved.is_relative_to(root) for root in worktrees)
328
+ and not resolved.is_relative_to(common)
329
+ ):
330
+ raise InstallerError(
331
+ f"git hooks path is inside the member worktree: {hooks_dir}; "
332
+ "point core.hooksPath outside the worktree or unset it"
333
+ )
334
+ interpreter = _required_python_runtime("--member-of")
335
+ plan = []
336
+ for name in GIT_HOOK_NAMES:
337
+ hook, chained = hooks_dir / name, hooks_dir / f"{name}{CHAINED_HOOK_SUFFIX}"
338
+ if hook.is_symlink() or chained.is_symlink():
339
+ raise InstallerError(f"refusing a symlinked git hook: {hook}")
340
+ if _lexists(hook) and not hook.is_file():
341
+ raise InstallerError(f"git hook is not a regular file: {hook}")
342
+ text = hook.read_text(encoding="utf-8", errors="replace") if hook.is_file() else None
343
+ chain = (
344
+ text is not None
345
+ and MEMBER_HOOK_MARKER not in text
346
+ and not _is_managed_git_hook_content(text)
347
+ )
348
+ if chain and _lexists(chained):
349
+ raise InstallerError(f"cannot chain {hook}: {chained} already exists")
350
+ plan.append((name, hook, chained, chain))
351
+ lines = []
352
+ for name, hook, chained, chain in plan:
353
+ note = " (keeps the existing hook as a chained hook)" if chain else ""
354
+ lines.append(f"member hook: {hook}{note}" + (" (dry run)" if dry_run else ""))
355
+ if dry_run:
356
+ return lines
357
+ hooks_dir.mkdir(parents=True, exist_ok=True)
358
+ staged = []
359
+ try:
360
+ for name, _, _, _ in plan:
361
+ temporary = hooks_dir / f".{name}.gsd-path-tmp"
362
+ # O_EXCL already refuses any existing name; O_NOFOLLOW (absent on
363
+ # Windows) adds defense in depth where it exists.
364
+ flags = (os.O_WRONLY | os.O_CREAT | os.O_EXCL
365
+ | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_BINARY", 0))
366
+ fd = os.open(temporary, flags, 0o755)
367
+ staged.append(temporary)
368
+ with os.fdopen(fd, "wb") as output:
369
+ if hasattr(os, "fchmod"): # Windows before 3.13; Git for Windows needs no exec bit.
370
+ os.fchmod(output.fileno(), 0o755)
371
+ output.write(member_hook(interpreter, name).encode("utf-8"))
372
+ for name, hook, chained, chain in plan:
373
+ if chain:
374
+ os.replace(hook, chained)
375
+ os.replace(hooks_dir / f".{name}.gsd-path-tmp", hook)
376
+ finally:
377
+ for temporary in staged:
378
+ temporary.unlink(missing_ok=True)
379
+ return lines
380
+
381
+
382
+ # Every interpreter a managed hook may be pinned to; each is a command prefix.
383
+ INTERPRETER_CANDIDATES = ("python3", "python", "py -3")
234
384
 
235
385
 
236
386
  def _detect_python_interpreter() -> Optional[str]:
237
- """Probe for a runnable interpreter (python3, then python).
387
+ """Probe for a runnable interpreter (python3, then python, then py -3).
238
388
 
239
389
  Emitted hooks must never hard-code an interpreter that does not exist on
240
- this machine (python3 is typically absent on Windows).
390
+ this machine (python3 is typically absent on Windows, and python.org
391
+ installs may put only the py launcher on PATH). The result is a command
392
+ prefix; split it before spawning.
241
393
  """
242
- for candidate in ("python3", "python"):
394
+ for candidate in INTERPRETER_CANDIDATES:
243
395
  try:
244
396
  result = subprocess.run(
245
397
  [
246
- candidate,
398
+ *candidate.split(),
247
399
  "-B",
248
400
  "-c",
249
401
  "import sys; raise SystemExit(sys.version_info < (3, 9))",
@@ -277,7 +429,7 @@ def _resolve_git_hooks_path(project: "Path") -> Optional["Path"]:
277
429
  ["git", "rev-parse", "--show-toplevel", "--git-path", "hooks"],
278
430
  cwd=os.fspath(project),
279
431
  capture_output=True,
280
- text=True,
432
+ encoding="utf-8", errors="replace",
281
433
  check=False,
282
434
  )
283
435
  except OSError:
@@ -353,9 +505,9 @@ HOST_NOTES = {
353
505
  "kiro": KIRO_NOTE,
354
506
  }
355
507
  EXPLICIT_ONLY_TARGETS = frozenset(
356
- {"claude", "grok", "copilot", "qwen", "cursor", "zed", "kimi", "shared-agents"}
508
+ {"claude", "grok", "copilot", "qwen", "cursor", "zed", "kimi", "muse", "shared-agents"}
357
509
  )
358
- SHARED_AGENT_TARGETS = frozenset({"codex", "antigravity", "zed"})
510
+ SHARED_AGENT_TARGETS = frozenset({"codex", "antigravity", "zed", "muse"})
359
511
  SHARED_AGENT_PROFILE = "shared-agents"
360
512
  CURSOR_AGENT_FILENAME = "gsd-path.md"
361
513
  CURSOR_AGENT_BACKUP_NAME = "cursor-agent-gsd-path.md"
@@ -381,7 +533,7 @@ def _shared_invocations(text: str) -> str:
381
533
  others = f"{hosts['antigravity']['invocation_prefix']}{skill}"
382
534
  return (
383
535
  f"{quote}{codex}{arguments}{quote} (Codex) or "
384
- f"{quote}{others}{arguments}{quote} (Antigravity/Zed)"
536
+ f"{quote}{others}{arguments}{quote} (Antigravity/Zed/Muse)"
385
537
  )
386
538
 
387
539
  return re.sub(
@@ -390,7 +542,7 @@ def _shared_invocations(text: str) -> str:
390
542
  r"(?: \(Codex\) (?:and|or) (?P<paired_quote>`?)/"
391
543
  r"(?P<paired_skill>gsd-path(?:-[a-z0-9]+)*|path)"
392
544
  r"(?P<paired_arguments> status)?(?P=paired_quote)"
393
- r" \((?:other hosts|Antigravity/Zed)\))?",
545
+ r" \((?:other hosts|Antigravity/Zed/Muse)\))?",
394
546
  replace,
395
547
  text,
396
548
  )
@@ -468,7 +620,7 @@ def default_root(target: str, environ: Optional[Mapping[str, str]] = None) -> Pa
468
620
  return absolute_path(Path("~/.gemini/antigravity-cli/skills"))
469
621
  if target == "cursor":
470
622
  return absolute_path(Path("~/.cursor/skills"))
471
- if target == "zed":
623
+ if target in ("zed", "muse"):
472
624
  return absolute_path(Path("~/.agents/skills"))
473
625
  if target == "kiro":
474
626
  return absolute_path(Path(env.get("KIRO_HOME") or "~/.kiro") / "skills")
@@ -504,7 +656,15 @@ def _is_owned_router_alias(skill_dir: Path) -> bool:
504
656
  version = version_file.read_text(encoding="utf-8").strip()
505
657
  except (OSError, UnicodeError):
506
658
  return False
507
- return re.fullmatch(r"[0-9]+(?:\.[0-9]+)+", version) is not None
659
+ return (
660
+ re.fullmatch(
661
+ r"[0-9]+(?:\.[0-9]+)+"
662
+ r"(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?"
663
+ r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?",
664
+ version,
665
+ )
666
+ is not None
667
+ )
508
668
 
509
669
 
510
670
  def _is_managed_install_entry(root: Path, name: str) -> bool:
@@ -534,7 +694,7 @@ def _stamp_installed_versions(staged_root: Path, version: Optional[str]) -> None
534
694
  if not version:
535
695
  return
536
696
  for name in {"gsd-path", *ROUTER_ALIASES}:
537
- (staged_root / name / "VERSION").write_text(f"{version}\n", encoding="utf-8")
697
+ (staged_root / name / "VERSION").write_bytes(f"{version}\n".encode("utf-8"))
538
698
 
539
699
 
540
700
  def _lexists(path: Path) -> bool:
@@ -670,7 +830,7 @@ def stage_target(source_root: Path, target: str, staged_root: Path) -> None:
670
830
  encoding="utf-8"
671
831
  )
672
832
  for dispatch in sorted(staged_root.glob("*/references/dispatch.md")):
673
- dispatch.write_text(adapter, encoding="utf-8")
833
+ dispatch.write_bytes(adapter.encode("utf-8"))
674
834
  if target == "cursor":
675
835
  shutil.copy2(
676
836
  source_root / "platforms" / "cursor" / "agent.md",
@@ -687,9 +847,8 @@ def stage_target(source_root: Path, target: str, staged_root: Path) -> None:
687
847
  path_invocation = "path" if target == "opencode" else "/path"
688
848
  for name in SKILL_NAMES:
689
849
  entrypoint = staged_root / name / "SKILL.md"
690
- entrypoint.write_text(
691
- _augment_frontmatter(entrypoint.read_text(encoding="utf-8"), target),
692
- encoding="utf-8",
850
+ entrypoint.write_bytes(
851
+ _augment_frontmatter(entrypoint.read_text(encoding="utf-8"), target).encode("utf-8"),
693
852
  )
694
853
  for markdown in sorted(staged_root.rglob("*.md")):
695
854
  content = markdown.read_text(encoding="utf-8")
@@ -698,7 +857,7 @@ def stage_target(source_root: Path, target: str, staged_root: Path) -> None:
698
857
  if target == SHARED_AGENT_PROFILE
699
858
  else content.replace("$gsd-path", invocation).replace("$path", path_invocation)
700
859
  )
701
- markdown.write_text(transformed, encoding="utf-8")
860
+ markdown.write_bytes(transformed.encode("utf-8"))
702
861
 
703
862
 
704
863
  def _missing_directories(path: Path) -> List[Path]:
@@ -735,7 +894,7 @@ def _process_identity(pid: int) -> Optional[str]:
735
894
  else:
736
895
  command = ["ps", "-o", "lstart=", "-p", str(pid)]
737
896
  try:
738
- result = subprocess.run(command, capture_output=True, text=True, check=False)
897
+ result = subprocess.run(command, capture_output=True, encoding="utf-8", errors="replace", check=False)
739
898
  except OSError:
740
899
  return None
741
900
  value = result.stdout.strip()
@@ -743,11 +902,39 @@ def _process_identity(pid: int) -> Optional[str]:
743
902
 
744
903
 
745
904
  def _process_alive(pid: int) -> bool:
905
+ """Whether pid names a running process, without signalling it.
906
+
907
+ Windows os.kill(pid, 0) sends CTRL_C_EVENT, so Windows asks the kernel.
908
+ status_runtime and install carry verbatim copies (they cannot import this
909
+ module); tests/test_common_platform.py keeps them identical.
910
+ """
911
+ if pid <= 0:
912
+ return False
913
+ if os.name != "nt":
914
+ try:
915
+ os.kill(pid, 0)
916
+ except OSError as error:
917
+ return error.errno == errno.EPERM
918
+ return True
919
+ import ctypes
920
+ from ctypes import wintypes
921
+
922
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
923
+ kernel32.OpenProcess.restype = wintypes.HANDLE
924
+ kernel32.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
925
+ kernel32.GetExitCodeProcess.argtypes = (wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD))
926
+ kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
927
+ handle = kernel32.OpenProcess(0x1000, False, pid) # PROCESS_QUERY_LIMITED_INFORMATION
928
+ if not handle:
929
+ # ERROR_ACCESS_DENIED: a protected process exists but refuses the query.
930
+ return ctypes.get_last_error() == 5
746
931
  try:
747
- os.kill(pid, 0)
748
- except OSError as error:
749
- return error.errno == errno.EPERM
750
- return True
932
+ code = wintypes.DWORD()
933
+ if not kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
934
+ return True
935
+ return code.value == 259 # STILL_ACTIVE
936
+ finally:
937
+ kernel32.CloseHandle(handle)
751
938
 
752
939
 
753
940
  def _stale_install_lock_snapshot(lock: Path) -> Tuple[os.stat_result, bytes]:
@@ -784,6 +971,31 @@ def _stale_install_lock_snapshot(lock: Path) -> Tuple[os.stat_result, bytes]:
784
971
  return observed, owner_bytes
785
972
 
786
973
 
974
+ # Access denied and sharing violation: on Windows a scanner or indexer that
975
+ # briefly opens a freshly written file blocks moving it or its directory.
976
+ _WINDOWS_BUSY_ERRORS = (5, 32)
977
+ _MOVE_ATTEMPTS = 20
978
+
979
+
980
+ def _move_retrying(move: Callable[[Path, Path], object], source: Path, destination: Path) -> None:
981
+ """Run ``move``, retried on Windows while another process briefly holds ``source``."""
982
+ if os.name != "nt":
983
+ move(source, destination)
984
+ return
985
+ for attempt in range(_MOVE_ATTEMPTS):
986
+ try:
987
+ move(source, destination)
988
+ return
989
+ except PermissionError as error:
990
+ # A directory moved onto an existing directory also reports access
991
+ # denied; that is not transient, so never retry it.
992
+ if (getattr(error, "winerror", None) not in _WINDOWS_BUSY_ERRORS
993
+ or os.path.isdir(destination)
994
+ or attempt == _MOVE_ATTEMPTS - 1):
995
+ raise
996
+ time.sleep(min(0.01 * 2 ** attempt, 0.25))
997
+
998
+
787
999
  def _recover_stale_install_lock(lock: Path, quarantine: Path) -> Optional[Path]:
788
1000
  legacy_quarantine = lock.with_name(f"{lock.name}.stale")
789
1001
  if not _lexists(lock):
@@ -839,19 +1051,18 @@ def _create_install_lock(lock: Path) -> None:
839
1051
  quarantine = None
840
1052
  published = False
841
1053
  try:
842
- (staging / INSTALL_LOCK_OWNER).write_text(
843
- json.dumps(
1054
+ (staging / INSTALL_LOCK_OWNER).write_bytes(
1055
+ (json.dumps(
844
1056
  {
845
1057
  "schema": INSTALL_LOCK_SCHEMA,
846
1058
  "pid": os.getpid(),
847
1059
  "identity": identity,
848
1060
  }
849
1061
  )
850
- + "\n",
851
- encoding="utf-8",
1062
+ + "\n").encode("utf-8"),
852
1063
  )
853
1064
  quarantine = _recover_stale_install_lock(lock, recovery)
854
- staging.rename(lock)
1065
+ _move_retrying(Path.rename, staging, lock)
855
1066
  published = True
856
1067
  if quarantine is not None:
857
1068
  shutil.rmtree(quarantine)
@@ -926,7 +1137,7 @@ def _backup_existing(
926
1137
  for entry, backup_name in existing:
927
1138
  stored = transaction.backup / backup_name
928
1139
  transaction.moved.append((entry, stored))
929
- os.replace(entry, stored)
1140
+ _move_retrying(os.replace, entry, stored)
930
1141
 
931
1142
 
932
1143
  def _reserve_directory(path: Path) -> None:
@@ -987,7 +1198,7 @@ def _rollback_target(transaction: TargetTransaction) -> None:
987
1198
  _remove_path(destination)
988
1199
  for original, stored in reversed(transaction.moved):
989
1200
  if _lexists(stored):
990
- os.replace(stored, original)
1201
+ _move_retrying(os.replace, stored, original)
991
1202
  if transaction.backup is not None:
992
1203
  try:
993
1204
  transaction.backup.rmdir()
@@ -1082,7 +1293,8 @@ def _update_replacement(
1082
1293
  project: Path, destination: Path, hooks_dir: Optional[Path]
1083
1294
  ) -> Optional[Callable[[Path], bool]]:
1084
1295
  """--update replaces managed runtime, guard, and git hook files in place
1085
- and keeps everything else (AGENTS.md, WORKFLOW.md, CLAUDE.md). Returns
1296
+ and keeps everything else (WORKFLOW.md, CLAUDE.md); AGENTS.md has its own
1297
+ block merge. Returns
1086
1298
  the managed-file check for a replaceable destination, or None for a
1087
1299
  kept one."""
1088
1300
  parent = project / HOOKS_DIRECTORY
@@ -1143,6 +1355,7 @@ def _project_result(
1143
1355
  ):
1144
1356
  replaceable = (
1145
1357
  destination in mergers
1358
+ or destination == project / "AGENTS.md"
1146
1359
  or _update_replacement(project, destination, hooks_dir) is not None
1147
1360
  )
1148
1361
  bucket = kept if _lexists(destination) and not replaceable else refreshed
@@ -1155,13 +1368,140 @@ def _project_result(
1155
1368
 
1156
1369
 
1157
1370
  def _existing_contract_error(destination: Path) -> "InstallerError":
1158
- return InstallerError(
1371
+ message = (
1159
1372
  f"project contract already exists: {destination} — the installer never "
1160
- "overwrites project files. Run --update --project PATH to refresh managed "
1373
+ "overwrites project files."
1374
+ )
1375
+ document = destination.name in ("AGENTS.md", "WORKFLOW.md") or (
1376
+ destination.name == "CLAUDE.md" and destination.parent.name == ".claude"
1377
+ )
1378
+ if document:
1379
+ aside = destination.with_name(f"{destination.stem}.pre-path{destination.suffix}")
1380
+ if _lexists(aside):
1381
+ message += (
1382
+ f" First install: {aside} already exists; move the contract to an "
1383
+ f"unused name, rerun, then merge its rules into the new {destination.name}."
1384
+ )
1385
+ else:
1386
+ message += (
1387
+ f" First install: move it aside with `mv {shlex.quote(str(destination))} "
1388
+ f"{shlex.quote(str(aside))}`, rerun, then merge its rules into the new "
1389
+ f"{destination.name}."
1390
+ )
1391
+ message += " Already a GSD Path project:"
1392
+ return InstallerError(
1393
+ f"{message} Run --update --project PATH to refresh managed "
1161
1394
  "runtime files; merge template changes manually (see UPDATE.md)."
1162
1395
  )
1163
1396
 
1164
1397
 
1398
+ def _agents_block(template: str) -> str:
1399
+ return f"{AGENTS_BEGIN}\n{template.rstrip(chr(10))}\n{AGENTS_END}\n"
1400
+
1401
+
1402
+ def _split_agents(text: str, destination: Path) -> Optional[Tuple[str, str]]:
1403
+ """Owner text (before, after) around Path's block, or None without markers.
1404
+ Markers count only as whole lines; inline mentions are owner text."""
1405
+ begins = [m.start() for m in re.finditer(rf"(?m)^{re.escape(AGENTS_BEGIN)}\r?$", text)]
1406
+ ends = list(re.finditer(rf"(?m)^{re.escape(AGENTS_END)}\r?(?:\n|$)", text))
1407
+ if not begins and not ends:
1408
+ return None
1409
+ if len(begins) != 1 or len(ends) != 1 or ends[0].start() < begins[0]:
1410
+ begins, ends = len(begins), len(ends)
1411
+ raise InstallerError(
1412
+ f"{destination} needs exactly one {AGENTS_BEGIN} line followed by one "
1413
+ f"{AGENTS_END} line; found {begins} begin and {ends} end markers. "
1414
+ "Fix the markers by hand, then rerun."
1415
+ )
1416
+ return text[:begins[0]], text[ends[0].end():]
1417
+
1418
+
1419
+ def _read_agents(destination: Path) -> Optional[bytes]:
1420
+ if not _lexists(destination):
1421
+ return None
1422
+ try:
1423
+ return destination.read_bytes()
1424
+ except OSError as error:
1425
+ raise InstallerError(f"cannot read {destination}: {error}") from error
1426
+
1427
+
1428
+ def _merged_agents(
1429
+ source_root: Path, destination: Path, raw: Optional[bytes] = None
1430
+ ) -> bytes:
1431
+ """AGENTS.md with Path's block inserted or replaced; owner text kept as is.
1432
+ raw is the current file (None when absent); read from disk when omitted."""
1433
+ template = (source_root / "AGENTS.md").read_text(encoding="utf-8")
1434
+ block = _agents_block(template)
1435
+ if raw is None:
1436
+ raw = _read_agents(destination)
1437
+ if raw is not None:
1438
+ try:
1439
+ text = raw.decode("utf-8")
1440
+ except UnicodeDecodeError as error:
1441
+ raise InstallerError(f"cannot read {destination}: {error}") from error
1442
+ parts = _split_agents(text, destination)
1443
+ if parts is not None:
1444
+ merged = parts[0] + block + parts[1]
1445
+ elif hashlib.sha256(raw).hexdigest() in RELEASED_AGENTS:
1446
+ merged = block # One-time migration of an unedited whole-file install.
1447
+ elif LEGACY_AGENTS_TITLE in text or re.search(
1448
+ rf"(?m)^{re.escape(LEGACY_AGENTS_MARKER)}\r?$", text
1449
+ ):
1450
+ diff = "".join(
1451
+ difflib.unified_diff(
1452
+ template.splitlines(keepends=True),
1453
+ text.splitlines(keepends=True),
1454
+ "AGENTS.md (current GSD Path contract)",
1455
+ str(destination),
1456
+ )
1457
+ )
1458
+ raise InstallerError(
1459
+ f"{destination} is an edited whole-file GSD Path contract; Path cannot "
1460
+ "tell its text from your edits. Migrate by hand: 1) keep your own "
1461
+ f"rules from the diff below; 2) move the file aside with `mv "
1462
+ f"{shlex.quote(str(destination))} "
1463
+ f"{shlex.quote(str(destination.with_name('AGENTS.pre-path.md')))}`; "
1464
+ "3) rerun; 4) add your rules after the "
1465
+ f"{AGENTS_END} line.\n{diff}"
1466
+ )
1467
+ else:
1468
+ merged = block + "\n" + text # Block first: Codex cuts the tail.
1469
+ else:
1470
+ merged = block
1471
+ data = merged.encode("utf-8")
1472
+ if len(data) > CODEX_DOC_LIMIT:
1473
+ aside = destination.with_name("AGENTS.pre-path.md")
1474
+ raise InstallerError(
1475
+ f"{destination} would be {len(data)} bytes (existing file {len(raw or b'')} "
1476
+ f"bytes, GSD Path block {len(block.encode('utf-8'))} bytes). Codex reads "
1477
+ f"only the first {CODEX_DOC_LIMIT} bytes and silently cuts the rest. "
1478
+ f"Move it aside with `mv {shlex.quote(str(destination))} "
1479
+ f"{shlex.quote(str(aside))}`, rerun, then add back only the rules that fit."
1480
+ )
1481
+ return data
1482
+
1483
+
1484
+ def _apply_agents(
1485
+ source_root: Path, destination: Path, transaction: "ProjectTransaction"
1486
+ ) -> None:
1487
+ original = _read_agents(destination)
1488
+ merged = _merged_agents(source_root, destination, original)
1489
+ if original is None:
1490
+ try:
1491
+ with destination.open("xb") as output:
1492
+ transaction.copied.append(destination)
1493
+ output.write(merged)
1494
+ except FileExistsError as error:
1495
+ raise _existing_contract_error(destination) from error
1496
+ return
1497
+ if original != merged:
1498
+ if _read_agents(destination) != original:
1499
+ raise InstallerError(f"{destination} changed during install; rerun")
1500
+ mode = destination.stat().st_mode & 0o777
1501
+ _atomic_write(destination, merged, mode)
1502
+ transaction.replaced.append((destination, original, mode))
1503
+
1504
+
1165
1505
  def _validate_project_git_root(project: Path) -> None:
1166
1506
  probe = project
1167
1507
  while not _lexists(probe):
@@ -1171,7 +1511,7 @@ def _validate_project_git_root(project: Path) -> None:
1171
1511
  ["git", "rev-parse", "--show-toplevel"],
1172
1512
  cwd=probe,
1173
1513
  capture_output=True,
1174
- text=True,
1514
+ encoding="utf-8", errors="replace",
1175
1515
  check=False,
1176
1516
  env={**os.environ, "GIT_OPTIONAL_LOCKS": "0"},
1177
1517
  )
@@ -1186,6 +1526,90 @@ def _validate_project_git_root(project: Path) -> None:
1186
1526
  raise InstallerError(f"project path is not the Git worktree root: {project}")
1187
1527
 
1188
1528
 
1529
+ GIT_ATTRIBUTES_BEGIN = "# gsd-path:begin"
1530
+ GIT_ATTRIBUTES_END = "# gsd-path:end"
1531
+ # Pipeline state is compared and hashed byte for byte. Git for Windows installs
1532
+ # with core.autocrlf=true, which would check .project files out as CRLF in
1533
+ # every worktree; this local rule keeps them LF without touching product files.
1534
+ GIT_ATTRIBUTES_BLOCK = (
1535
+ f"{GIT_ATTRIBUTES_BEGIN}\n/.project/** text eol=lf\n{GIT_ATTRIBUTES_END}\n"
1536
+ )
1537
+
1538
+
1539
+ def _git_attributes_path(project: Path) -> Optional[Path]:
1540
+ """The repository's local attributes file, shared by every worktree.
1541
+
1542
+ `--git-path info/attributes` resolves to the common git directory, so
1543
+ managed worktrees see the same rule. None when git cannot resolve it.
1544
+ """
1545
+ try:
1546
+ result = subprocess.run(
1547
+ ["git", "rev-parse", "--show-toplevel", "--git-path", "info/attributes"],
1548
+ cwd=os.fspath(project), capture_output=True,
1549
+ encoding="utf-8", errors="replace", check=False,
1550
+ )
1551
+ except OSError:
1552
+ return None
1553
+ lines = [line for line in result.stdout.splitlines() if line]
1554
+ if result.returncode != 0 or len(lines) != 2 or not _same_path(Path(lines[0]), project):
1555
+ return None
1556
+ return Path(os.path.abspath(os.path.join(os.fspath(project), lines[1])))
1557
+
1558
+
1559
+ def _merged_git_attributes(raw: Optional[bytes], destination: Path) -> bytes:
1560
+ """The attributes file with Path's block inserted or replaced; other lines kept."""
1561
+ if raw is None:
1562
+ return GIT_ATTRIBUTES_BLOCK.encode("utf-8")
1563
+ text = raw.decode("utf-8", "surrogateescape")
1564
+ lines = text.splitlines(keepends=True)
1565
+ begins = [i for i, line in enumerate(lines) if line.rstrip("\r\n") == GIT_ATTRIBUTES_BEGIN]
1566
+ ends = [i for i, line in enumerate(lines) if line.rstrip("\r\n") == GIT_ATTRIBUTES_END]
1567
+ if not begins and not ends:
1568
+ separator = "" if not text or text.endswith("\n") else "\n"
1569
+ merged = text + separator + GIT_ATTRIBUTES_BLOCK
1570
+ elif len(begins) != 1 or len(ends) != 1 or ends[0] < begins[0]:
1571
+ raise InstallerError(
1572
+ f"{destination} needs exactly one {GIT_ATTRIBUTES_BEGIN} line followed by "
1573
+ f"one {GIT_ATTRIBUTES_END} line; fix the markers by hand, then rerun"
1574
+ )
1575
+ else:
1576
+ merged = "".join(lines[:begins[0]]) + GIT_ATTRIBUTES_BLOCK + "".join(lines[ends[0] + 1:])
1577
+ return merged.encode("utf-8", "surrogateescape")
1578
+
1579
+
1580
+ def _read_git_attributes(destination: Path) -> Optional[bytes]:
1581
+ if not _lexists(destination):
1582
+ return None
1583
+ if destination.is_symlink() or not destination.is_file():
1584
+ raise InstallerError(f"unsafe git attributes file: {destination}")
1585
+ try:
1586
+ return destination.read_bytes()
1587
+ except OSError as error:
1588
+ raise InstallerError(f"cannot read {destination}: {error}") from error
1589
+
1590
+
1591
+ def _apply_git_attributes(project: Path, transaction: "ProjectTransaction") -> None:
1592
+ destination = _git_attributes_path(project)
1593
+ if destination is None:
1594
+ return
1595
+ original = _read_git_attributes(destination)
1596
+ merged = _merged_git_attributes(original, destination)
1597
+ if original == merged:
1598
+ return
1599
+ _create_directory(destination.parent, transaction.created_directories)
1600
+ if original is None:
1601
+ try:
1602
+ with destination.open("xb") as output:
1603
+ transaction.copied.append(destination)
1604
+ output.write(merged)
1605
+ except FileExistsError as error:
1606
+ raise InstallerError(f"{destination} changed during install; rerun") from error
1607
+ return
1608
+ mode = destination.stat().st_mode & 0o777
1609
+ _atomic_write(destination, merged, mode)
1610
+ transaction.replaced.append((destination, original, mode))
1611
+
1612
+
1189
1613
  def _validate_project(
1190
1614
  source_root: Path,
1191
1615
  project: Path,
@@ -1232,11 +1656,18 @@ def _validate_project(
1232
1656
  runtime_store.prepare(source_root, project, dry_run=True)
1233
1657
  except (OSError, ValueError) as error:
1234
1658
  raise InstallerError(str(error)) from error
1659
+ attributes = _git_attributes_path(project)
1660
+ if attributes is not None:
1661
+ _merged_git_attributes(_read_git_attributes(attributes), attributes)
1235
1662
  mergers = _native_settings_mergers(project, selected, hooks)
1236
1663
  for destination, _, _, _ in _project_destinations(
1237
1664
  project, selected, hooks, interpreter, hooks_dir
1238
1665
  ):
1239
- if _lexists(destination):
1666
+ if destination == project / "AGENTS.md":
1667
+ if destination.is_symlink():
1668
+ raise _existing_contract_error(destination)
1669
+ _merged_agents(source_root, destination)
1670
+ elif _lexists(destination):
1240
1671
  merge = mergers.get(destination)
1241
1672
  managed = (
1242
1673
  _update_replacement(project, destination, hooks_dir) if update else None
@@ -1265,7 +1696,10 @@ def _apply_project(
1265
1696
  interpreter: str,
1266
1697
  hooks_dir: Optional[Path],
1267
1698
  update: bool = False,
1699
+ contracts: bool = True,
1268
1700
  ) -> None:
1701
+ """contracts=False (hook refresh) creates a missing AGENTS.md but never
1702
+ rewrites an existing one."""
1269
1703
  try:
1270
1704
  pin = runtime_store.prepare(source_root, project)
1271
1705
  except (OSError, ValueError) as error:
@@ -1278,6 +1712,11 @@ def _apply_project(
1278
1712
  if destination.name == "runtime.json" and destination.parent == project / HOOKS_DIRECTORY:
1279
1713
  content = runtime_store.pin_text(pin)
1280
1714
  _create_directory(destination.parent, transaction.created_directories)
1715
+ if destination == project / "AGENTS.md" and (
1716
+ contracts or not _lexists(destination)
1717
+ ):
1718
+ _apply_agents(source_root, destination, transaction)
1719
+ continue
1281
1720
  merge = mergers.get(destination)
1282
1721
  if _lexists(destination) and not destination.is_symlink():
1283
1722
  replaceable = (
@@ -1320,6 +1759,7 @@ def _apply_project(
1320
1759
  if created:
1321
1760
  _remove_path(destination)
1322
1761
  raise
1762
+ _apply_git_attributes(project, transaction)
1323
1763
 
1324
1764
 
1325
1765
  def _rollback_project(transaction: ProjectTransaction) -> None:
@@ -1411,7 +1851,7 @@ def _atomic_write(
1411
1851
  with os.fdopen(descriptor, "wb") as output:
1412
1852
  output.write(content)
1413
1853
  else:
1414
- with os.fdopen(descriptor, "w", encoding="utf-8") as output:
1854
+ with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as output:
1415
1855
  output.write(content)
1416
1856
  if mode is not None:
1417
1857
  temporary.chmod(mode)
@@ -1448,7 +1888,7 @@ def _is_managed_hook_entry(entry) -> bool:
1448
1888
  def _is_guard_command(command: str) -> bool:
1449
1889
  normalized = command.replace("\\", "/")
1450
1890
  match = re.fullmatch(
1451
- r"(?:python3|python)\s+(?:\"([^\"\r\n]+)\"|'([^'\r\n]+)'|(\S+))",
1891
+ r"(?:python3|python|py\s+-3)\s+(?:\"([^\"\r\n]+)\"|'([^'\r\n]+)'|(\S+))",
1452
1892
  normalized,
1453
1893
  )
1454
1894
  if match is None:
@@ -1761,7 +2201,7 @@ def refresh_hooks(
1761
2201
  _validate_hooks_refresh(source_root, project, full, hooks_dir, selected, True)
1762
2202
  transaction = ProjectTransaction()
1763
2203
  try:
1764
- _apply_project(source_root, project, selected, full, transaction, interpreter, hooks_dir, update=True)
2204
+ _apply_project(source_root, project, selected, full, transaction, interpreter, hooks_dir, update=True, contracts=False)
1765
2205
  except BaseException:
1766
2206
  _rollback_project(transaction)
1767
2207
  raise
@@ -1777,7 +2217,7 @@ def refresh_hooks(
1777
2217
  refresh_targets.append(target)
1778
2218
  transaction = ProjectTransaction()
1779
2219
  try:
1780
- _apply_project(source_root, project, refresh_targets, True, transaction, interpreter, hooks_dir, update=True)
2220
+ _apply_project(source_root, project, refresh_targets, True, transaction, interpreter, hooks_dir, update=True, contracts=False)
1781
2221
  except BaseException:
1782
2222
  _rollback_project(transaction)
1783
2223
  raise
@@ -1827,7 +2267,7 @@ def _deployment_plans(plans: Sequence[TargetPlan]) -> List[DeploymentPlan]:
1827
2267
  if not target_set <= SHARED_AGENT_TARGETS:
1828
2268
  labels = ", ".join(targets)
1829
2269
  raise InstallerError(
1830
- "only Codex, Antigravity, and Zed may share a skills root: "
2270
+ "only Codex, Antigravity, Zed, and Muse may share a skills root: "
1831
2271
  f"{labels}"
1832
2272
  )
1833
2273
  profile = SHARED_AGENT_PROFILE
@@ -1940,10 +2380,10 @@ def _validated_project_state(source_root: Path, project: Path) -> dict:
1940
2380
  interpreter = _required_python_runtime("--doctor")
1941
2381
  try:
1942
2382
  result = subprocess.run(
1943
- [interpreter, "-B", str(validator), "validate", "--repo", str(project)],
2383
+ [*interpreter.split(), "-B", str(validator), "validate", "--repo", str(project)],
1944
2384
  cwd=project,
1945
2385
  capture_output=True,
1946
- text=True,
2386
+ encoding="utf-8", errors="replace",
1947
2387
  check=False,
1948
2388
  )
1949
2389
  except OSError as error:
@@ -1981,10 +2421,10 @@ def _validate_project_runtime_status(source_root: Path, project: Path) -> None:
1981
2421
  environment["GIT_OPTIONAL_LOCKS"] = "0"
1982
2422
  try:
1983
2423
  result = subprocess.run(
1984
- [interpreter, "-B", str(runtime), "status", "--repo", str(project)],
2424
+ [*interpreter.split(), "-B", str(runtime), "status", "--repo", str(project)],
1985
2425
  cwd=project,
1986
2426
  capture_output=True,
1987
- text=True,
2427
+ encoding="utf-8", errors="replace",
1988
2428
  check=False,
1989
2429
  env=environment,
1990
2430
  )
@@ -2148,9 +2588,13 @@ def _valid_status_branch(value: object) -> Optional[re.Match[str]]:
2148
2588
 
2149
2589
  def _is_executable(path: Path) -> bool:
2150
2590
  try:
2151
- return bool(path.stat().st_mode & 0o111)
2591
+ status = path.stat()
2152
2592
  except OSError:
2153
2593
  return False
2594
+ if os.name == "nt":
2595
+ # Windows has no exec bit; Git for Windows runs any regular hook file.
2596
+ return path.is_file()
2597
+ return bool(status.st_mode & 0o111)
2154
2598
 
2155
2599
 
2156
2600
  def _native_guard_contract(
@@ -2165,6 +2609,32 @@ def _native_guard_contract(
2165
2609
  return None
2166
2610
 
2167
2611
 
2612
+ def _windows_path_warnings() -> List[str]:
2613
+ """Settings that deep managed-worktree paths need past 260 characters."""
2614
+ import winreg
2615
+
2616
+ warnings = []
2617
+ try:
2618
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
2619
+ r"SYSTEM\CurrentControlSet\Control\FileSystem") as key:
2620
+ enabled = winreg.QueryValueEx(key, "LongPathsEnabled")[0] == 1
2621
+ except OSError:
2622
+ enabled = False
2623
+ if not enabled:
2624
+ warnings.append("windows: long paths are disabled (LongPathsEnabled=0); files deep in "
2625
+ "managed worktrees may exceed 260 characters")
2626
+ try:
2627
+ result = subprocess.run(["git", "config", "--get", "core.longpaths"], capture_output=True,
2628
+ encoding="utf-8", errors="replace", check=False)
2629
+ except OSError:
2630
+ # Without git on PATH there is no git setting to advise on.
2631
+ return warnings
2632
+ if result.stdout.strip().lower() != "true":
2633
+ warnings.append("windows: git core.longpaths is not true; run "
2634
+ "`git config --global core.longpaths true`")
2635
+ return warnings
2636
+
2637
+
2168
2638
  def doctor(
2169
2639
  source_root: Path,
2170
2640
  targets: Sequence[str],
@@ -2186,6 +2656,9 @@ def doctor(
2186
2656
  version = _read_package_version(source_root / "package.json")
2187
2657
  if version is None:
2188
2658
  push("fail", "package: version cannot be read")
2659
+ if os.name == "nt":
2660
+ for text in _windows_path_warnings():
2661
+ push("warn", text)
2189
2662
  seen: List[Tuple[str, Path]] = []
2190
2663
  installed_targets = set()
2191
2664
  for target in targets:
@@ -2235,6 +2708,29 @@ def doctor(
2235
2708
  if project is None:
2236
2709
  return findings
2237
2710
 
2711
+ attributes = _git_attributes_path(project)
2712
+ if attributes is not None:
2713
+ try:
2714
+ current = _read_git_attributes(attributes)
2715
+ if current is None or _merged_git_attributes(current, attributes) != current:
2716
+ push("warn", f"git: {attributes} lacks the GSD Path LF rule for .project — "
2717
+ f'run --update --project "{project}"')
2718
+ else:
2719
+ push("ok", "git: .project is checked out LF in every worktree")
2720
+ except InstallerError as error:
2721
+ push("fail", f"git: {error}")
2722
+ crlf = sorted(
2723
+ path.relative_to(project).as_posix()
2724
+ for path in (project / ".project").rglob("*")
2725
+ if path.is_file() and not path.is_symlink() and b"\r\n" in path.read_bytes()
2726
+ ) if (project / ".project").is_dir() else []
2727
+ if crlf:
2728
+ # git checkout skips files it considers clean, so delete them first.
2729
+ push("warn", f"git: {len(crlf)} .project file(s) were checked out as CRLF "
2730
+ f"(first: {crlf[0]}); with the LF rule in place and changes "
2731
+ "committed, run `git ls-files -z .project | xargs -0 rm -f && "
2732
+ "git checkout -- .project` to restore LF")
2733
+
2238
2734
  for name, heading in PROJECT_CONTRACTS:
2239
2735
  contract = project / name
2240
2736
  if contract.is_symlink():
@@ -2249,6 +2745,26 @@ def doctor(
2249
2745
  canonical = read_project_file(source_root / name, f"package: {name}")
2250
2746
  if canonical is None:
2251
2747
  continue
2748
+ if name == "AGENTS.md":
2749
+ text = content.decode("utf-8", errors="replace")
2750
+ try:
2751
+ parts = _split_agents(text, contract)
2752
+ except InstallerError as error:
2753
+ push("fail", f"project: {error}")
2754
+ continue
2755
+ if parts is None:
2756
+ push("fail", f'project: AGENTS.md has no GSD Path block — run --update --project "{project}"')
2757
+ elif _agents_block(canonical.decode("utf-8", errors="replace")) not in text:
2758
+ push("fail", f'project: AGENTS.md block is stale — run --update --project "{project}"')
2759
+ elif len(content) > CODEX_DOC_LIMIT:
2760
+ push(
2761
+ "fail",
2762
+ f"project: AGENTS.md is {len(content)} bytes; Codex reads only "
2763
+ f"the first {CODEX_DOC_LIMIT} — reduce the file to keep all rules visible",
2764
+ )
2765
+ else:
2766
+ push("ok", "project: AGENTS.md block present")
2767
+ continue
2252
2768
  installed_section = _contract_section(
2253
2769
  content.decode("utf-8", errors="replace"), heading
2254
2770
  )
@@ -2442,7 +2958,7 @@ def doctor(
2442
2958
  try:
2443
2959
  parsed = json.loads(settings.read_text(encoding="utf-8"))
2444
2960
  entries = parsed.get("hooks", {}).get(event)
2445
- variants = [entry(candidate) for candidate in ("python3", "python")]
2961
+ variants = [entry(candidate) for candidate in INTERPRETER_CANDIDATES]
2446
2962
  current = isinstance(entries, list) and any(
2447
2963
  candidate in variants for candidate in entries
2448
2964
  )
@@ -2488,7 +3004,7 @@ def doctor(
2488
3004
  push("warn", f"hooks: {label} is not a managed GSD Path git hook")
2489
3005
  elif not any(
2490
3006
  hook_text == generator(candidate)
2491
- for candidate in ("python3", "python")
3007
+ for candidate in INTERPRETER_CANDIDATES
2492
3008
  ):
2493
3009
  push("warn", f"hooks: {label} is stale — run --hooks-refresh-full")
2494
3010
  elif not _is_executable(hook_path):
@@ -2715,7 +3231,7 @@ def install(
2715
3231
  results.append(
2716
3232
  "codex-legacy: backed up "
2717
3233
  f"{len(legacy_transaction.moved)} entries to "
2718
- f"{legacy_transaction.backup}"
3234
+ f"{legacy_transaction.backup}{LOCAL_EDITS_NOTE}"
2719
3235
  )
2720
3236
  for index, plan in enumerate(deployments):
2721
3237
  transaction = TargetTransaction(plan.root)
@@ -2726,7 +3242,7 @@ def install(
2726
3242
  results.append(
2727
3243
  f"{'+'.join(plan.targets)}: backed up "
2728
3244
  f"{len(transaction.moved)} entries "
2729
- f"to {transaction.backup}"
3245
+ f"to {transaction.backup}{LOCAL_EDITS_NOTE}"
2730
3246
  )
2731
3247
  if project is not None:
2732
3248
  result_line = _project_result(
@@ -2767,6 +3283,31 @@ def install(
2767
3283
  return results
2768
3284
 
2769
3285
 
3286
+ def _runtime_provenance(arguments, source_root: Path):
3287
+ """Owner-recorded provenance from --runtime-provenance-* upgrade flags.
3288
+
3289
+ When the supplied source root is a git checkout and no patch reference
3290
+ was given, its HEAD sha is recorded with one best-effort rev-parse.
3291
+ Provenance is display/audit metadata on the runtime declaration only; it
3292
+ never joins the pinned digest or runtime validation.
3293
+ """
3294
+ supplied = (arguments.runtime_provenance_source, arguments.runtime_provenance_patch,
3295
+ arguments.runtime_provenance_note)
3296
+ recorded = {key: value for key, value in zip(runtime_store.PROVENANCE_KEYS, supplied) if value}
3297
+ if not recorded:
3298
+ return None
3299
+ if "patch_ref" not in recorded:
3300
+ try:
3301
+ head = subprocess.run(["git", "rev-parse", "--show-toplevel", "HEAD"], cwd=source_root,
3302
+ capture_output=True, encoding="utf-8", check=True)
3303
+ toplevel, _, sha = head.stdout.strip().partition("\n")
3304
+ if sha and Path(toplevel).resolve() == Path(source_root).resolve():
3305
+ recorded["patch_ref"] = sha
3306
+ except (OSError, subprocess.CalledProcessError):
3307
+ pass
3308
+ return recorded
3309
+
3310
+
2770
3311
  def parser() -> argparse.ArgumentParser:
2771
3312
  argument_parser = argparse.ArgumentParser(description=__doc__)
2772
3313
  for target in TARGETS:
@@ -2778,10 +3319,14 @@ def parser() -> argparse.ArgumentParser:
2778
3319
  argument_parser.add_argument("--local", action="store_true")
2779
3320
  argument_parser.add_argument("--dry-run", action="store_true")
2780
3321
  argument_parser.add_argument("--project", type=Path)
3322
+ argument_parser.add_argument("--member-of", type=Path)
2781
3323
  argument_parser.add_argument("--doctor", action="store_true")
2782
3324
  runtime_actions = argument_parser.add_mutually_exclusive_group()
2783
3325
  for action in ("restore", "upgrade", "migrate"):
2784
3326
  runtime_actions.add_argument(f"--runtime-{action}", action="store_true")
3327
+ argument_parser.add_argument("--runtime-provenance-source")
3328
+ argument_parser.add_argument("--runtime-provenance-patch")
3329
+ argument_parser.add_argument("--runtime-provenance-note")
2785
3330
  argument_parser.add_argument("--hooks", action="store_true")
2786
3331
  argument_parser.add_argument("--hooks-init", action="store_true")
2787
3332
  argument_parser.add_argument("--hooks-refresh", action="store_true")
@@ -2827,6 +3372,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2827
3372
  )
2828
3373
  runtime_action = next((action for action in ("restore", "upgrade", "migrate")
2829
3374
  if getattr(arguments, "runtime_" + action)), None)
3375
+ provenance = _runtime_provenance(arguments, source_root)
3376
+ if provenance is not None and runtime_action != "upgrade":
3377
+ argument_parser.error("--runtime-provenance-* requires --runtime-upgrade")
2830
3378
  if runtime_action:
2831
3379
  if project is None:
2832
3380
  argument_parser.error(f"--runtime-{runtime_action} requires --project")
@@ -2834,7 +3382,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2834
3382
  _validate_project_git_root(project)
2835
3383
  locks, created = ([], []) if arguments.dry_run else _acquire_install_locks([project / HOOKS_DIRECTORY])
2836
3384
  try:
2837
- pin = runtime_store.operate(source_root, project, runtime_action, dry_run=arguments.dry_run)
3385
+ pin = runtime_store.operate(source_root, project, runtime_action,
3386
+ dry_run=arguments.dry_run, provenance=provenance)
2838
3387
  finally:
2839
3388
  _release_install_locks(locks, created)
2840
3389
  print(f"runtime: {runtime_action} {pin['version']} ({pin['digest']})"
@@ -2845,6 +3394,18 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2845
3394
  except (InstallerError, OSError, ValueError) as error:
2846
3395
  print(f"error: {error}", file=sys.stderr)
2847
3396
  return 1
3397
+ if arguments.member_of is not None:
3398
+ if project is None:
3399
+ argument_parser.error("--member-of requires --project")
3400
+ try:
3401
+ for line in install_member_hooks(
3402
+ absolute_path(arguments.member_of), project, arguments.dry_run
3403
+ ):
3404
+ print(line)
3405
+ return 0
3406
+ except (InstallerError, OSError, subprocess.CalledProcessError) as error:
3407
+ print(f"error: {error}", file=sys.stderr)
3408
+ return 1
2848
3409
  if arguments.doctor:
2849
3410
  named = [target for target in TARGETS if getattr(arguments, target)]
2850
3411
  selected = named if named and not arguments.all_targets else list(TARGETS)