@opengsd/gsd-path 1.1.0 → 1.2.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 (577) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +68 -0
  3. package/DOCS.md +69 -1
  4. package/FULL.md +3 -2
  5. package/HOOKS.md +18 -9
  6. package/QUICK.md +5 -3
  7. package/README.md +72 -5
  8. package/RUNTIME.md +9 -4
  9. package/UPDATE.md +30 -11
  10. package/WORKFLOW.md +5 -2
  11. package/package.json +9 -1
  12. package/platforms/claude/dispatch.md +4 -4
  13. package/platforms/copilot/dispatch.md +4 -4
  14. package/platforms/cursor/dispatch.md +5 -10
  15. package/platforms/grok/dispatch.md +4 -6
  16. package/platforms/kimi/dispatch.md +4 -4
  17. package/platforms/kiro/dispatch.md +4 -6
  18. package/platforms/opencode/dispatch.md +4 -4
  19. package/platforms/qwen/dispatch.md +4 -4
  20. package/platforms/shared-agents/dispatch.md +6 -19
  21. package/scripts/_common.py +5 -0
  22. package/scripts/archive_milestone.py +20 -6
  23. package/scripts/bootstrap_repository.py +8 -1
  24. package/scripts/build_recovery.py +5 -0
  25. package/scripts/build_state.py +2 -0
  26. package/scripts/check_docs_audit.py +2 -0
  27. package/scripts/check_handoffs.py +6 -0
  28. package/scripts/check_task_briefs.py +3 -0
  29. package/scripts/detect_project.py +11 -1
  30. package/scripts/discussion_records.py +3 -0
  31. package/scripts/discussion_validate.py +8 -3
  32. package/scripts/dispatch_driver.py +230 -21
  33. package/scripts/git_guard.py +12 -4
  34. package/scripts/guard_hook.py +11 -1
  35. package/scripts/install.mjs +52 -1448
  36. package/scripts/install.py +155 -190
  37. package/scripts/integration.py +28 -8
  38. package/scripts/isolation.py +15 -5
  39. package/scripts/jev_review.py +138 -0
  40. package/scripts/lean_verification.py +2 -0
  41. package/scripts/loop_run.py +2 -0
  42. package/scripts/model_policy.py +279 -0
  43. package/scripts/path_config.py +248 -0
  44. package/scripts/pipeline_diagnose.py +3 -0
  45. package/scripts/pipeline_git.py +3 -0
  46. package/scripts/pipeline_state.py +4 -1
  47. package/scripts/pipeline_undo.py +3 -0
  48. package/scripts/promote_lookahead.py +3 -0
  49. package/scripts/review_panel.py +11 -0
  50. package/scripts/roadmap.py +5 -0
  51. package/scripts/runtime_store.py +278 -0
  52. package/scripts/skill-resources.json +104 -4
  53. package/scripts/state_checkpoint.py +5 -0
  54. package/scripts/state_promote.py +5 -0
  55. package/scripts/status_runtime.py +92 -6
  56. package/scripts/workflow_run.py +12 -1
  57. package/scripts/worktree_paths.py +94 -0
  58. package/skills/gsd-path/BUILD.md +7 -0
  59. package/skills/gsd-path/DECIDE.md +2 -0
  60. package/skills/gsd-path/DEFINE.md +13 -7
  61. package/skills/gsd-path/DOCS-AUDIT.md +2 -0
  62. package/skills/gsd-path/INSPECT.md +2 -0
  63. package/skills/gsd-path/PLAN.md +4 -2
  64. package/skills/gsd-path/RESEARCH.md +2 -0
  65. package/skills/gsd-path/ROADMAP.md +2 -0
  66. package/skills/gsd-path/SHIP.md +24 -13
  67. package/skills/gsd-path/SKILL.md +23 -9
  68. package/skills/gsd-path/references/build-native.md +15 -7
  69. package/skills/gsd-path/references/config.md +57 -0
  70. package/skills/gsd-path/references/dispatch.md +6 -19
  71. package/skills/gsd-path/references/jev-review.md +82 -0
  72. package/skills/gsd-path/references/model-policy.md +139 -0
  73. package/skills/gsd-path/references/reviewer-panel.md +1 -1
  74. package/skills/gsd-path/references/reviewer.md +2 -0
  75. package/skills/gsd-path/references/runtime-selection.md +25 -0
  76. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  77. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  78. package/skills/gsd-path/scripts/_common.py +5 -0
  79. package/skills/gsd-path/scripts/archive_milestone.py +20 -6
  80. package/skills/gsd-path/scripts/bootstrap_repository.py +8 -1
  81. package/skills/gsd-path/scripts/build_recovery.py +5 -0
  82. package/skills/gsd-path/scripts/build_state.py +2 -0
  83. package/skills/gsd-path/scripts/check_docs_audit.py +2 -0
  84. package/skills/gsd-path/scripts/check_handoffs.py +6 -0
  85. package/skills/gsd-path/scripts/check_task_briefs.py +3 -0
  86. package/skills/gsd-path/scripts/detect_project.py +11 -1
  87. package/skills/gsd-path/scripts/discussion_records.py +3 -0
  88. package/skills/gsd-path/scripts/discussion_validate.py +8 -3
  89. package/skills/gsd-path/scripts/dispatch_driver.py +230 -21
  90. package/skills/gsd-path/scripts/git_guard.py +768 -0
  91. package/skills/gsd-path/scripts/integration.py +28 -8
  92. package/skills/gsd-path/scripts/isolation.py +15 -5
  93. package/skills/gsd-path/scripts/jev_review.py +138 -0
  94. package/skills/gsd-path/scripts/lean_verification.py +2 -0
  95. package/skills/gsd-path/scripts/loop_run.py +2 -0
  96. package/skills/gsd-path/scripts/model_policy.py +279 -0
  97. package/skills/gsd-path/scripts/path_config.py +248 -0
  98. package/skills/gsd-path/scripts/pipeline_git.py +3 -0
  99. package/skills/gsd-path/scripts/pipeline_state.py +4 -1
  100. package/skills/gsd-path/scripts/promote_lookahead.py +3 -0
  101. package/skills/gsd-path/scripts/review_panel.py +11 -0
  102. package/skills/gsd-path/scripts/roadmap.py +5 -0
  103. package/skills/gsd-path/scripts/state_checkpoint.py +5 -0
  104. package/skills/gsd-path/scripts/state_promote.py +5 -0
  105. package/skills/gsd-path/scripts/status_runtime.py +208 -0
  106. package/skills/gsd-path/scripts/workflow_run.py +12 -1
  107. package/skills/gsd-path/scripts/worktree_paths.py +94 -0
  108. package/skills/gsd-path/templates/skeptic.md +1 -1
  109. package/skills/gsd-path/templates/task.md +2 -0
  110. package/skills/gsd-path/templates/wave-panel.md +1 -1
  111. package/skills/gsd-path-build/SKILL.md +7 -0
  112. package/skills/gsd-path-build/references/build-native.md +15 -7
  113. package/skills/gsd-path-build/references/dispatch.md +6 -19
  114. package/skills/gsd-path-build/references/jev-review.md +82 -0
  115. package/skills/gsd-path-build/references/model-policy.md +139 -0
  116. package/skills/gsd-path-build/references/reviewer-panel.md +1 -1
  117. package/skills/gsd-path-build/references/reviewer.md +2 -0
  118. package/skills/gsd-path-build/references/runtime-selection.md +25 -0
  119. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  120. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  121. package/skills/gsd-path-build/scripts/_common.py +5 -0
  122. package/skills/gsd-path-build/scripts/archive_milestone.py +20 -6
  123. package/skills/gsd-path-build/scripts/build_recovery.py +5 -0
  124. package/skills/gsd-path-build/scripts/build_state.py +2 -0
  125. package/skills/gsd-path-build/scripts/check_handoffs.py +6 -0
  126. package/skills/gsd-path-build/scripts/check_task_briefs.py +3 -0
  127. package/skills/gsd-path-build/scripts/detect_project.py +11 -1
  128. package/skills/gsd-path-build/scripts/discussion_records.py +3 -0
  129. package/skills/gsd-path-build/scripts/discussion_validate.py +8 -3
  130. package/skills/gsd-path-build/scripts/dispatch_driver.py +230 -21
  131. package/skills/gsd-path-build/scripts/integration.py +28 -8
  132. package/skills/gsd-path-build/scripts/isolation.py +15 -5
  133. package/skills/gsd-path-build/scripts/jev_review.py +138 -0
  134. package/skills/gsd-path-build/scripts/loop_run.py +2 -0
  135. package/skills/gsd-path-build/scripts/model_policy.py +279 -0
  136. package/skills/gsd-path-build/scripts/path_config.py +248 -0
  137. package/skills/gsd-path-build/scripts/pipeline_git.py +3 -0
  138. package/skills/gsd-path-build/scripts/pipeline_state.py +4 -1
  139. package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -0
  140. package/skills/gsd-path-build/scripts/review_panel.py +11 -0
  141. package/skills/gsd-path-build/scripts/roadmap.py +5 -0
  142. package/skills/gsd-path-build/scripts/state_checkpoint.py +5 -0
  143. package/skills/gsd-path-build/scripts/state_promote.py +5 -0
  144. package/skills/gsd-path-build/scripts/workflow_run.py +12 -1
  145. package/skills/gsd-path-build/scripts/worktree_paths.py +94 -0
  146. package/skills/gsd-path-build/templates/skeptic.md +1 -1
  147. package/skills/gsd-path-build/templates/task.md +2 -0
  148. package/skills/gsd-path-build/templates/wave-panel.md +1 -1
  149. package/skills/gsd-path-decide/SKILL.md +2 -0
  150. package/skills/gsd-path-decide/references/dispatch.md +6 -19
  151. package/skills/gsd-path-decide/references/model-policy.md +139 -0
  152. package/skills/gsd-path-decide/references/runtime-selection.md +25 -0
  153. package/skills/gsd-path-decide/scripts/_common.py +5 -0
  154. package/skills/gsd-path-decide/scripts/archive_milestone.py +20 -6
  155. package/skills/gsd-path-decide/scripts/build_recovery.py +5 -0
  156. package/skills/gsd-path-decide/scripts/check_handoffs.py +6 -0
  157. package/skills/gsd-path-decide/scripts/check_task_briefs.py +3 -0
  158. package/skills/gsd-path-decide/scripts/detect_project.py +11 -1
  159. package/skills/gsd-path-decide/scripts/discussion_records.py +3 -0
  160. package/skills/gsd-path-decide/scripts/discussion_validate.py +8 -3
  161. package/skills/gsd-path-decide/scripts/integration.py +28 -8
  162. package/skills/gsd-path-decide/scripts/isolation.py +15 -5
  163. package/skills/gsd-path-decide/scripts/loop_run.py +839 -0
  164. package/skills/gsd-path-decide/scripts/model_policy.py +279 -0
  165. package/skills/gsd-path-decide/scripts/path_config.py +248 -0
  166. package/skills/gsd-path-decide/scripts/pipeline_git.py +3 -0
  167. package/skills/gsd-path-decide/scripts/pipeline_state.py +4 -1
  168. package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -0
  169. package/skills/gsd-path-decide/scripts/review_panel.py +11 -0
  170. package/skills/gsd-path-decide/scripts/roadmap.py +5 -0
  171. package/skills/gsd-path-decide/scripts/state_checkpoint.py +5 -0
  172. package/skills/gsd-path-decide/scripts/state_promote.py +5 -0
  173. package/skills/gsd-path-decide/scripts/worktree_paths.py +94 -0
  174. package/skills/gsd-path-define/SKILL.md +13 -7
  175. package/skills/gsd-path-define/references/runtime-selection.md +25 -0
  176. package/skills/gsd-path-define/scripts/_common.py +5 -0
  177. package/skills/gsd-path-define/scripts/archive_milestone.py +20 -6
  178. package/skills/gsd-path-define/scripts/build_recovery.py +5 -0
  179. package/skills/gsd-path-define/scripts/check_handoffs.py +6 -0
  180. package/skills/gsd-path-define/scripts/check_task_briefs.py +3 -0
  181. package/skills/gsd-path-define/scripts/detect_project.py +11 -1
  182. package/skills/gsd-path-define/scripts/discussion_records.py +3 -0
  183. package/skills/gsd-path-define/scripts/discussion_validate.py +8 -3
  184. package/skills/gsd-path-define/scripts/integration.py +28 -8
  185. package/skills/gsd-path-define/scripts/isolation.py +15 -5
  186. package/skills/gsd-path-define/scripts/loop_run.py +839 -0
  187. package/skills/gsd-path-define/scripts/model_policy.py +279 -0
  188. package/skills/gsd-path-define/scripts/path_config.py +248 -0
  189. package/skills/gsd-path-define/scripts/pipeline_git.py +3 -0
  190. package/skills/gsd-path-define/scripts/pipeline_state.py +4 -1
  191. package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -0
  192. package/skills/gsd-path-define/scripts/review_panel.py +11 -0
  193. package/skills/gsd-path-define/scripts/roadmap.py +5 -0
  194. package/skills/gsd-path-define/scripts/state_checkpoint.py +5 -0
  195. package/skills/gsd-path-define/scripts/state_promote.py +5 -0
  196. package/skills/gsd-path-define/scripts/worktree_paths.py +94 -0
  197. package/skills/gsd-path-discuss/SKILL.md +2 -0
  198. package/skills/gsd-path-discuss/references/runtime-selection.md +25 -0
  199. package/skills/gsd-path-discuss/scripts/_common.py +5 -0
  200. package/skills/gsd-path-discuss/scripts/discussion_records.py +3 -0
  201. package/skills/gsd-path-discuss/scripts/loop_run.py +839 -0
  202. package/skills/gsd-path-discuss/scripts/model_policy.py +279 -0
  203. package/skills/gsd-path-discuss/scripts/path_config.py +248 -0
  204. package/skills/gsd-path-discuss/scripts/review_panel.py +564 -0
  205. package/skills/gsd-path-discuss/scripts/worktree_paths.py +94 -0
  206. package/skills/gsd-path-docs-audit/SKILL.md +2 -0
  207. package/skills/gsd-path-docs-audit/references/dispatch.md +6 -19
  208. package/skills/gsd-path-docs-audit/references/model-policy.md +139 -0
  209. package/skills/gsd-path-docs-audit/references/runtime-selection.md +25 -0
  210. package/skills/gsd-path-docs-audit/scripts/_common.py +5 -0
  211. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +20 -6
  212. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +5 -0
  213. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +2 -0
  214. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +6 -0
  215. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +3 -0
  216. package/skills/gsd-path-docs-audit/scripts/detect_project.py +11 -1
  217. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +3 -0
  218. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +8 -3
  219. package/skills/gsd-path-docs-audit/scripts/integration.py +28 -8
  220. package/skills/gsd-path-docs-audit/scripts/isolation.py +15 -5
  221. package/skills/gsd-path-docs-audit/scripts/loop_run.py +839 -0
  222. package/skills/gsd-path-docs-audit/scripts/model_policy.py +279 -0
  223. package/skills/gsd-path-docs-audit/scripts/path_config.py +248 -0
  224. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +3 -0
  225. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +4 -1
  226. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -0
  227. package/skills/gsd-path-docs-audit/scripts/review_panel.py +11 -0
  228. package/skills/gsd-path-docs-audit/scripts/roadmap.py +5 -0
  229. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +5 -0
  230. package/skills/gsd-path-docs-audit/scripts/state_promote.py +5 -0
  231. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +94 -0
  232. package/skills/gsd-path-forensics/SKILL.md +9 -5
  233. package/skills/gsd-path-forensics/references/runtime-selection.md +25 -0
  234. package/skills/gsd-path-forensics/scripts/_common.py +5 -0
  235. package/skills/gsd-path-forensics/scripts/discussion_records.py +3 -0
  236. package/skills/gsd-path-forensics/scripts/loop_run.py +839 -0
  237. package/skills/gsd-path-forensics/scripts/model_policy.py +279 -0
  238. package/skills/gsd-path-forensics/scripts/path_config.py +248 -0
  239. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +3 -0
  240. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +3 -0
  241. package/skills/gsd-path-forensics/scripts/review_panel.py +564 -0
  242. package/skills/gsd-path-forensics/scripts/worktree_paths.py +94 -0
  243. package/skills/gsd-path-inspect/SKILL.md +2 -0
  244. package/skills/gsd-path-inspect/references/dispatch.md +6 -19
  245. package/skills/gsd-path-inspect/references/model-policy.md +139 -0
  246. package/skills/gsd-path-inspect/references/runtime-selection.md +25 -0
  247. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  248. package/skills/gsd-path-inspect/scripts/_common.py +5 -0
  249. package/skills/gsd-path-inspect/scripts/archive_milestone.py +20 -6
  250. package/skills/gsd-path-inspect/scripts/build_recovery.py +5 -0
  251. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +2 -0
  252. package/skills/gsd-path-inspect/scripts/check_handoffs.py +6 -0
  253. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +3 -0
  254. package/skills/gsd-path-inspect/scripts/detect_project.py +11 -1
  255. package/skills/gsd-path-inspect/scripts/discussion_records.py +3 -0
  256. package/skills/gsd-path-inspect/scripts/discussion_validate.py +8 -3
  257. package/skills/gsd-path-inspect/scripts/integration.py +28 -8
  258. package/skills/gsd-path-inspect/scripts/isolation.py +15 -5
  259. package/skills/gsd-path-inspect/scripts/loop_run.py +839 -0
  260. package/skills/gsd-path-inspect/scripts/model_policy.py +279 -0
  261. package/skills/gsd-path-inspect/scripts/path_config.py +248 -0
  262. package/skills/gsd-path-inspect/scripts/pipeline_git.py +3 -0
  263. package/skills/gsd-path-inspect/scripts/pipeline_state.py +4 -1
  264. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -0
  265. package/skills/gsd-path-inspect/scripts/review_panel.py +11 -0
  266. package/skills/gsd-path-inspect/scripts/roadmap.py +5 -0
  267. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +5 -0
  268. package/skills/gsd-path-inspect/scripts/state_promote.py +5 -0
  269. package/skills/gsd-path-inspect/scripts/workflow_run.py +12 -1
  270. package/skills/gsd-path-inspect/scripts/worktree_paths.py +94 -0
  271. package/skills/gsd-path-loop/SKILL.md +2 -0
  272. package/skills/gsd-path-loop/references/runtime-selection.md +25 -0
  273. package/skills/gsd-path-loop/scripts/_common.py +5 -0
  274. package/skills/gsd-path-loop/scripts/discussion_records.py +3 -0
  275. package/skills/gsd-path-loop/scripts/loop_run.py +2 -0
  276. package/skills/gsd-path-loop/scripts/model_policy.py +279 -0
  277. package/skills/gsd-path-loop/scripts/path_config.py +248 -0
  278. package/skills/gsd-path-loop/scripts/review_panel.py +564 -0
  279. package/skills/gsd-path-loop/scripts/worktree_paths.py +94 -0
  280. package/skills/gsd-path-migrate/SKILL.md +2 -0
  281. package/skills/gsd-path-migrate/references/runtime-selection.md +25 -0
  282. package/skills/gsd-path-migrate/scripts/_common.py +185 -0
  283. package/skills/gsd-path-migrate/scripts/discussion_records.py +3 -0
  284. package/skills/gsd-path-migrate/scripts/loop_run.py +839 -0
  285. package/skills/gsd-path-migrate/scripts/model_policy.py +279 -0
  286. package/skills/gsd-path-migrate/scripts/path_config.py +248 -0
  287. package/skills/gsd-path-migrate/scripts/review_panel.py +564 -0
  288. package/skills/gsd-path-plan/SKILL.md +4 -2
  289. package/skills/gsd-path-plan/references/dispatch.md +6 -19
  290. package/skills/gsd-path-plan/references/model-policy.md +139 -0
  291. package/skills/gsd-path-plan/references/runtime-selection.md +25 -0
  292. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  293. package/skills/gsd-path-plan/scripts/_common.py +5 -0
  294. package/skills/gsd-path-plan/scripts/archive_milestone.py +20 -6
  295. package/skills/gsd-path-plan/scripts/build_recovery.py +5 -0
  296. package/skills/gsd-path-plan/scripts/check_handoffs.py +6 -0
  297. package/skills/gsd-path-plan/scripts/check_task_briefs.py +3 -0
  298. package/skills/gsd-path-plan/scripts/detect_project.py +11 -1
  299. package/skills/gsd-path-plan/scripts/discussion_records.py +3 -0
  300. package/skills/gsd-path-plan/scripts/discussion_validate.py +8 -3
  301. package/skills/gsd-path-plan/scripts/integration.py +28 -8
  302. package/skills/gsd-path-plan/scripts/isolation.py +15 -5
  303. package/skills/gsd-path-plan/scripts/loop_run.py +839 -0
  304. package/skills/gsd-path-plan/scripts/model_policy.py +279 -0
  305. package/skills/gsd-path-plan/scripts/path_config.py +248 -0
  306. package/skills/gsd-path-plan/scripts/pipeline_git.py +3 -0
  307. package/skills/gsd-path-plan/scripts/pipeline_state.py +4 -1
  308. package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -0
  309. package/skills/gsd-path-plan/scripts/review_panel.py +11 -0
  310. package/skills/gsd-path-plan/scripts/roadmap.py +5 -0
  311. package/skills/gsd-path-plan/scripts/state_checkpoint.py +5 -0
  312. package/skills/gsd-path-plan/scripts/state_promote.py +5 -0
  313. package/skills/gsd-path-plan/scripts/workflow_run.py +12 -1
  314. package/skills/gsd-path-plan/scripts/worktree_paths.py +94 -0
  315. package/skills/gsd-path-plan/templates/task.md +2 -0
  316. package/skills/gsd-path-research/SKILL.md +2 -0
  317. package/skills/gsd-path-research/references/dispatch.md +6 -19
  318. package/skills/gsd-path-research/references/model-policy.md +139 -0
  319. package/skills/gsd-path-research/references/runtime-selection.md +25 -0
  320. package/skills/gsd-path-research/scripts/_common.py +5 -0
  321. package/skills/gsd-path-research/scripts/archive_milestone.py +20 -6
  322. package/skills/gsd-path-research/scripts/build_recovery.py +5 -0
  323. package/skills/gsd-path-research/scripts/check_handoffs.py +6 -0
  324. package/skills/gsd-path-research/scripts/check_task_briefs.py +3 -0
  325. package/skills/gsd-path-research/scripts/detect_project.py +11 -1
  326. package/skills/gsd-path-research/scripts/discussion_records.py +3 -0
  327. package/skills/gsd-path-research/scripts/discussion_validate.py +8 -3
  328. package/skills/gsd-path-research/scripts/integration.py +28 -8
  329. package/skills/gsd-path-research/scripts/isolation.py +15 -5
  330. package/skills/gsd-path-research/scripts/loop_run.py +839 -0
  331. package/skills/gsd-path-research/scripts/model_policy.py +279 -0
  332. package/skills/gsd-path-research/scripts/path_config.py +248 -0
  333. package/skills/gsd-path-research/scripts/pipeline_git.py +3 -0
  334. package/skills/gsd-path-research/scripts/pipeline_state.py +4 -1
  335. package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -0
  336. package/skills/gsd-path-research/scripts/review_panel.py +11 -0
  337. package/skills/gsd-path-research/scripts/roadmap.py +5 -0
  338. package/skills/gsd-path-research/scripts/state_checkpoint.py +5 -0
  339. package/skills/gsd-path-research/scripts/state_promote.py +5 -0
  340. package/skills/gsd-path-research/scripts/worktree_paths.py +94 -0
  341. package/skills/gsd-path-roadmap/SKILL.md +2 -0
  342. package/skills/gsd-path-roadmap/references/dispatch.md +6 -19
  343. package/skills/gsd-path-roadmap/references/model-policy.md +139 -0
  344. package/skills/gsd-path-roadmap/references/runtime-selection.md +25 -0
  345. package/skills/gsd-path-roadmap/scripts/_common.py +5 -0
  346. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +20 -6
  347. package/skills/gsd-path-roadmap/scripts/build_recovery.py +5 -0
  348. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +6 -0
  349. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +3 -0
  350. package/skills/gsd-path-roadmap/scripts/detect_project.py +11 -1
  351. package/skills/gsd-path-roadmap/scripts/discussion_records.py +3 -0
  352. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +8 -3
  353. package/skills/gsd-path-roadmap/scripts/integration.py +28 -8
  354. package/skills/gsd-path-roadmap/scripts/isolation.py +15 -5
  355. package/skills/gsd-path-roadmap/scripts/loop_run.py +839 -0
  356. package/skills/gsd-path-roadmap/scripts/model_policy.py +279 -0
  357. package/skills/gsd-path-roadmap/scripts/path_config.py +248 -0
  358. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +3 -0
  359. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +4 -1
  360. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -0
  361. package/skills/gsd-path-roadmap/scripts/review_panel.py +11 -0
  362. package/skills/gsd-path-roadmap/scripts/roadmap.py +5 -0
  363. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +5 -0
  364. package/skills/gsd-path-roadmap/scripts/state_promote.py +5 -0
  365. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +94 -0
  366. package/skills/gsd-path-ship/SKILL.md +24 -13
  367. package/skills/gsd-path-ship/references/dispatch.md +6 -19
  368. package/skills/gsd-path-ship/references/jev-review.md +82 -0
  369. package/skills/gsd-path-ship/references/model-policy.md +139 -0
  370. package/skills/gsd-path-ship/references/reviewer-panel.md +1 -1
  371. package/skills/gsd-path-ship/references/reviewer.md +2 -0
  372. package/skills/gsd-path-ship/references/runtime-selection.md +25 -0
  373. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  374. package/skills/gsd-path-ship/scripts/_common.py +5 -0
  375. package/skills/gsd-path-ship/scripts/archive_milestone.py +20 -6
  376. package/skills/gsd-path-ship/scripts/build_recovery.py +5 -0
  377. package/skills/gsd-path-ship/scripts/build_state.py +2 -0
  378. package/skills/gsd-path-ship/scripts/check_handoffs.py +6 -0
  379. package/skills/gsd-path-ship/scripts/check_task_briefs.py +3 -0
  380. package/skills/gsd-path-ship/scripts/detect_project.py +11 -1
  381. package/skills/gsd-path-ship/scripts/discussion_records.py +3 -0
  382. package/skills/gsd-path-ship/scripts/discussion_validate.py +8 -3
  383. package/skills/gsd-path-ship/scripts/integration.py +28 -8
  384. package/skills/gsd-path-ship/scripts/isolation.py +15 -5
  385. package/skills/gsd-path-ship/scripts/jev_review.py +138 -0
  386. package/skills/gsd-path-ship/scripts/lean_verification.py +2 -0
  387. package/skills/gsd-path-ship/scripts/loop_run.py +839 -0
  388. package/skills/gsd-path-ship/scripts/model_policy.py +279 -0
  389. package/skills/gsd-path-ship/scripts/path_config.py +248 -0
  390. package/skills/gsd-path-ship/scripts/pipeline_git.py +3 -0
  391. package/skills/gsd-path-ship/scripts/pipeline_state.py +4 -1
  392. package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -0
  393. package/skills/gsd-path-ship/scripts/review_panel.py +11 -0
  394. package/skills/gsd-path-ship/scripts/roadmap.py +5 -0
  395. package/skills/gsd-path-ship/scripts/state_checkpoint.py +5 -0
  396. package/skills/gsd-path-ship/scripts/state_promote.py +5 -0
  397. package/skills/gsd-path-ship/scripts/workflow_run.py +12 -1
  398. package/skills/gsd-path-ship/scripts/worktree_paths.py +94 -0
  399. package/skills/gsd-path-undo/SKILL.md +2 -0
  400. package/skills/gsd-path-undo/references/runtime-selection.md +25 -0
  401. package/skills/gsd-path-undo/scripts/_common.py +5 -0
  402. package/skills/gsd-path-undo/scripts/discussion_records.py +3 -0
  403. package/skills/gsd-path-undo/scripts/loop_run.py +839 -0
  404. package/skills/gsd-path-undo/scripts/model_policy.py +279 -0
  405. package/skills/gsd-path-undo/scripts/path_config.py +248 -0
  406. package/skills/gsd-path-undo/scripts/pipeline_undo.py +3 -0
  407. package/skills/gsd-path-undo/scripts/review_panel.py +564 -0
  408. package/skills/gsd-path-undo/scripts/worktree_paths.py +94 -0
  409. package/skills/path/BUILD.md +7 -0
  410. package/skills/path/DECIDE.md +2 -0
  411. package/skills/path/DEFINE.md +13 -7
  412. package/skills/path/DOCS-AUDIT.md +2 -0
  413. package/skills/path/INSPECT.md +2 -0
  414. package/skills/path/PLAN.md +4 -2
  415. package/skills/path/RESEARCH.md +2 -0
  416. package/skills/path/ROADMAP.md +2 -0
  417. package/skills/path/SHIP.md +24 -13
  418. package/skills/path/SKILL.md +23 -9
  419. package/skills/path/references/build-native.md +15 -7
  420. package/skills/path/references/config.md +57 -0
  421. package/skills/path/references/dispatch.md +6 -19
  422. package/skills/path/references/jev-review.md +82 -0
  423. package/skills/path/references/model-policy.md +139 -0
  424. package/skills/path/references/reviewer-panel.md +1 -1
  425. package/skills/path/references/reviewer.md +2 -0
  426. package/skills/path/references/runtime-selection.md +25 -0
  427. package/skills/path/scripts/_common.py +5 -0
  428. package/skills/path/scripts/archive_milestone.py +20 -6
  429. package/skills/path/scripts/bootstrap_repository.py +8 -1
  430. package/skills/path/scripts/build_recovery.py +5 -0
  431. package/skills/path/scripts/build_state.py +2 -0
  432. package/skills/path/scripts/check_docs_audit.py +2 -0
  433. package/skills/path/scripts/check_handoffs.py +6 -0
  434. package/skills/path/scripts/check_task_briefs.py +3 -0
  435. package/skills/path/scripts/detect_project.py +11 -1
  436. package/skills/path/scripts/discussion_records.py +3 -0
  437. package/skills/path/scripts/discussion_validate.py +8 -3
  438. package/skills/path/scripts/dispatch_driver.py +230 -21
  439. package/skills/path/scripts/git_guard.py +768 -0
  440. package/skills/path/scripts/integration.py +28 -8
  441. package/skills/path/scripts/isolation.py +15 -5
  442. package/skills/path/scripts/jev_review.py +138 -0
  443. package/skills/path/scripts/lean_verification.py +2 -0
  444. package/skills/path/scripts/loop_run.py +2 -0
  445. package/skills/path/scripts/model_policy.py +279 -0
  446. package/skills/path/scripts/path_config.py +248 -0
  447. package/skills/path/scripts/pipeline_git.py +3 -0
  448. package/skills/path/scripts/pipeline_state.py +4 -1
  449. package/skills/path/scripts/promote_lookahead.py +3 -0
  450. package/skills/path/scripts/review_panel.py +11 -0
  451. package/skills/path/scripts/roadmap.py +5 -0
  452. package/skills/path/scripts/state_checkpoint.py +5 -0
  453. package/skills/path/scripts/state_promote.py +5 -0
  454. package/skills/path/scripts/status_runtime.py +208 -0
  455. package/skills/path/scripts/workflow_run.py +12 -1
  456. package/skills/path/scripts/worktree_paths.py +94 -0
  457. package/skills/path/templates/skeptic.md +1 -1
  458. package/skills/path/templates/task.md +2 -0
  459. package/skills/path/templates/wave-panel.md +1 -1
  460. package/skills/gsd-path/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  461. package/skills/gsd-path/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  462. package/skills/gsd-path/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
  463. package/skills/gsd-path/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
  464. package/skills/gsd-path/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  465. package/skills/gsd-path/scripts/__pycache__/discussion_records.cpython-312.pyc +0 -0
  466. package/skills/gsd-path/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  467. package/skills/gsd-path/scripts/__pycache__/integration.cpython-312.pyc +0 -0
  468. package/skills/gsd-path/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  469. package/skills/gsd-path/scripts/__pycache__/loop_run.cpython-312.pyc +0 -0
  470. package/skills/gsd-path/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  471. package/skills/gsd-path/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  472. package/skills/gsd-path/scripts/__pycache__/review_findings.cpython-312.pyc +0 -0
  473. package/skills/gsd-path/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  474. package/skills/gsd-path/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  475. package/skills/gsd-path/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  476. package/skills/gsd-path/scripts/__pycache__/task_context.cpython-312.pyc +0 -0
  477. package/skills/gsd-path/scripts/__pycache__/workflow_run.cpython-312.pyc +0 -0
  478. package/skills/gsd-path-build/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  479. package/skills/gsd-path-build/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  480. package/skills/gsd-path-build/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
  481. package/skills/gsd-path-build/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
  482. package/skills/gsd-path-build/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  483. package/skills/gsd-path-build/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  484. package/skills/gsd-path-build/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  485. package/skills/gsd-path-build/scripts/__pycache__/loop_run.cpython-312.pyc +0 -0
  486. package/skills/gsd-path-build/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  487. package/skills/gsd-path-build/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  488. package/skills/gsd-path-build/scripts/__pycache__/review_findings.cpython-312.pyc +0 -0
  489. package/skills/gsd-path-build/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  490. package/skills/gsd-path-build/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  491. package/skills/gsd-path-build/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  492. package/skills/gsd-path-build/scripts/__pycache__/task_context.cpython-312.pyc +0 -0
  493. package/skills/gsd-path-build/scripts/__pycache__/workflow_run.cpython-312.pyc +0 -0
  494. package/skills/gsd-path-decide/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  495. package/skills/gsd-path-decide/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  496. package/skills/gsd-path-decide/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  497. package/skills/gsd-path-decide/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  498. package/skills/gsd-path-decide/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  499. package/skills/gsd-path-decide/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  500. package/skills/gsd-path-decide/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  501. package/skills/gsd-path-decide/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  502. package/skills/gsd-path-decide/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  503. package/skills/gsd-path-decide/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  504. package/skills/gsd-path-define/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  505. package/skills/gsd-path-define/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  506. package/skills/gsd-path-define/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  507. package/skills/gsd-path-define/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  508. package/skills/gsd-path-define/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  509. package/skills/gsd-path-define/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  510. package/skills/gsd-path-define/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  511. package/skills/gsd-path-define/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  512. package/skills/gsd-path-define/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  513. package/skills/gsd-path-define/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  514. package/skills/gsd-path-docs-audit/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  515. package/skills/gsd-path-docs-audit/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  516. package/skills/gsd-path-docs-audit/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  517. package/skills/gsd-path-docs-audit/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  518. package/skills/gsd-path-docs-audit/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  519. package/skills/gsd-path-docs-audit/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  520. package/skills/gsd-path-docs-audit/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  521. package/skills/gsd-path-docs-audit/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  522. package/skills/gsd-path-docs-audit/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  523. package/skills/gsd-path-docs-audit/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  524. package/skills/gsd-path-docs-audit/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  525. package/skills/gsd-path-forensics/scripts/__pycache__/pipeline_undo.cpython-312.pyc +0 -0
  526. package/skills/gsd-path-inspect/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  527. package/skills/gsd-path-inspect/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  528. package/skills/gsd-path-inspect/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  529. package/skills/gsd-path-inspect/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  530. package/skills/gsd-path-inspect/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  531. package/skills/gsd-path-inspect/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  532. package/skills/gsd-path-inspect/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  533. package/skills/gsd-path-inspect/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  534. package/skills/gsd-path-inspect/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  535. package/skills/gsd-path-inspect/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  536. package/skills/gsd-path-plan/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  537. package/skills/gsd-path-plan/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  538. package/skills/gsd-path-plan/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  539. package/skills/gsd-path-plan/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  540. package/skills/gsd-path-plan/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  541. package/skills/gsd-path-plan/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  542. package/skills/gsd-path-plan/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  543. package/skills/gsd-path-plan/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  544. package/skills/gsd-path-plan/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  545. package/skills/gsd-path-research/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  546. package/skills/gsd-path-research/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  547. package/skills/gsd-path-research/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  548. package/skills/gsd-path-research/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  549. package/skills/gsd-path-research/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  550. package/skills/gsd-path-research/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  551. package/skills/gsd-path-research/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  552. package/skills/gsd-path-research/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  553. package/skills/gsd-path-research/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  554. package/skills/gsd-path-research/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  555. package/skills/gsd-path-roadmap/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  556. package/skills/gsd-path-roadmap/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  557. package/skills/gsd-path-roadmap/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  558. package/skills/gsd-path-roadmap/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  559. package/skills/gsd-path-roadmap/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  560. package/skills/gsd-path-roadmap/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  561. package/skills/gsd-path-roadmap/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  562. package/skills/gsd-path-roadmap/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  563. package/skills/gsd-path-roadmap/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  564. package/skills/gsd-path-roadmap/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
  565. package/skills/gsd-path-ship/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
  566. package/skills/gsd-path-ship/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
  567. package/skills/gsd-path-ship/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
  568. package/skills/gsd-path-ship/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  569. package/skills/gsd-path-ship/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
  570. package/skills/gsd-path-ship/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
  571. package/skills/gsd-path-ship/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
  572. package/skills/gsd-path-ship/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
  573. package/skills/gsd-path-ship/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
  574. package/skills/gsd-path-ship/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
  575. package/skills/gsd-path-ship/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
  576. package/skills/gsd-path-ship/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
  577. package/skills/gsd-path-ship/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
