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