@@ -3,6 +3,7 @@
3
3
  // Dependency-free; requires Node >= 18.17.
4
4
 
5
5
  import { spawnSync } from "node:child_process";
6
+ import { createHash } from "node:crypto";
6
7
  import fs from "node:fs";
7
8
  import os from "node:os";
8
9
  import path from "node:path";
@@ -28,6 +29,7 @@ export const PROJECT_RUNTIME_SCRIPTS = [
28
29
  "archive_milestone.py",
29
30
  "review_panel.py",
30
31
  "_common.py",
32
+ "worktree_paths.py",
31
33
  "build_recovery.py",
32
34
  "state_checkpoint.py",
33
35
  "state_promote.py",
@@ -40,52 +42,6 @@ export const PROJECT_STATUS_MARKER = "gsd-path project status launcher";
40
42
  const INSTALL_LOCK_NAME = ".gsd-path-install-lock";
41
43
  const INSTALL_LOCK_OWNER = "owner.json";
42
44
  const INSTALL_LOCK_SCHEMA = "gsd-path/install-lock/v2";
43
- const PROJECT_CONTRACTS = [
44
- ["AGENTS.md", "## Plain-prompt re-entry"],
45
- ["WORKFLOW.md", "### Plain-prompt re-entry"],
46
- ];
47
- const STATUS_ACTIONS = new Set([
48
- "bind-initial",
49
- "block",
50
- "resume-checkpoint",
51
- "resume-next-handoff",
52
- "resume-promotion",
53
- "resume-shipment",
54
- "resume-undo",
55
- "run-phase",
56
- "validate-integrated",
57
- "wait",
58
- ]);
59
- const STATUS_PHASES = new Set(["inspect", "define", "research", "decide", "roadmap", "plan", "build", "ship", "shipped"]);
60
- const STATUS_VALUES = new Set(["active", "done", "blocked"]);
61
- const STATUS_STATE_FIELDS = [
62
- "archive",
63
- "branch",
64
- "integration",
65
- "integration_default",
66
- "integration_source",
67
- "milestone",
68
- "phase",
69
- "pipeline",
70
- "project",
71
- "status",
72
- ];
73
- const STATUS_INTEGRATION_MODES = new Set(["direct", "pull-request"]);
74
- const STATUS_INTEGRATION_SOURCES = new Set(["default", "milestone"]);
75
- const STATUS_TRANSITIONS = new Map([
76
- ["inspect", new Set(["inspect", "define"])],
77
- ["define", new Set(["define", "research", "plan"])],
78
- ["research", new Set(["research", "decide"])],
79
- ["decide", new Set(["decide", "roadmap", "plan"])],
80
- ["roadmap", new Set(["roadmap", "define"])],
81
- ["plan", new Set(["plan", "build"])],
82
- ["build", new Set(["build"])],
83
- ["ship", new Set(["ship", "plan"])],
84
- ["shipped", new Set(["ship"])],
85
- ]);
86
- const STATUS_SLUG = /^[a-z0-9][a-z0-9-]*$/;
87
- const STATUS_BRANCH = /^gsd-path\/M(\d{3,})$/;
88
- const STATUS_ARCHIVE = /^\.project\/archive\/(\d{3,})-([a-z0-9][a-z0-9-]*)\/?$/;
89
45
  export const CLAUDE_MATCHER = ".*";
90
46
  // The managed PreToolUse guard entry, as an object.
91
47
  export function claudeGuardEntry(interpreter) {
@@ -236,21 +192,7 @@ export function resolveGitHooksPath(project) {
236
192
  return path.resolve(project, hooksPath);
237
193
  }
238
194
 
239
- function gitHooksDirectory(project) {
240
- return gitHooksLocation(project).directory;
241
- }
242
195
 
243
- function gitHooksLocation(project) {
244
- const dotGit = path.join(project, ".git");
245
- if (!lexists(dotGit)) return { directory: null, resolved: false };
246
- const resolved = hooks.resolveGitHooksPath(project);
247
- if (resolved !== null) return { directory: resolved, resolved: true };
248
- // Fallback when git is not runnable: only a plain .git directory is safe.
249
- return {
250
- directory: isDirectory(dotGit) ? path.join(dotGit, "hooks") : null,
251
- resolved: false,
252
- };
253
- }
254
196
 
255
197
  function requiredPythonRuntime(command, selected = []) {
256
198
  const targetSuffix = selected.length ? ` for selected hosts: ${selected.join(", ")}` : "";
@@ -263,18 +205,6 @@ function requiredPythonRuntime(command, selected = []) {
263
205
  return interpreter;
264
206
  }
265
207
 
266
- function requiredHookRuntime(project, command, selected = []) {
267
- const targetSuffix = selected.length ? ` for selected hosts: ${selected.join(", ")}` : "";
268
- const interpreter = requiredPythonRuntime(command, selected);
269
- const location = gitHooksLocation(project);
270
- if (!location.resolved) {
271
- throw new InstallerError(
272
- `${command} requires an initialized Git repository with a resolvable hooks ` +
273
- `directory${targetSuffix}`
274
- );
275
- }
276
- return { interpreter, hooksDir: location.directory };
277
- }
278
208
  const HOST_NOTES = {
279
209
  opencode:
280
210
  "note: OpenCode stable discovers the skills but has no documented hard " +
@@ -475,6 +405,11 @@ function isOwnedRouterAlias(skillDir) {
475
405
  }
476
406
  }
477
407
 
408
+ function isManagedProjectRuntime(destination) {
409
+ if (!isFile(destination)) return false;
410
+ return fs.readFileSync(destination, "utf8").includes(PROJECT_RUNTIME_MARKER);
411
+ }
412
+
478
413
  function isManagedInstallEntry(root, name) {
479
414
  if (!isManagedName(name)) return false;
480
415
  if (!Object.hasOwn(ROUTER_ALIASES, name.toLowerCase())) return true;
@@ -578,19 +513,6 @@ function samePath(left, right) {
578
513
  return comparisonPath(left) === comparisonPath(right);
579
514
  }
580
515
 
581
- function validateProjectGitRoot(project) {
582
- let probe = project;
583
- while (!lexists(probe)) probe = path.dirname(probe);
584
- const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
585
- cwd: probe,
586
- encoding: "utf8",
587
- env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
588
- });
589
- const topLevel = result.status === 0 ? result.stdout.trim() : "";
590
- if (topLevel && !samePath(topLevel, project)) {
591
- throw new InstallerError(`project path is not the Git worktree root: ${project}`);
592
- }
593
- }
594
516
 
595
517
  function validateDirectoryDestination(candidate, label) {
596
518
  if (lexists(candidate)) {
@@ -1051,10 +973,15 @@ function createInstallLock(lock) {
1051
973
  }
1052
974
  }
1053
975
 
976
+ export function installLockPath(root) {
977
+ const identity = createHash("sha256").update(resolveNonStrict(root)).digest("hex");
978
+ return path.join(os.homedir(), ".gsd-path", "install-locks", identity, INSTALL_LOCK_NAME);
979
+ }
980
+
1054
981
  function acquireInstallLocks(roots) {
1055
982
  const locks = [];
1056
983
  for (const root of roots) {
1057
- const candidate = path.join(path.dirname(root), INSTALL_LOCK_NAME);
984
+ const candidate = installLockPath(root);
1058
985
  if (!locks.some((lock) => samePath(lock, candidate))) locks.push(candidate);
1059
986
  }
1060
987
  locks.sort();
@@ -1178,822 +1105,12 @@ function rollbackTarget(transaction) {
1178
1105
  // Each entry: [destination, sourceName, literalContent, executable].
1179
1106
  // hooksDir is the pre-resolved git hooks directory (or null); resolving it
1180
1107
  // once per run avoids repeated `git rev-parse` spawns.
1181
- function projectDestinations(project, selected, hooksEnabled, interpreter, hooksDir) {
1182
- const includeClaude = selected.includes("claude");
1183
- const destinations = [
1184
- [path.join(project, "AGENTS.md"), "AGENTS.md", null, false],
1185
- [path.join(project, "WORKFLOW.md"), "WORKFLOW.md", null, false],
1186
- [
1187
- path.join(project, HOOKS_DIRECTORY, PROJECT_STATUS_LAUNCHER),
1188
- path.join("scripts", PROJECT_STATUS_LAUNCHER),
1189
- null,
1190
- false,
1191
- ],
1192
- ];
1193
- for (const name of PROJECT_RUNTIME_SCRIPTS) {
1194
- destinations.push([
1195
- path.join(project, HOOKS_DIRECTORY, "runtime", name),
1196
- path.join("scripts", name),
1197
- null,
1198
- false,
1199
- ]);
1200
- }
1201
- if (includeClaude) {
1202
- destinations.push([path.join(project, ".claude", "CLAUDE.md"), null, CLAUDE_BRIDGE, false]);
1203
- }
1204
- if (hooksEnabled) {
1205
- for (const name of GUARD_SCRIPTS) {
1206
- destinations.push([
1207
- path.join(project, HOOKS_DIRECTORY, name),
1208
- path.join("scripts", name),
1209
- null,
1210
- false,
1211
- ]);
1212
- }
1213
- if (includeClaude) {
1214
- destinations.push([
1215
- path.join(project, ".claude", "settings.json"),
1216
- null,
1217
- claudeHooksSettings(interpreter),
1218
- false,
1219
- ]);
1220
- }
1221
- if (selected.includes("codex")) {
1222
- destinations.push([
1223
- path.join(project, ".codex", "hooks.json"),
1224
- null,
1225
- codexHooksSettings(interpreter),
1226
- false,
1227
- ]);
1228
- }
1229
- if (selected.includes("cursor")) {
1230
- destinations.push([
1231
- path.join(project, ".cursor", "hooks.json"),
1232
- null,
1233
- cursorHooksSettings(interpreter),
1234
- false,
1235
- ]);
1236
- }
1237
- if (hooksDir !== null) {
1238
- for (const [hookName, generator] of GIT_HOOKS) {
1239
- destinations.push([path.join(hooksDir, hookName), null, generator(interpreter), true]);
1240
- }
1241
- }
1242
- }
1243
- return destinations;
1244
- }
1245
-
1246
- function nativeSettingsMergers(project, selected, hooksEnabled) {
1247
- const mergers = new Map();
1248
- if (!hooksEnabled) return mergers;
1249
- if (selected.includes("claude")) {
1250
- mergers.set(path.join(project, ".claude", "settings.json"), mergedClaudeSettings);
1251
- }
1252
- if (selected.includes("codex")) {
1253
- mergers.set(path.join(project, ".codex", "hooks.json"), mergedCodexSettings);
1254
- }
1255
- if (selected.includes("cursor")) {
1256
- mergers.set(path.join(project, ".cursor", "hooks.json"), mergedCursorSettings);
1257
- }
1258
- return mergers;
1259
- }
1260
-
1261
- // --update replaces managed runtime, guard, and git hook files in place and
1262
- // keeps everything else (AGENTS.md, WORKFLOW.md, CLAUDE.md). Returns the
1263
- // managed-file check for a replaceable destination, or null for a kept one.
1264
- function updateReplacement(project, destination, hooksDir) {
1265
- const parent = path.join(project, HOOKS_DIRECTORY);
1266
- const directory = path.dirname(destination);
1267
- if (samePath(directory, path.join(parent, "runtime"))) return isManagedProjectRuntime;
1268
- if (samePath(destination, path.join(parent, PROJECT_STATUS_LAUNCHER))) {
1269
- return isManagedProjectStatusLauncher;
1270
- }
1271
- if (GUARD_SCRIPTS.some((name) => samePath(destination, path.join(parent, name)))) {
1272
- return isManagedGuardScript;
1273
- }
1274
- if (hooksDir !== null && samePath(directory, hooksDir)) return isManagedGitHook;
1275
- return null;
1276
- }
1277
-
1278
- function describeProjectPath(project, destination) {
1279
- const relative = path.relative(project, destination);
1280
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
1281
- return destination.split(path.sep).join("/");
1282
- }
1283
- return relative.split(path.sep).join("/");
1284
- }
1285
-
1286
- function projectFiles(project, selected, hooksEnabled, interpreter, hooksDir) {
1287
- return projectDestinations(project, selected, hooksEnabled, interpreter, hooksDir)
1288
- .map(([destination]) => describeProjectPath(project, destination))
1289
- .join(", ");
1290
- }
1291
-
1292
- function projectResult(project, selected, hooksEnabled, interpreter, hooksDir, update, dryRun) {
1293
- if (!update) {
1294
- const files = projectFiles(project, selected, hooksEnabled, interpreter, hooksDir);
1295
- return `project: ${dryRun ? "would copy" : "copied"} ${files} to ${project}`;
1296
- }
1297
- const mergers = nativeSettingsMergers(project, selected, hooksEnabled);
1298
- const refreshed = [];
1299
- const kept = [];
1300
- for (const [destination] of projectDestinations(project, selected, hooksEnabled, interpreter, hooksDir)) {
1301
- const replaceable =
1302
- mergers.has(destination) || updateReplacement(project, destination, hooksDir) !== null;
1303
- (lexists(destination) && !replaceable ? kept : refreshed).push(describeProjectPath(project, destination));
1304
- }
1305
- let line = `project: ${dryRun ? "would refresh" : "refreshed"} ${refreshed.join(", ")} in ${project}`;
1306
- if (kept.length) line += `; kept ${kept.join(", ")}`;
1307
- return line;
1308
- }
1309
-
1310
- function existingContractError(destination) {
1311
- return new InstallerError(
1312
- `project contract already exists: ${destination} — the installer never ` +
1313
- "overwrites project files. Run --update --project PATH to refresh managed " +
1314
- "runtime files; merge template changes manually (see UPDATE.md)."
1315
- );
1316
- }
1317
-
1318
- function validateProject(sourceRoot, project, selected, hooksEnabled, reservedRoots, interpreter, hooksDir, update = false) {
1319
- const includeClaude = selected.includes("claude");
1320
- validateDirectoryDestination(project, "project path");
1321
- validateProjectGitRoot(project);
1322
- validateDirectoryDestination(
1323
- path.join(project, HOOKS_DIRECTORY),
1324
- "project runtime parent directory"
1325
- );
1326
- validateDirectoryDestination(
1327
- path.join(project, HOOKS_DIRECTORY, "runtime"),
1328
- "project runtime directory"
1329
- );
1330
- const projectDirectories = [];
1331
- if (includeClaude) projectDirectories.push(["Claude", path.join(project, ".claude")]);
1332
- if (hooksEnabled && selected.includes("codex")) {
1333
- projectDirectories.push(["Codex", path.join(project, ".codex")]);
1334
- }
1335
- if (hooksEnabled && selected.includes("cursor")) {
1336
- projectDirectories.push(["Cursor", path.join(project, ".cursor")]);
1337
- }
1338
- for (const [label, directory] of projectDirectories) {
1339
- if (lexists(directory) && (isSymlink(directory) || !isDirectory(directory))) {
1340
- throw new InstallerError(`unsafe ${label} project directory: ${directory}`);
1341
- }
1342
- }
1343
- const sources = [
1344
- "AGENTS.md",
1345
- "WORKFLOW.md",
1346
- path.join("scripts", PROJECT_STATUS_LAUNCHER),
1347
- ...PROJECT_RUNTIME_SCRIPTS.map((name) => path.join("scripts", name)),
1348
- ];
1349
- if (hooksEnabled) {
1350
- sources.push(...GUARD_SCRIPTS.map((name) => path.join("scripts", name)));
1351
- }
1352
- for (const sourceName of sources) {
1353
- const source = path.join(sourceRoot, sourceName);
1354
- if (isSymlink(source) || !isFile(source)) {
1355
- throw new InstallerError(`missing project contract: ${source}`);
1356
- }
1357
- }
1358
- const mergers = nativeSettingsMergers(project, selected, hooksEnabled);
1359
- for (const [destination] of projectDestinations(project, selected, hooksEnabled, interpreter, hooksDir)) {
1360
- if (lexists(destination)) {
1361
- const merge = mergers.get(destination);
1362
- const managed = update ? updateReplacement(project, destination, hooksDir) : null;
1363
- if (isSymlink(destination) || (merge === undefined && !update)) {
1364
- throw existingContractError(destination);
1365
- }
1366
- if (merge !== undefined) {
1367
- merge(destination, interpreter);
1368
- } else if (managed !== null && !managed(destination)) {
1369
- throw new InstallerError(`not a managed GSD Path project file: ${destination}`);
1370
- }
1371
- }
1372
- for (const [label, root] of reservedRoots) {
1373
- if (pathsOverlap(destination, root)) {
1374
- throw new InstallerError(`project contract overlaps ${label}: ${destination}, ${root}`);
1375
- }
1376
- }
1377
- }
1378
- }
1379
-
1380
- function applyProject(sourceRoot, project, selected, hooksEnabled, transaction, interpreter, hooksDir, update = false) {
1381
- createDirectory(project, transaction.createdDirectories);
1382
- const mergers = nativeSettingsMergers(project, selected, hooksEnabled);
1383
- for (const [destination, sourceName, literal, executable] of projectDestinations(
1384
- project,
1385
- selected,
1386
- hooksEnabled,
1387
- interpreter,
1388
- hooksDir
1389
- )) {
1390
- createDirectory(path.dirname(destination), transaction.createdDirectories);
1391
- const merge = mergers.get(destination);
1392
- const content = sourceName
1393
- ? fs.readFileSync(path.join(sourceRoot, sourceName))
1394
- : Buffer.from(literal, "utf8");
1395
- if (lexists(destination) && !isSymlink(destination)) {
1396
- const replaceable = update && updateReplacement(project, destination, hooksDir) !== null;
1397
- if (merge !== undefined || replaceable) {
1398
- const original = fs.readFileSync(destination);
1399
- const mode = fs.statSync(destination).mode & 0o777;
1400
- const replacement = merge !== undefined ? merge(destination, interpreter) : content;
1401
- writeFileAtomic(destination, replacement, executable ? 0o755 : mode);
1402
- transaction.replaced.push({ destination, original, mode });
1403
- continue;
1404
- }
1405
- if (update) continue;
1406
- }
1407
- let fd = null;
1408
- try {
1409
- fd = fs.openSync(destination, "wx");
1410
- fs.writeSync(fd, content);
1411
- if (executable) fs.fchmodSync(fd, 0o755);
1412
- fs.closeSync(fd);
1413
- fd = null;
1414
- transaction.copied.push(destination);
1415
- } catch (error) {
1416
- if (fd !== null) fs.closeSync(fd);
1417
- if (error && error.code === "EEXIST") {
1418
- throw existingContractError(destination);
1419
- }
1420
- removePath(destination);
1421
- throw error;
1422
- }
1423
- }
1424
- }
1425
-
1426
- function rollbackProject(transaction) {
1427
- for (const { destination, original, mode } of [...transaction.replaced].reverse()) {
1428
- writeFileAtomic(destination, original, mode);
1429
- }
1430
- for (const destination of [...transaction.copied].reverse()) {
1431
- removePath(destination);
1432
- }
1433
- removeEmptyDirectories(transaction.createdDirectories);
1434
- }
1435
-
1436
- function managedFileContains(destination, marker, label) {
1437
- if (!isFile(destination)) return false;
1438
- try {
1439
- return fs.readFileSync(destination, "utf8").includes(marker);
1440
- } catch {
1441
- throw new InstallerError(`cannot read ${label}: ${destination}`);
1442
- }
1443
- }
1444
-
1445
- function isManagedGuardScript(destination) {
1446
- return managedFileContains(destination, GUARD_MARKER, "guard script");
1447
- }
1448
-
1449
- function isManagedProjectRuntime(destination) {
1450
- return managedFileContains(destination, PROJECT_RUNTIME_MARKER, "project runtime");
1451
- }
1452
-
1453
- function isManagedProjectStatusLauncher(destination) {
1454
- return managedFileContains(
1455
- destination,
1456
- PROJECT_STATUS_MARKER,
1457
- "project status launcher"
1458
- );
1459
- }
1460
-
1461
- function isManagedGitHook(destination) {
1462
- if (!isFile(destination)) return false;
1463
- return isManagedGitHookContent(fs.readFileSync(destination, "utf8"));
1464
- }
1465
-
1466
- function isManagedGitHookContent(text) {
1467
- return text.includes(GUARD_MARKER) && text.includes("git_guard.py");
1468
- }
1469
-
1470
- function isManagedHookSettings(destination) {
1471
- if (!isFile(destination)) return false;
1472
- try {
1473
- return hasManagedHookSettings(parsedManagedSettings(destination));
1474
- } catch {
1475
- return false;
1476
- }
1477
- }
1478
-
1479
- function hasManagedGuardWiring(project) {
1480
- const settings = [
1481
- path.join(project, ".claude", "settings.json"),
1482
- path.join(project, ".codex", "hooks.json"),
1483
- path.join(project, ".cursor", "hooks.json"),
1484
- ];
1485
- if (settings.some(isManagedHookSettings)) return true;
1486
- const hooksDir = gitHooksDirectory(project);
1487
- return (
1488
- hooksDir !== null &&
1489
- GIT_HOOK_NAMES.some((name) => isManagedGitHook(path.join(hooksDir, name)))
1490
- );
1491
- }
1492
-
1493
- function atomicTemporary(destination) {
1494
- const directory = fs.mkdtempSync(
1495
- path.join(path.dirname(destination), `.${path.basename(destination)}.gsd-path-tmp-`)
1496
- );
1497
- return { directory, temporary: path.join(directory, "value") };
1498
- }
1499
-
1500
- function writeFileAtomic(destination, content, mode) {
1501
- const { directory, temporary } = atomicTemporary(destination);
1502
- try {
1503
- fs.writeFileSync(temporary, content, { flag: "wx" });
1504
- if (mode !== undefined) {
1505
- fs.chmodSync(temporary, mode);
1506
- } else if (isFile(destination) && !isSymlink(destination)) {
1507
- fs.chmodSync(temporary, fs.statSync(destination).mode & 0o777);
1508
- }
1509
- fs.renameSync(temporary, destination);
1510
- } finally {
1511
- removePath(directory);
1512
- }
1513
- }
1514
-
1515
- function copyFileAtomic(source, destination) {
1516
- const { directory, temporary } = atomicTemporary(destination);
1517
- try {
1518
- fs.copyFileSync(source, temporary, fs.constants.COPYFILE_EXCL);
1519
- fs.renameSync(temporary, destination);
1520
- } finally {
1521
- removePath(directory);
1522
- }
1523
- }
1524
-
1525
- // A PreToolUse entry is ours when one of its commands runs the guard hook.
1526
- function isGuardCommand(command) {
1527
- const normalized = command.replaceAll("\\", "/");
1528
- const match = /^(?:python3|python)\s+(?:"([^"\r\n]+)"|'([^'\r\n]+)'|(\S+))$/.exec(
1529
- normalized
1530
- );
1531
- if (match === null) return false;
1532
- const script = match[1] || match[2] || match[3];
1533
- const managedScript = `${HOOKS_DIRECTORY}/guard_hook.py`;
1534
- return script === managedScript || script.endsWith(`/${managedScript}`);
1535
- }
1536
-
1537
- function isManagedCommandHook(hook) {
1538
- return Boolean(
1539
- hook &&
1540
- typeof hook === "object" &&
1541
- !Array.isArray(hook) &&
1542
- typeof hook.command === "string" &&
1543
- isGuardCommand(hook.command)
1544
- );
1545
- }
1546
-
1547
- function isManagedHookEntry(entry) {
1548
- return Boolean(
1549
- entry &&
1550
- typeof entry === "object" &&
1551
- !Array.isArray(entry) &&
1552
- Array.isArray(entry.hooks) &&
1553
- entry.hooks.some(isManagedCommandHook)
1554
- );
1555
- }
1556
-
1557
- function isManagedDirectHookEntry(entry) {
1558
- return isManagedCommandHook(entry);
1559
- }
1560
-
1561
- function hasManagedHookSettings(parsed) {
1562
- const hooks = parsed.hooks;
1563
- if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return false;
1564
- const nested = hooks.PreToolUse;
1565
- const direct = hooks.preToolUse;
1566
- return (
1567
- (Array.isArray(nested) && nested.some(isManagedHookEntry)) ||
1568
- (Array.isArray(direct) && direct.some(isManagedDirectHookEntry))
1569
- );
1570
- }
1571
-
1572
- function parsedManagedSettings(settings) {
1573
- let parsed;
1574
- try {
1575
- parsed = JSON.parse(fs.readFileSync(settings, "utf8"));
1576
- } catch {
1577
- throw new InstallerError(`managed hook settings file is not valid JSON: ${settings}`);
1578
- }
1579
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1580
- throw new InstallerError(`managed hook settings file is not a JSON object: ${settings}`);
1581
- }
1582
- return parsed;
1583
- }
1584
-
1585
- function mergedHookSettings(settings, eventName, managedEntry, isManagedEntry) {
1586
- const parsed = parsedManagedSettings(settings);
1587
- const hooksObject =
1588
- parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks)
1589
- ? parsed.hooks
1590
- : {};
1591
- const existing = Array.isArray(hooksObject[eventName]) ? hooksObject[eventName] : [];
1592
- const merged = [];
1593
- let replaced = false;
1594
- for (const entry of existing) {
1595
- if (isManagedEntry(entry)) {
1596
- if (!replaced) {
1597
- merged.push(managedEntry);
1598
- replaced = true;
1599
- }
1600
- } else {
1601
- merged.push(entry);
1602
- }
1603
- }
1604
- if (!replaced) merged.push(managedEntry);
1605
- hooksObject[eventName] = merged;
1606
- parsed.hooks = hooksObject;
1607
- return JSON.stringify(parsed, null, 2) + "\n";
1608
- }
1609
-
1610
- function mergedNestedHookSettings(settings, managedEntry) {
1611
- const parsed = parsedManagedSettings(settings);
1612
- const hooksObject =
1613
- parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks)
1614
- ? parsed.hooks
1615
- : {};
1616
- const existing = Array.isArray(hooksObject.PreToolUse) ? hooksObject.PreToolUse : [];
1617
- const merged = [];
1618
- let replaced = false;
1619
- for (const entry of existing) {
1620
- if (!isManagedHookEntry(entry)) {
1621
- merged.push(entry);
1622
- continue;
1623
- }
1624
- const unrelatedHooks = entry.hooks.filter((hook) => !isManagedCommandHook(hook));
1625
- if (!replaced) {
1626
- merged.push(managedEntry);
1627
- replaced = true;
1628
- }
1629
- if (unrelatedHooks.length > 0) {
1630
- merged.push({ ...entry, hooks: unrelatedHooks });
1631
- }
1632
- }
1633
- if (!replaced) merged.push(managedEntry);
1634
- hooksObject.PreToolUse = merged;
1635
- parsed.hooks = hooksObject;
1636
- return JSON.stringify(parsed, null, 2) + "\n";
1637
- }
1638
-
1639
- function mergedClaudeSettings(settings, interpreter) {
1640
- return mergedNestedHookSettings(settings, claudeGuardEntry(interpreter));
1641
- }
1642
-
1643
- function mergedCodexSettings(settings, interpreter) {
1644
- const managedEntry = JSON.parse(codexHooksSettings(interpreter)).hooks.PreToolUse[0];
1645
- return mergedNestedHookSettings(settings, managedEntry);
1646
- }
1647
-
1648
- function mergedCursorSettings(settings, interpreter) {
1649
- const managedEntry = JSON.parse(cursorHooksSettings(interpreter)).hooks.preToolUse[0];
1650
- return mergedHookSettings(settings, "preToolUse", managedEntry, isManagedDirectHookEntry);
1651
- }
1652
-
1653
- function refreshesGuards(project, full, initialize) {
1654
- return (
1655
- full ||
1656
- initialize ||
1657
- hasManagedGuardWiring(project) ||
1658
- GUARD_SCRIPTS.some((name) => lexists(path.join(project, HOOKS_DIRECTORY, name)))
1659
- );
1660
- }
1661
-
1662
- function contractSection(content, heading) {
1663
- const normalized = content.replaceAll("\r\n", "\n");
1664
- const startPattern = new RegExp(`^${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m");
1665
- const startMatch = startPattern.exec(normalized);
1666
- if (startMatch === null) return null;
1667
- const depth = heading.indexOf(" ");
1668
- const afterHeading = normalized.indexOf("\n", startMatch.index);
1669
- const bodyStart = afterHeading === -1 ? normalized.length : afterHeading + 1;
1670
- const nextHeading = new RegExp(`^#{1,${depth}}\\s`, "m").exec(normalized.slice(bodyStart));
1671
- const end = nextHeading === null ? normalized.length : bodyStart + nextHeading.index;
1672
- return normalized.slice(startMatch.index, end).trimEnd();
1673
- }
1674
-
1675
- function hasLegacyProjectContracts(sourceRoot, project) {
1676
- return PROJECT_CONTRACTS.every(([name, heading]) => {
1677
- const candidate = path.join(project, name);
1678
- const source = path.join(sourceRoot, name);
1679
- if (
1680
- isSymlink(candidate) ||
1681
- !isFile(candidate) ||
1682
- isSymlink(source) ||
1683
- !isFile(source)
1684
- ) return false;
1685
- const installed = contractSection(fs.readFileSync(candidate, "utf8"), heading);
1686
- const canonical = contractSection(fs.readFileSync(source, "utf8"), heading);
1687
- return installed !== null && installed === canonical;
1688
- });
1689
- }
1690
-
1691
- function validateHooksRefresh(sourceRoot, project, full, hooksDir, selected, initialize = false) {
1692
- validateDirectoryDestination(project, "project path");
1693
- validateProjectGitRoot(project);
1694
- validateDirectoryDestination(
1695
- path.join(project, HOOKS_DIRECTORY),
1696
- "guard hooks directory"
1697
- );
1698
- validateDirectoryDestination(
1699
- path.join(project, HOOKS_DIRECTORY, "runtime"),
1700
- "project runtime directory"
1701
- );
1702
- const refreshGuards = refreshesGuards(project, full, initialize);
1703
- const runtimeExists = PROJECT_RUNTIME_SCRIPTS.some((name) =>
1704
- lexists(path.join(project, HOOKS_DIRECTORY, "runtime", name))
1705
- );
1706
- if (
1707
- !initialize &&
1708
- !refreshGuards &&
1709
- !runtimeExists &&
1710
- !hasLegacyProjectContracts(sourceRoot, project)
1711
- ) {
1712
- throw new InstallerError(
1713
- `no managed GSD Path hooks or runtime found in project: ${project}`
1714
- );
1715
- }
1716
- const runtime = path.join(project, HOOKS_DIRECTORY, "runtime");
1717
- const launcher = path.join(project, HOOKS_DIRECTORY, PROJECT_STATUS_LAUNCHER);
1718
- const launcherSource = path.join(sourceRoot, "scripts", PROJECT_STATUS_LAUNCHER);
1719
- if (isSymlink(launcher) || (lexists(launcher) && !isManagedProjectStatusLauncher(launcher))) {
1720
- throw new InstallerError(`not a managed GSD Path status launcher: ${launcher}`);
1721
- }
1722
- if (isSymlink(launcherSource) || !isFile(launcherSource)) {
1723
- throw new InstallerError(`missing project status launcher source: ${launcherSource}`);
1724
- }
1725
- if (isDirectory(runtime)) {
1726
- const unexpected = fs
1727
- .readdirSync(runtime)
1728
- .filter((name) => !PROJECT_RUNTIME_SCRIPTS.includes(name));
1729
- if (unexpected.length) {
1730
- throw new InstallerError(`unexpected project runtime entries: ${unexpected.join(", ")}`);
1731
- }
1732
- }
1733
- if (refreshGuards) {
1734
- for (const name of GUARD_SCRIPTS) {
1735
- const destination = path.join(project, HOOKS_DIRECTORY, name);
1736
- const exists = lexists(destination);
1737
- if (isSymlink(destination)) {
1738
- throw new InstallerError(`refusing to refresh a symlink: ${destination}`);
1739
- }
1740
- if (exists && !isManagedGuardScript(destination)) {
1741
- throw new InstallerError(`not a managed GSD Path guard script: ${destination}`);
1742
- }
1743
- const source = path.join(sourceRoot, "scripts", name);
1744
- if (isSymlink(source) || !isFile(source)) {
1745
- throw new InstallerError(`missing guard script source: ${source}`);
1746
- }
1747
- }
1748
- }
1749
- for (const name of PROJECT_RUNTIME_SCRIPTS) {
1750
- const destination = path.join(project, HOOKS_DIRECTORY, "runtime", name);
1751
- if (isSymlink(destination)) {
1752
- throw new InstallerError(`refusing to refresh a symlink: ${destination}`);
1753
- }
1754
- if (lexists(destination) && !isManagedProjectRuntime(destination)) {
1755
- throw new InstallerError(`not a managed GSD Path project runtime: ${destination}`);
1756
- }
1757
- const source = path.join(sourceRoot, "scripts", name);
1758
- if (isSymlink(source) || !isFile(source)) {
1759
- throw new InstallerError(`missing project runtime source: ${source}`);
1760
- }
1761
- }
1762
- if (full) {
1763
- for (const [target, label, settings] of [
1764
- ["claude", "Claude", path.join(project, ".claude", "settings.json")],
1765
- ["codex", "Codex", path.join(project, ".codex", "hooks.json")],
1766
- ["cursor", "Cursor", path.join(project, ".cursor", "hooks.json")],
1767
- ]) {
1768
- const exists = lexists(settings);
1769
- if (initialize && !selected.includes(target)) continue;
1770
- if (!exists && !selected.includes(target)) continue;
1771
- validateDirectoryDestination(
1772
- path.dirname(settings),
1773
- `unsafe ${label} project directory`
1774
- );
1775
- if (isSymlink(settings)) {
1776
- throw new InstallerError(`refusing to refresh a symlink: ${settings}`);
1777
- }
1778
- if (exists) {
1779
- const parsed = parsedManagedSettings(settings);
1780
- if (!selected.includes(target) && !hasManagedHookSettings(parsed)) {
1781
- throw new InstallerError(`not a managed GSD Path hook settings file: ${settings}`);
1782
- }
1783
- }
1784
- }
1785
- if (hooksDir !== null) {
1786
- for (const hookName of GIT_HOOK_NAMES) {
1787
- const hookPath = path.join(hooksDir, hookName);
1788
- if (isSymlink(hookPath)) {
1789
- throw new InstallerError(`refusing to refresh a symlink: ${hookPath}`);
1790
- }
1791
- if (lexists(hookPath) && !isManagedGitHook(hookPath)) {
1792
- throw new InstallerError(`not a managed GSD Path git hook: ${hookPath}`);
1793
- }
1794
- }
1795
- }
1796
- }
1797
- }
1798
-
1799
- function publishProjectRuntime(sourceRoot, project, refreshGuards) {
1800
- const parent = path.join(project, HOOKS_DIRECTORY);
1801
- const runtime = path.join(parent, "runtime");
1802
- fs.mkdirSync(parent, { recursive: true });
1803
- const staging = fs.mkdtempSync(path.join(parent, ".runtime-stage-"));
1804
- const previous = `${staging}-previous`;
1805
- let movedPrevious = false;
1806
- let publishedRuntime = false;
1807
- const launcher = path.join(parent, PROJECT_STATUS_LAUNCHER);
1808
- const launcherOriginal = lexists(launcher) ? fs.readFileSync(launcher) : null;
1809
- const launcherMode = launcherOriginal === null ? undefined : fs.statSync(launcher).mode & 0o777;
1810
- const guardOriginals = [];
1811
- try {
1812
- for (const name of PROJECT_RUNTIME_SCRIPTS) {
1813
- hooks.copyRuntimeFile(
1814
- path.join(sourceRoot, "scripts", name),
1815
- path.join(staging, name)
1816
- );
1817
- }
1818
- copyFileAtomic(path.join(sourceRoot, "scripts", PROJECT_STATUS_LAUNCHER), launcher);
1819
- if (lexists(runtime)) {
1820
- fs.renameSync(runtime, previous);
1821
- movedPrevious = true;
1822
- }
1823
- fs.renameSync(staging, runtime);
1824
- publishedRuntime = true;
1825
- if (refreshGuards) {
1826
- for (const name of GUARD_SCRIPTS) {
1827
- const destination = path.join(parent, name);
1828
- const original = lexists(destination) ? fs.readFileSync(destination) : null;
1829
- const mode =
1830
- original === null ? undefined : fs.statSync(destination).mode & 0o777;
1831
- guardOriginals.push([destination, original, mode]);
1832
- hooks.copyGuardFile(path.join(sourceRoot, "scripts", name), destination);
1833
- }
1834
- }
1835
- if (movedPrevious) removePath(previous);
1836
- } catch (error) {
1837
- for (const [destination, original, mode] of guardOriginals.reverse()) {
1838
- if (original === null) removePath(destination);
1839
- else writeFileAtomic(destination, original, mode);
1840
- }
1841
- if (launcherOriginal === null) removePath(launcher);
1842
- else writeFileAtomic(launcher, launcherOriginal, launcherMode);
1843
- if (publishedRuntime && lexists(runtime)) removePath(runtime);
1844
- if (!lexists(runtime) && movedPrevious && lexists(previous)) {
1845
- fs.renameSync(previous, runtime);
1846
- }
1847
- throw new InstallerError(`project runtime refresh failed: ${error.message}`);
1848
- } finally {
1849
- removePath(staging);
1850
- }
1851
- }
1852
-
1853
- // Returns refreshed project-relative paths; entries prefixed "note:" are
1854
- // user-facing notes rather than refreshed files.
1855
- function refreshHooksUnlocked(sourceRoot, project, full, dryRun, selected, initialize) {
1856
- let hooksDir = full ? gitHooksDirectory(project) : null;
1857
- let interpreter = null;
1858
- if (!dryRun) {
1859
- if (full) {
1860
- ({ interpreter, hooksDir } = requiredHookRuntime(
1861
- project,
1862
- initialize ? "--hooks-init" : "--hooks-refresh-full",
1863
- selected
1864
- ));
1865
- } else {
1866
- interpreter = requiredPythonRuntime("--hooks-refresh", selected);
1867
- }
1868
- }
1869
- validateHooksRefresh(sourceRoot, project, full, hooksDir, selected, initialize);
1870
- const refreshed = [];
1871
- const refreshGuards = refreshesGuards(project, full, initialize);
1872
- if (!dryRun) publishProjectRuntime(sourceRoot, project, refreshGuards);
1873
- for (const name of PROJECT_RUNTIME_SCRIPTS) {
1874
- refreshed.push(
1875
- describeProjectPath(project, path.join(project, HOOKS_DIRECTORY, "runtime", name))
1876
- );
1877
- }
1878
- refreshed.push(
1879
- describeProjectPath(
1880
- project,
1881
- path.join(project, HOOKS_DIRECTORY, PROJECT_STATUS_LAUNCHER)
1882
- )
1883
- );
1884
- if (refreshGuards) {
1885
- for (const name of GUARD_SCRIPTS) {
1886
- const destination = path.join(project, HOOKS_DIRECTORY, name);
1887
- refreshed.push(describeProjectPath(project, destination));
1888
- }
1889
- }
1890
- if (full) {
1891
- for (const [target, settings, merge, generated] of [
1892
- [
1893
- "claude",
1894
- path.join(project, ".claude", "settings.json"),
1895
- mergedClaudeSettings,
1896
- claudeHooksSettings,
1897
- ],
1898
- [
1899
- "codex",
1900
- path.join(project, ".codex", "hooks.json"),
1901
- mergedCodexSettings,
1902
- codexHooksSettings,
1903
- ],
1904
- [
1905
- "cursor",
1906
- path.join(project, ".cursor", "hooks.json"),
1907
- mergedCursorSettings,
1908
- cursorHooksSettings,
1909
- ],
1910
- ]) {
1911
- if (initialize && !selected.includes(target)) continue;
1912
- const exists = lexists(settings);
1913
- if (exists || selected.includes(target)) {
1914
- if (!dryRun) {
1915
- fs.mkdirSync(path.dirname(settings), { recursive: true });
1916
- const content = exists ? merge(settings, interpreter) : generated(interpreter);
1917
- writeFileAtomic(settings, content);
1918
- }
1919
- refreshed.push(describeProjectPath(project, settings));
1920
- }
1921
- }
1922
- if (hooksDir !== null) {
1923
- for (const [hookName, generator] of GIT_HOOKS) {
1924
- const hookPath = path.join(hooksDir, hookName);
1925
- if (!dryRun) {
1926
- const content = generator(interpreter);
1927
- fs.mkdirSync(path.dirname(hookPath), { recursive: true });
1928
- writeFileAtomic(hookPath, content, 0o755);
1929
- }
1930
- refreshed.push(describeProjectPath(project, hookPath));
1931
- }
1932
- }
1933
- }
1934
- return refreshed;
1935
- }
1936
-
1937
1108
  function refreshHooks(sourceRoot, project, full, dryRun, selected = [], initialize = false) {
1938
- if (dryRun) {
1939
- return refreshHooksUnlocked(sourceRoot, project, full, true, selected, initialize);
1940
- }
1941
- validateDirectoryDestination(project, "project path");
1942
- const ownership = acquireInstallLocks([path.join(project, HOOKS_DIRECTORY)]);
1943
- try {
1944
- return refreshHooksUnlocked(sourceRoot, project, full, false, selected, initialize);
1945
- } finally {
1946
- releaseInstallLocks(ownership.locks, ownership.createdDirectories);
1947
- }
1109
+ return hooks.projectAdapter(sourceRoot, project, "refresh", { full, dryRun, selected, initialize });
1948
1110
  }
1949
1111
 
1950
1112
  const STATE_SCHEMA = "gsd-path/state/v1";
1951
1113
 
1952
- function validatedProjectState(sourceRoot, project) {
1953
- const validator = path.join(sourceRoot, "scripts", "pipeline_state.py");
1954
- if (isSymlink(validator) || !isFile(validator)) {
1955
- throw new InstallerError(`canonical state validator is unavailable: ${validator}`);
1956
- }
1957
- const interpreter = effectiveInterpreter();
1958
- if (interpreter === null) {
1959
- throw new InstallerError("canonical state validation requires a working Python interpreter");
1960
- }
1961
- const result = spawnSync(
1962
- interpreter,
1963
- ["-B", validator, "validate", "--repo", project],
1964
- { cwd: project, encoding: "utf8" }
1965
- );
1966
- if (result.error || result.status !== 0) {
1967
- const detail =
1968
- result.error?.message ||
1969
- (result.stderr || "").trim() ||
1970
- (result.stdout || "").trim() ||
1971
- "unknown failure";
1972
- throw new InstallerError(`canonical state validation failed: ${detail}`);
1973
- }
1974
- let payload;
1975
- try {
1976
- payload = JSON.parse(result.stdout);
1977
- } catch {
1978
- throw new InstallerError("canonical state validator returned invalid JSON");
1979
- }
1980
- if (!payload || typeof payload !== "object") {
1981
- throw new InstallerError("canonical state validator returned an invalid payload");
1982
- }
1983
- const state = payload.state;
1984
- if (
1985
- payload.schema !== STATE_SCHEMA ||
1986
- payload.status !== "valid" ||
1987
- !state ||
1988
- typeof state !== "object" ||
1989
- typeof state.phase !== "string" ||
1990
- typeof state.status !== "string"
1991
- ) {
1992
- throw new InstallerError("canonical state validator returned an invalid payload");
1993
- }
1994
- return state;
1995
- }
1996
-
1997
1114
  function hasManagedInstall(root) {
1998
1115
  return (
1999
1116
  isDirectory(root) &&
@@ -2001,190 +1118,16 @@ function hasManagedInstall(root) {
2001
1118
  );
2002
1119
  }
2003
1120
 
2004
- function validateProjectRuntimeStatus(sourceRoot, project) {
2005
- const interpreter = requiredPythonRuntime("--doctor");
2006
- const runtime = path.join(sourceRoot, "scripts", "pipeline_state.py");
2007
- const result = spawnSync(
2008
- interpreter,
2009
- ["-B", runtime, "status", "--repo", project],
2010
- {
2011
- cwd: project,
2012
- encoding: "utf8",
2013
- env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
2014
- }
2015
- );
2016
- if (result.error || result.status !== 0) {
2017
- const detail = result.error?.message || result.stderr?.trim() || result.stdout?.trim() || "unknown failure";
2018
- throw new InstallerError(`project runtime status failed: ${detail}`);
2019
- }
2020
- let payload;
2021
- try {
2022
- payload = JSON.parse(result.stdout);
2023
- } catch {
2024
- throw new InstallerError("project runtime status returned invalid JSON");
2025
- }
2026
- if (!validStatusPayload(payload, project)) {
2027
- throw new InstallerError("project runtime status returned an invalid payload");
2028
- }
2029
- }
2030
-
2031
- function projectRuntimeMatches(sourceRoot, project) {
2032
- const pairs = [
2033
- [
2034
- path.join(project, HOOKS_DIRECTORY, PROJECT_STATUS_LAUNCHER),
2035
- path.join(sourceRoot, "scripts", PROJECT_STATUS_LAUNCHER),
2036
- ],
2037
- ...PROJECT_RUNTIME_SCRIPTS.map((name) => [
2038
- path.join(project, HOOKS_DIRECTORY, "runtime", name),
2039
- path.join(sourceRoot, "scripts", name),
2040
- ]),
2041
- ];
2042
- try {
2043
- return pairs.every(
2044
- ([destination, source]) =>
2045
- !isSymlink(destination) &&
2046
- isFile(destination) &&
2047
- fs.readFileSync(destination).equals(fs.readFileSync(source))
2048
- );
2049
- } catch {
2050
- return false;
2051
- }
2052
- }
2053
-
2054
- function validStatusPayload(payload, project) {
2055
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
2056
- const state = payload.state;
2057
- const route = payload.route;
2058
- if (
2059
- payload.schema !== "gsd-path/status/v1" ||
2060
- payload.advance !== false ||
2061
- !state ||
2062
- typeof state !== "object" ||
2063
- !validStatusState(state) ||
2064
- !route ||
2065
- typeof route !== "object" ||
2066
- !STATUS_ACTIONS.has(route.action) ||
2067
- typeof route.reason !== "string" ||
2068
- !route.reason ||
2069
- typeof payload.path !== "string" ||
2070
- !path.isAbsolute(payload.path) ||
2071
- !samePath(payload.path, path.join(project, ".project", "STATE.md"))
2072
- ) {
2073
- return false;
2074
- }
2075
- let expectedNextSkill = null;
2076
- if (route.action === "run-phase") {
2077
- if (
2078
- typeof route.phase !== "string" ||
2079
- !STATUS_TRANSITIONS.get(state.phase).has(route.phase) ||
2080
- state.branch === null
2081
- ) return false;
2082
- expectedNextSkill = `gsd-path-${route.phase}`;
2083
- } else if (Object.hasOwn(route, "phase")) {
2084
- return false;
2085
- } else if (route.action === "bind-initial") {
2086
- return (
2087
- state.branch === null &&
2088
- typeof route.branch === "string" &&
2089
- validStatusBranch(route.branch) !== null &&
2090
- payload.next_skill === "gsd-path"
2091
- );
2092
- } else if (route.action === "resume-undo") {
2093
- expectedNextSkill = "gsd-path-undo";
2094
- } else if (route.action !== "wait") {
2095
- expectedNextSkill = "gsd-path";
2096
- }
2097
- if (state.branch === null) return false;
2098
- if (route.action === "validate-integrated" && !(state.phase === "shipped" && state.status === "done")) return false;
2099
- if (route.action === "wait" && !(state.phase === "plan" && state.status === "done")) return false;
2100
- return payload.next_skill === expectedNextSkill;
2101
- }
2102
-
2103
- function validStatusState(state) {
2104
- if (Object.keys(state).sort().join("\0") !== STATUS_STATE_FIELDS.join("\0")) return false;
2105
- const archiveMatch = typeof state.archive === "string" ? STATUS_ARCHIVE.exec(state.archive) : null;
2106
- if (
2107
- state.pipeline !== "gsd-path/v2" ||
2108
- typeof state.project !== "string" ||
2109
- !STATUS_SLUG.test(state.project) ||
2110
- !(state.milestone === null || typeof state.milestone === "string" && STATUS_SLUG.test(state.milestone)) ||
2111
- !STATUS_PHASES.has(state.phase) ||
2112
- !STATUS_VALUES.has(state.status) ||
2113
- !STATUS_INTEGRATION_MODES.has(state.integration_default) ||
2114
- !STATUS_INTEGRATION_MODES.has(state.integration) ||
2115
- !STATUS_INTEGRATION_SOURCES.has(state.integration_source) ||
2116
- (state.integration_source === "default" &&
2117
- state.integration !== state.integration_default) ||
2118
- !(state.branch === null || validStatusBranch(state.branch) !== null) ||
2119
- !(state.archive === null || archiveMatch)
2120
- ) return false;
2121
- if (state.phase === "shipped" && (state.status !== "done" || state.archive === null)) return false;
2122
- if (
2123
- state.archive !== null &&
2124
- state.phase !== "build" &&
2125
- state.phase !== "ship" &&
2126
- state.phase !== "shipped"
2127
- ) return false;
2128
- if ((state.phase === "ship" || state.phase === "shipped") && state.branch === null) return false;
2129
- if (archiveMatch) {
2130
- const branchMatch = validStatusBranch(state.branch);
2131
- return state.milestone === archiveMatch[2] && branchMatch !== null && Number(branchMatch[1]) === Number(archiveMatch[1]);
2132
- }
2133
- return true;
2134
- }
2135
-
2136
- function validStatusBranch(value) {
2137
- if (typeof value !== "string") return null;
2138
- const match = STATUS_BRANCH.exec(value);
2139
- return match !== null && Number(match[1]) >= 1 ? match : null;
2140
- }
2141
-
2142
- function isExecutable(candidate) {
2143
- try {
2144
- return (fs.statSync(candidate).mode & 0o111) !== 0;
2145
- } catch {
2146
- return false;
2147
- }
2148
- }
2149
-
2150
- function nativeGuardContract(target, project) {
2151
- if (target === "claude") {
2152
- return {
2153
- settings: path.join(project, ".claude", "settings.json"),
2154
- event: "PreToolUse",
2155
- entry: claudeGuardEntry,
2156
- };
2157
- }
2158
- if (target === "codex") {
2159
- return {
2160
- settings: path.join(project, ".codex", "hooks.json"),
2161
- event: "PreToolUse",
2162
- entry: codexGuardEntry,
2163
- };
2164
- }
2165
- if (target === "cursor") {
2166
- return {
2167
- settings: path.join(project, ".cursor", "hooks.json"),
2168
- event: "preToolUse",
2169
- entry: cursorGuardEntry,
2170
- };
2171
- }
2172
- return null;
2173
- }
2174
-
2175
1121
  // Read-only health check: host installs, project contracts, guard hooks,
2176
1122
  // and pipeline state. Never writes.
2177
1123
  export function doctor(sourceRoot, { targets, rootFor, project = null }) {
1124
+ if (project !== null) {
1125
+ return hooks.projectAdapter(sourceRoot, project, "doctor", {
1126
+ targets, roots: Object.fromEntries(targets.map(target => [target, rootFor(target)])),
1127
+ });
1128
+ }
2178
1129
  const findings = [];
2179
1130
  const push = (level, text) => findings.push({ level, text });
2180
- function readProjectFile(candidate, label) {
2181
- try {
2182
- return fs.readFileSync(candidate);
2183
- } catch (error) {
2184
- push("fail", `${label} cannot be read: ${error.message}`);
2185
- return null;
2186
- }
2187
- }
2188
1131
  const version = readPackageVersion(path.join(sourceRoot, "package.json"));
2189
1132
  if (version === null) push("fail", "package: version cannot be read");
2190
1133
 
@@ -2230,314 +1173,6 @@ export function doctor(sourceRoot, { targets, rootFor, project = null }) {
2230
1173
  }
2231
1174
  }
2232
1175
 
2233
- if (project === null) return findings;
2234
-
2235
- for (const [name, heading] of PROJECT_CONTRACTS) {
2236
- const contract = path.join(project, name);
2237
- if (isSymlink(contract)) {
2238
- push("fail", `project: contract ${name} is a symlink`);
2239
- continue;
2240
- }
2241
- if (!isFile(contract)) {
2242
- push("fail", `project: missing contract ${name} — run --project "${project}"`);
2243
- continue;
2244
- }
2245
- const content = readProjectFile(contract, `project: ${name}`);
2246
- if (content === null) continue;
2247
- const canonical = readProjectFile(
2248
- path.join(sourceRoot, name),
2249
- `package: ${name}`
2250
- );
2251
- if (canonical === null) continue;
2252
- const installedSection = contractSection(content.toString("utf8"), heading);
2253
- const canonicalSection = contractSection(canonical.toString("utf8"), heading);
2254
- if (installedSection !== null && installedSection === canonicalSection) {
2255
- push("ok", `project: ${name} present`);
2256
- } else {
2257
- push("fail", `project: ${name} lacks plain-prompt re-entry — merge the current contract`);
2258
- }
2259
- }
2260
- const bridge = path.join(project, ".claude", "CLAUDE.md");
2261
- if (!isFile(bridge)) {
2262
- push("note", "project: no .claude/CLAUDE.md bridge (only written for --claude installs)");
2263
- } else {
2264
- const bridgeContent = readProjectFile(bridge, "project: .claude/CLAUDE.md");
2265
- if (bridgeContent !== null) {
2266
- if (bridgeContent.toString("utf8") === CLAUDE_BRIDGE) {
2267
- push("ok", "project: .claude/CLAUDE.md bridge present");
2268
- } else {
2269
- push("note", "project: .claude/CLAUDE.md exists but is not the managed bridge");
2270
- }
2271
- }
2272
- }
2273
-
2274
- const runtime = path.join(project, HOOKS_DIRECTORY, "runtime");
2275
- const launcher = path.join(project, HOOKS_DIRECTORY, PROJECT_STATUS_LAUNCHER);
2276
- let runtimeCurrent = true;
2277
- if (isSymlink(launcher)) {
2278
- push("fail", "project: status launcher is a symlink");
2279
- runtimeCurrent = false;
2280
- } else if (!isFile(launcher)) {
2281
- push("fail", "project: missing status launcher");
2282
- runtimeCurrent = false;
2283
- } else {
2284
- const launcherContent = readProjectFile(launcher, "project: status launcher");
2285
- const launcherSource = readProjectFile(
2286
- path.join(sourceRoot, "scripts", PROJECT_STATUS_LAUNCHER),
2287
- "package: status launcher"
2288
- );
2289
- if (launcherContent === null || launcherSource === null) {
2290
- runtimeCurrent = false;
2291
- } else {
2292
- if (!launcherContent.toString("utf8").includes(PROJECT_STATUS_MARKER)) {
2293
- push("warn", "project: status launcher is not managed");
2294
- runtimeCurrent = false;
2295
- } else if (!launcherContent.equals(launcherSource)) {
2296
- push(
2297
- "warn",
2298
- "project: status launcher is stale — refresh it with the project contracts"
2299
- );
2300
- runtimeCurrent = false;
2301
- } else {
2302
- push("ok", "project: status launcher current");
2303
- }
2304
- }
2305
- }
2306
- let runtimeSafe = true;
2307
- try {
2308
- validateDirectoryDestination(
2309
- path.join(project, HOOKS_DIRECTORY),
2310
- "project runtime parent directory"
2311
- );
2312
- validateDirectoryDestination(runtime, "project runtime directory");
2313
- } catch (error) {
2314
- if (!(error instanceof InstallerError)) throw error;
2315
- push("fail", `project: unsafe runtime — ${error.message}`);
2316
- runtimeSafe = false;
2317
- runtimeCurrent = false;
2318
- }
2319
- if (runtimeSafe) {
2320
- for (const name of PROJECT_RUNTIME_SCRIPTS) {
2321
- const destination = path.join(runtime, name);
2322
- if (isSymlink(destination)) {
2323
- push("fail", `project: runtime ${name} is a symlink`);
2324
- runtimeCurrent = false;
2325
- continue;
2326
- }
2327
- if (!isFile(destination)) {
2328
- push("fail", `project: missing runtime ${name}`);
2329
- runtimeCurrent = false;
2330
- continue;
2331
- }
2332
- const content = readProjectFile(destination, `project: runtime ${name}`);
2333
- if (content === null) {
2334
- runtimeCurrent = false;
2335
- continue;
2336
- }
2337
- const source = readProjectFile(
2338
- path.join(sourceRoot, "scripts", name),
2339
- `package: runtime ${name}`
2340
- );
2341
- if (source === null) {
2342
- runtimeCurrent = false;
2343
- continue;
2344
- }
2345
- if (!content.toString("utf8").includes(PROJECT_RUNTIME_MARKER)) {
2346
- push("warn", `project: runtime ${name} is not managed`);
2347
- runtimeCurrent = false;
2348
- } else if (!content.equals(source)) {
2349
- push("warn", `project: runtime ${name} is stale — refresh it with the project contracts`);
2350
- runtimeCurrent = false;
2351
- } else {
2352
- push("ok", `project: runtime ${name} current`);
2353
- }
2354
- }
2355
- }
2356
-
2357
- let guardInstalled = GUARD_SCRIPTS.some((name) =>
2358
- lexists(path.join(project, HOOKS_DIRECTORY, name))
2359
- );
2360
- const nativeWiredTargets = new Set();
2361
- for (const target of targets) {
2362
- const contract = nativeGuardContract(target, project);
2363
- if (
2364
- contract !== null &&
2365
- (isSymlink(contract.settings) || isManagedHookSettings(contract.settings))
2366
- ) {
2367
- nativeWiredTargets.add(target);
2368
- }
2369
- }
2370
- let guardWired = nativeWiredTargets.size > 0;
2371
- const unreadableGitHooks = new Map();
2372
- if (lexists(path.join(project, ".git"))) {
2373
- const hooksDir = gitHooksDirectory(project);
2374
- if (hooksDir !== null) {
2375
- for (const name of GIT_HOOK_NAMES) {
2376
- const hookPath = path.join(hooksDir, name);
2377
- if (!lexists(hookPath)) continue;
2378
- if (isSymlink(hookPath)) {
2379
- guardWired = true;
2380
- continue;
2381
- }
2382
- try {
2383
- const managed = isManagedGitHookContent(fs.readFileSync(hookPath, "utf8"));
2384
- guardWired = managed || guardWired;
2385
- } catch (error) {
2386
- unreadableGitHooks.set(hookPath, error.message);
2387
- }
2388
- }
2389
- }
2390
- }
2391
- for (const [hookPath, error] of unreadableGitHooks) {
2392
- const label = describeProjectPath(project, hookPath);
2393
- push("fail", `hooks: ${label} cannot be read: ${error}`);
2394
- }
2395
- guardInstalled = guardInstalled || guardWired;
2396
- if (!guardInstalled) {
2397
- push("note", "hooks: guard hooks not installed (opt in with --hooks; see HOOKS.md)");
2398
- } else {
2399
- for (const name of GUARD_SCRIPTS) {
2400
- const destination = path.join(project, HOOKS_DIRECTORY, name);
2401
- if (isSymlink(destination)) {
2402
- push("fail", `hooks: ${HOOKS_DIRECTORY}/${name} is a symlink`);
2403
- continue;
2404
- }
2405
- if (!isFile(destination)) {
2406
- push("fail", `hooks: missing ${HOOKS_DIRECTORY}/${name} — run --hooks-refresh`);
2407
- continue;
2408
- }
2409
- const content = readProjectFile(destination, `hooks: ${HOOKS_DIRECTORY}/${name}`);
2410
- if (content === null) continue;
2411
- const source = readProjectFile(
2412
- path.join(sourceRoot, "scripts", name),
2413
- `package: guard ${name}`
2414
- );
2415
- if (source === null) continue;
2416
- if (!content.toString("utf8").includes(GUARD_MARKER)) {
2417
- push("warn", `hooks: ${HOOKS_DIRECTORY}/${name} is not a managed guard script`);
2418
- } else if (!content.equals(source)) {
2419
- push("warn", `hooks: ${HOOKS_DIRECTORY}/${name} is stale — run --hooks-refresh`);
2420
- } else {
2421
- push("ok", `hooks: ${HOOKS_DIRECTORY}/${name} current`);
2422
- }
2423
- }
2424
- for (const target of targets) {
2425
- if (!installedTargets.has(target) && !nativeWiredTargets.has(target)) continue;
2426
- const contract = nativeGuardContract(target, project);
2427
- if (contract === null) {
2428
- if (MANIFEST.hosts[target]?.guard_tier !== "git-only") {
2429
- push("fail", `hooks: ${target} declares a native guard without a health contract`);
2430
- }
2431
- continue;
2432
- }
2433
- if (isSymlink(contract.settings)) {
2434
- push("fail", `hooks: ${target} native guard wiring is a symlink`);
2435
- continue;
2436
- }
2437
- if (!isFile(contract.settings)) {
2438
- push("fail", `hooks: ${target} native guard wiring is missing — run --hooks-refresh-full`);
2439
- continue;
2440
- }
2441
- let current = false;
2442
- try {
2443
- const parsed = JSON.parse(fs.readFileSync(contract.settings, "utf8"));
2444
- const entries =
2445
- parsed && typeof parsed === "object" && parsed.hooks
2446
- ? parsed.hooks[contract.event]
2447
- : null;
2448
- const variants = INTERPRETER_CANDIDATES.map((candidate) =>
2449
- JSON.stringify(contract.entry(candidate))
2450
- );
2451
- current =
2452
- Array.isArray(entries) &&
2453
- entries.some((entry) => variants.includes(JSON.stringify(entry)));
2454
- } catch {
2455
- current = false;
2456
- }
2457
- if (current) {
2458
- push("ok", `hooks: ${target} native guard wiring present`);
2459
- } else {
2460
- push("fail", `hooks: ${target} native guard wiring is stale — run --hooks-refresh-full`);
2461
- }
2462
- }
2463
- const dotGit = path.join(project, ".git");
2464
- if (lexists(dotGit)) {
2465
- const hooksDir = gitHooksDirectory(project);
2466
- if (hooksDir === null) {
2467
- push(
2468
- "fail",
2469
- "hooks: cannot resolve the git hooks directory (is git runnable?); git hooks unverified"
2470
- );
2471
- } else {
2472
- const custom = !samePath(hooksDir, path.join(dotGit, "hooks"));
2473
- let missingFromCustom = false;
2474
- for (const [hookName, generator] of GIT_HOOKS) {
2475
- const hookPath = path.join(hooksDir, hookName);
2476
- const label = describeProjectPath(project, hookPath);
2477
- if (unreadableGitHooks.has(hookPath)) {
2478
- continue;
2479
- }
2480
- if (!lexists(hookPath)) {
2481
- push(
2482
- "fail",
2483
- `hooks: ${hookName} is missing from the effective git hooks directory ` +
2484
- `${hooksDir} — run --hooks-refresh-full`
2485
- );
2486
- if (custom) missingFromCustom = true;
2487
- } else if (isSymlink(hookPath)) {
2488
- push("fail", `hooks: ${label} is a symlink`);
2489
- } else {
2490
- const hookContent = readProjectFile(hookPath, `hooks: ${label}`);
2491
- if (hookContent === null) continue;
2492
- const hookText = hookContent.toString("utf8");
2493
- if (!isManagedGitHookContent(hookText)) {
2494
- push("warn", `hooks: ${label} is not a managed GSD Path git hook`);
2495
- } else if (
2496
- !INTERPRETER_CANDIDATES.some(
2497
- (candidate) => hookText === generator(candidate)
2498
- )
2499
- ) {
2500
- push("warn", `hooks: ${label} is stale — run --hooks-refresh-full`);
2501
- } else if (!isExecutable(hookPath)) {
2502
- push("warn", `hooks: ${label} is not executable — run --hooks-refresh-full`);
2503
- } else {
2504
- push("ok", `hooks: ${label} wired`);
2505
- }
2506
- }
2507
- }
2508
- if (missingFromCustom) {
2509
- push(
2510
- "warn",
2511
- `hooks: core.hooksPath points this repository at ${hooksDir}, ` +
2512
- "but the guard hooks are not wired there"
2513
- );
2514
- }
2515
- }
2516
- }
2517
- }
2518
-
2519
- const stateFile = path.join(project, ".project", "STATE.md");
2520
- if (!isDirectory(path.join(project, ".project"))) {
2521
- push("note", "state: no .project/ pipeline state (nothing started yet)");
2522
- } else if (!isFile(stateFile)) {
2523
- push("warn", "state: .project/ exists but STATE.md is missing");
2524
- } else {
2525
- try {
2526
- const state = hooks.validatedProjectState(sourceRoot, project);
2527
- if (!runtimeCurrent) {
2528
- throw new InstallerError(
2529
- "project runtime status was not executed because the installed runtime is not current"
2530
- );
2531
- }
2532
- validateProjectRuntimeStatus(sourceRoot, project);
2533
- if (!projectRuntimeMatches(sourceRoot, project)) {
2534
- throw new InstallerError("project runtime changed during doctor validation");
2535
- }
2536
- push("ok", `state: ${state.phase}/${state.status}`);
2537
- } catch (error) {
2538
- push("fail", `state: ${messageOf(error)}`);
2539
- }
2540
- }
2541
1176
  return findings;
2542
1177
  }
2543
1178
 
@@ -2686,16 +1321,26 @@ export const hooks = {
2686
1321
  rename: fs.renameSync.bind(fs),
2687
1322
  renameInstallLock: fs.renameSync.bind(fs),
2688
1323
  renameInstallStage: fs.renameSync.bind(fs),
2689
- validatedProjectState,
2690
1324
  processIdentity,
2691
1325
  reserveDirectory,
2692
1326
  reserveFile,
2693
1327
  detectPythonInterpreter,
2694
1328
  resolveGitHooksPath,
2695
- copyRuntimeFile: fs.copyFileSync.bind(fs),
2696
- copyGuardFile: copyFileAtomic,
1329
+ projectAdapter,
2697
1330
  };
2698
1331
 
1332
+ // Project installation already requires Python; keep runtime lifecycle ownership there.
1333
+ function projectAdapter(source, project, action, payload = {}, env = process.env) {
1334
+ const interpreter = requiredPythonRuntime("project runtime");
1335
+ const result = spawnSync(interpreter, ["-B", path.join(SCRIPT_DIRECTORY, "install.py"), "--adapter-request"], {
1336
+ input: JSON.stringify({ source, project, action, ...payload }), encoding: "utf8", env: { ...process.env, ...env },
1337
+ });
1338
+ if (result.error || result.status !== 0) {
1339
+ throw new InstallerError(result.stderr?.trim() || result.error?.message || "project runtime operation failed");
1340
+ }
1341
+ return JSON.parse(result.stdout);
1342
+ }
1343
+
2699
1344
  export async function install(sourceRoot, plans, options = {}) {
2700
1345
  const {
2701
1346
  project = null,
@@ -2709,14 +1354,13 @@ export async function install(sourceRoot, plans, options = {}) {
2709
1354
  if (hooksEnabled && project === null) {
2710
1355
  throw new InstallerError("--hooks requires --project");
2711
1356
  }
2712
- const selected = plans.map((plan) => plan.name);
2713
- let interpreter = "python3";
2714
- let hooksDir = null;
2715
- if (project !== null && !hooksEnabled && !dryRun) {
2716
- interpreter = requiredPythonRuntime("--project", selected);
2717
- } else if (hooksEnabled) {
2718
- ({ interpreter, hooksDir } = requiredHookRuntime(project, "--hooks", selected));
1357
+ if (project !== null) {
1358
+ return hooks.projectAdapter(sourceRoot, project, "install", {
1359
+ plans: plans.map(({ name, root }) => ({ name, root })), dryRun,
1360
+ hooks: hooksEnabled, migrateLegacy, update,
1361
+ }, env);
2719
1362
  }
1363
+ const selected = plans.map((plan) => plan.name);
2720
1364
  const progress = async (text) => {
2721
1365
  if (onProgress) onProgress(text);
2722
1366
  await tick();
@@ -2791,21 +1435,6 @@ export async function install(sourceRoot, plans, options = {}) {
2791
1435
  }
2792
1436
  }
2793
1437
 
2794
- if (project !== null) {
2795
- validateDirectoryDestination(project, "project path");
2796
- if (dryRun) {
2797
- validateProject(
2798
- sourceRoot,
2799
- project,
2800
- selected,
2801
- hooksEnabled,
2802
- [...mutationRoots, ...plannedBackups],
2803
- interpreter,
2804
- hooksDir,
2805
- update
2806
- );
2807
- }
2808
- }
2809
1438
 
2810
1439
  const results = [];
2811
1440
  const staging = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-path-install-"));
@@ -2832,32 +1461,15 @@ export async function install(sourceRoot, plans, options = {}) {
2832
1461
  const suffix = count ? `; would back up ${count} entries` : "";
2833
1462
  results.push(installResult(plan, true, update) + suffix);
2834
1463
  }
2835
- if (project !== null) {
2836
- results.push(projectResult(project, selected, hooksEnabled, interpreter, hooksDir, update, true));
2837
- }
2838
1464
  appendHostNotes(results, selected);
2839
1465
  return results;
2840
1466
  }
2841
1467
 
2842
1468
  const lockRoots = deployments.map((plan) => plan.root);
2843
1469
  if (legacyRoot !== null && isDirectory(legacyRoot)) lockRoots.push(legacyRoot);
2844
- if (project !== null) lockRoots.push(path.join(project, HOOKS_DIRECTORY));
2845
1470
  const ownership = acquireInstallLocks(lockRoots);
2846
1471
  const targetTransactions = [];
2847
- const projectTransaction = { createdDirectories: [], copied: [], replaced: [] };
2848
1472
  try {
2849
- if (project !== null) {
2850
- validateProject(
2851
- sourceRoot,
2852
- project,
2853
- selected,
2854
- hooksEnabled,
2855
- [...mutationRoots, ...plannedBackups],
2856
- interpreter,
2857
- hooksDir,
2858
- update
2859
- );
2860
- }
2861
1473
  if (legacyRoot !== null && isDirectory(legacyRoot)) {
2862
1474
  await progress("Backing up legacy Codex skills");
2863
1475
  const legacyTransaction = {
@@ -2895,28 +1507,8 @@ export async function install(sourceRoot, plans, options = {}) {
2895
1507
  );
2896
1508
  }
2897
1509
  }
2898
- if (project !== null) {
2899
- await progress("Writing project contracts");
2900
- const resultLine = projectResult(project, selected, hooksEnabled, interpreter, hooksDir, update, false);
2901
- applyProject(
2902
- sourceRoot,
2903
- project,
2904
- selected,
2905
- hooksEnabled,
2906
- projectTransaction,
2907
- interpreter,
2908
- hooksDir,
2909
- update
2910
- );
2911
- results.push(resultLine);
2912
- }
2913
1510
  } catch (error) {
2914
1511
  const rollbackErrors = [];
2915
- try {
2916
- rollbackProject(projectTransaction);
2917
- } catch (rollbackError) {
2918
- rollbackErrors.push(messageOf(rollbackError));
2919
- }
2920
1512
  for (const transaction of [...targetTransactions].reverse()) {
2921
1513
  try {
2922
1514
  rollbackTarget(transaction);
@@ -2970,6 +1562,9 @@ export function parseCli(argv) {
2970
1562
  doctor: { type: "boolean", default: false },
2971
1563
  hooks: { type: "boolean", default: false },
2972
1564
  "hooks-init": { type: "boolean", default: false },
1565
+ "runtime-restore": { type: "boolean", default: false },
1566
+ "runtime-upgrade": { type: "boolean", default: false },
1567
+ "runtime-migrate": { type: "boolean", default: false },
2973
1568
  "hooks-refresh": { type: "boolean", default: false },
2974
1569
  "hooks-refresh-full": { type: "boolean", default: false },
2975
1570
  "source-root": { type: "string" },
@@ -3052,13 +1647,16 @@ function usage() {
3052
1647
  " Update skills: gsd-path --update (or npx @opengsd/gsd-path@latest --update)\n\n" +
3053
1648
  `targets: ${flags}\n` +
3054
1649
  " --update refresh existing installs in place; with --project also\n" +
3055
- " refreshes .gsd-path/ and keeps AGENTS.md/WORKFLOW.md\n" +
1650
+ " keeps its selected runtime and project contracts\n" +
3056
1651
  " --local install into this project's per-host skill dirs\n" +
3057
1652
  " --project PATH write project contracts and status runtime; requires Python 3.9+\n" +
3058
1653
  " --doctor read-only health check of installs, hooks, and state\n" +
3059
1654
  " --hooks with --project: install guard hooks (see HOOKS.md)\n" +
3060
1655
  " --hooks-init add guards to an existing project without changing its contracts\n" +
3061
- " --hooks-refresh with --project: overwrite managed .gsd-path scripts\n" +
1656
+ " --runtime-restore restore the exact declared runtime from --source-root\n" +
1657
+ " --runtime-upgrade explicitly select the supplied package runtime\n" +
1658
+ " --runtime-migrate migrate a tracked legacy runtime for review\n" +
1659
+ " --hooks-refresh validate the selected runtime; keep its version\n" +
3062
1660
  " --hooks-refresh-full refresh native settings/git hooks; target flags create missing configs\n" +
3063
1661
  " --dry-run preview without writing\n" +
3064
1662
  " each target also accepts --<target>-root PATH to override its skills root"
@@ -3097,6 +1695,12 @@ export async function main(argv, env = process.env) {
3097
1695
  : path.resolve(SCRIPT_DIRECTORY, "..");
3098
1696
  const project =
3099
1697
  values.project !== undefined ? absolutePath(values.project) : null;
1698
+ if (["runtime-restore", "runtime-upgrade", "runtime-migrate"].some(name => values[name])) {
1699
+ const interpreter = requiredPythonRuntime("project runtime");
1700
+ const result = spawnSync(interpreter, ["-B", path.join(SCRIPT_DIRECTORY, "install.py"),
1701
+ ...argv.filter(arg => arg !== "--no-color")], { stdio: "inherit", env });
1702
+ return result.status ?? 1;
1703
+ }
3100
1704
  const local = values.local;
3101
1705
  const rootFor = (target) => {
3102
1706
  const override = values[`${target}-root`];