@opengsd/gsd-core 1.17.0 → 1.19.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 (369) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.opencode/plugins/gsd-core.js +2 -3
  4. package/agents/gsd-advisor-researcher.compact.md +1 -1
  5. package/agents/gsd-advisor-researcher.md +1 -1
  6. package/agents/gsd-assumptions-analyzer.compact.md +1 -1
  7. package/agents/gsd-assumptions-analyzer.md +1 -1
  8. package/agents/gsd-code-fixer.compact.md +3 -3
  9. package/agents/gsd-code-fixer.md +5 -4
  10. package/agents/gsd-code-reviewer.compact.md +1 -1
  11. package/agents/gsd-code-reviewer.md +1 -1
  12. package/agents/gsd-codebase-mapper.compact.md +1 -1
  13. package/agents/gsd-codebase-mapper.md +1 -1
  14. package/agents/gsd-debugger.md +1 -1
  15. package/agents/gsd-doc-verifier.compact.md +4 -4
  16. package/agents/gsd-doc-verifier.md +4 -3
  17. package/agents/gsd-doc-writer.compact.md +23 -5
  18. package/agents/gsd-doc-writer.md +19 -6
  19. package/agents/gsd-eval-auditor.compact.md +1 -1
  20. package/agents/gsd-eval-auditor.md +1 -1
  21. package/agents/gsd-executor.md +7 -3
  22. package/agents/gsd-integration-checker.compact.md +1 -1
  23. package/agents/gsd-integration-checker.md +1 -1
  24. package/agents/gsd-nyquist-auditor.compact.md +1 -1
  25. package/agents/gsd-nyquist-auditor.md +1 -1
  26. package/agents/gsd-phase-researcher.md +1 -1
  27. package/agents/gsd-plan-checker.md +2 -1
  28. package/agents/gsd-planner.md +5 -4
  29. package/agents/gsd-project-researcher.compact.md +1 -1
  30. package/agents/gsd-project-researcher.md +1 -1
  31. package/agents/gsd-research-synthesizer.compact.md +5 -5
  32. package/agents/gsd-research-synthesizer.md +5 -5
  33. package/agents/gsd-roadmapper.compact.md +1 -1
  34. package/agents/gsd-roadmapper.md +1 -1
  35. package/agents/gsd-security-auditor.compact.md +1 -1
  36. package/agents/gsd-security-auditor.md +1 -1
  37. package/agents/gsd-ui-auditor.compact.md +1 -1
  38. package/agents/gsd-ui-auditor.md +1 -1
  39. package/agents/gsd-ui-checker.compact.md +1 -1
  40. package/agents/gsd-ui-checker.md +1 -1
  41. package/agents/gsd-ui-researcher.compact.md +13 -1
  42. package/agents/gsd-ui-researcher.md +28 -1
  43. package/agents/gsd-verifier.md +2 -2
  44. package/bin/install.js +460 -105
  45. package/commands/gsd/code-review.md +8 -0
  46. package/commands/gsd/pause-work.md +1 -1
  47. package/gsd-core/bin/gsd-tools.cjs +124 -18
  48. package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
  49. package/gsd-core/bin/lib/args-terminator.cjs +114 -0
  50. package/gsd-core/bin/lib/broken-windows.cjs +59 -1
  51. package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
  52. package/gsd-core/bin/lib/capability-lock.cjs +41 -8
  53. package/gsd-core/bin/lib/capability-registry.cjs +162 -74
  54. package/gsd-core/bin/lib/capability-source.cjs +3 -1
  55. package/gsd-core/bin/lib/capability-state.cjs +20 -11
  56. package/gsd-core/bin/lib/capability-validator.cjs +23 -4
  57. package/gsd-core/bin/lib/commands.cjs +5 -1
  58. package/gsd-core/bin/lib/config-loader.cjs +13 -3
  59. package/gsd-core/bin/lib/config.cjs +256 -58
  60. package/gsd-core/bin/lib/core-utils.cjs +34 -0
  61. package/gsd-core/bin/lib/decision-coverage-support.cjs +93 -0
  62. package/gsd-core/bin/lib/gate-context-drift.cjs +8 -3
  63. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +21 -1
  64. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +19 -1
  65. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
  66. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
  67. package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
  68. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +114 -18
  69. package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
  70. package/gsd-core/bin/lib/git-info-exclude.cjs +57 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +18 -3
  72. package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
  73. package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
  74. package/gsd-core/bin/lib/host-integration.cjs +13 -6
  75. package/gsd-core/bin/lib/init.cjs +76 -5
  76. package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
  77. package/gsd-core/bin/lib/install-engine.cjs +246 -32
  78. package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
  79. package/gsd-core/bin/lib/install-profiles.cjs +14 -7
  80. package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
  81. package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
  82. package/gsd-core/bin/lib/markdown-sectionizer.cjs +113 -0
  83. package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
  84. package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
  85. package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
  86. package/gsd-core/bin/lib/milestone.cjs +43 -1
  87. package/gsd-core/bin/lib/model-catalog.cjs +8 -0
  88. package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
  89. package/gsd-core/bin/lib/phase-id.cjs +21 -4
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +37 -4
  91. package/gsd-core/bin/lib/phase.cjs +235 -37
  92. package/gsd-core/bin/lib/planning-snapshot.cjs +3 -1
  93. package/gsd-core/bin/lib/planning-workspace.cjs +18 -0
  94. package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
  95. package/gsd-core/bin/lib/read-only-git-env.cjs +58 -0
  96. package/gsd-core/bin/lib/report-parser.cjs +57 -1
  97. package/gsd-core/bin/lib/review-lane-descriptor.cjs +35 -2
  98. package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
  99. package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
  100. package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
  101. package/gsd-core/bin/lib/roadmap-parser.cjs +12 -0
  102. package/gsd-core/bin/lib/roadmap-upgrade.cjs +39 -2
  103. package/gsd-core/bin/lib/roadmap.cjs +89 -30
  104. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +428 -16
  105. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
  106. package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
  107. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
  108. package/gsd-core/bin/lib/shell-command-projection.cjs +123 -16
  109. package/gsd-core/bin/lib/smart-entry.cjs +37 -16
  110. package/gsd-core/bin/lib/state-contract.cjs +29 -9
  111. package/gsd-core/bin/lib/state-document.cjs +47 -5
  112. package/gsd-core/bin/lib/state-transition.cjs +150 -34
  113. package/gsd-core/bin/lib/state.cjs +140 -88
  114. package/gsd-core/bin/lib/surface.cjs +9 -0
  115. package/gsd-core/bin/lib/task-command-router.cjs +7 -28
  116. package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
  117. package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
  118. package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
  119. package/gsd-core/bin/lib/verification.cjs +96 -9
  120. package/gsd-core/bin/lib/verify.cjs +38 -1
  121. package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
  122. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  123. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  124. package/gsd-core/references/agent-contracts.md +1 -1
  125. package/gsd-core/references/agent-skills-bootstrap.md +26 -7
  126. package/gsd-core/references/git-integration.md +14 -3
  127. package/gsd-core/references/loop-hook-dispatch.md +13 -6
  128. package/gsd-core/references/planner-reversibility.md +18 -4
  129. package/gsd-core/references/planner-verify-command-grounding.md +18 -0
  130. package/gsd-core/references/planning-config.md +2 -0
  131. package/gsd-core/references/tdd.md +5 -4
  132. package/gsd-core/references/verifier-phase-gates.md +8 -1
  133. package/gsd-core/templates/DEBUG.md +16 -0
  134. package/gsd-core/templates/config.json +2 -1
  135. package/gsd-core/templates/verification-report.md +1 -1
  136. package/gsd-core/workflows/add-backlog.md +10 -2
  137. package/gsd-core/workflows/add-phase.md +5 -1
  138. package/gsd-core/workflows/add-tests.md +5 -1
  139. package/gsd-core/workflows/add-todo.md +5 -1
  140. package/gsd-core/workflows/ai-integration-phase.md +5 -1
  141. package/gsd-core/workflows/audit-fix.md +2 -1
  142. package/gsd-core/workflows/audit-milestone.md +21 -9
  143. package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
  144. package/gsd-core/workflows/autonomous.md +52 -64
  145. package/gsd-core/workflows/check-todos.md +5 -1
  146. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
  147. package/gsd-core/workflows/code-review-fix.md +5 -1
  148. package/gsd-core/workflows/code-review.md +66 -8
  149. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
  150. package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
  151. package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
  152. package/gsd-core/workflows/complete-milestone.md +64 -157
  153. package/gsd-core/workflows/debug.md +5 -1
  154. package/gsd-core/workflows/diagnose-issues.md +34 -6
  155. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
  156. package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
  157. package/gsd-core/workflows/discuss-phase-assumptions.md +44 -5
  158. package/gsd-core/workflows/discuss-phase-power.md +19 -2
  159. package/gsd-core/workflows/discuss-phase.md +11 -11
  160. package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
  161. package/gsd-core/workflows/docs-update.md +27 -5
  162. package/gsd-core/workflows/edit-phase.md +5 -1
  163. package/gsd-core/workflows/eval-review.md +11 -3
  164. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
  165. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
  166. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +19 -8
  167. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +21 -3
  168. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
  169. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +7 -3
  170. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
  171. package/gsd-core/workflows/execute-phase.md +46 -11
  172. package/gsd-core/workflows/execute-plan.md +8 -4
  173. package/gsd-core/workflows/extract-learnings.md +6 -2
  174. package/gsd-core/workflows/forensics.md +5 -1
  175. package/gsd-core/workflows/health.md +2 -1
  176. package/gsd-core/workflows/import.md +6 -2
  177. package/gsd-core/workflows/insert-phase.md +5 -1
  178. package/gsd-core/workflows/manager.md +10 -2
  179. package/gsd-core/workflows/map-codebase.md +5 -1
  180. package/gsd-core/workflows/milestone-summary.md +10 -2
  181. package/gsd-core/workflows/new-milestone.md +13 -5
  182. package/gsd-core/workflows/new-project.md +9 -5
  183. package/gsd-core/workflows/next.md +8 -4
  184. package/gsd-core/workflows/pause-work.md +3 -3
  185. package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
  186. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +7 -3
  187. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
  188. package/gsd-core/workflows/plan-phase.md +54 -86
  189. package/gsd-core/workflows/plan-review-convergence.md +17 -5
  190. package/gsd-core/workflows/plant-seed.md +8 -4
  191. package/gsd-core/workflows/progress.md +6 -2
  192. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
  193. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
  194. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
  195. package/gsd-core/workflows/quick-batch.md +5 -1
  196. package/gsd-core/workflows/quick.md +23 -8
  197. package/gsd-core/workflows/remove-phase.md +5 -1
  198. package/gsd-core/workflows/resume-project.md +5 -1
  199. package/gsd-core/workflows/review.md +22 -15
  200. package/gsd-core/workflows/scan.md +5 -1
  201. package/gsd-core/workflows/section-manifest.json +5 -0
  202. package/gsd-core/workflows/secure-phase.md +6 -2
  203. package/gsd-core/workflows/settings.md +4 -3
  204. package/gsd-core/workflows/ship.md +16 -14
  205. package/gsd-core/workflows/spec-phase.md +3 -6
  206. package/gsd-core/workflows/thread.md +10 -2
  207. package/gsd-core/workflows/transition.md +5 -1
  208. package/gsd-core/workflows/ui-phase.md +5 -1
  209. package/gsd-core/workflows/ui-review.md +6 -2
  210. package/gsd-core/workflows/ultraplan-phase.md +5 -1
  211. package/gsd-core/workflows/update.md +5 -1
  212. package/gsd-core/workflows/validate-phase.md +6 -2
  213. package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
  214. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
  215. package/gsd-core/workflows/verify-work.md +15 -6
  216. package/hooks/dist/gsd-agent-isolation-guard.js +14 -2
  217. package/hooks/dist/gsd-cursor-subagent-start.js +8 -3
  218. package/hooks/dist/gsd-path-export.js +95 -0
  219. package/hooks/dist/gsd-secret-read-guard.js +244 -53
  220. package/hooks/dist/gsd-statusline.js +35 -2
  221. package/hooks/dist/gsd-validate-commit.sh +27 -5
  222. package/hooks/dist/gsd-windsurf-pre-write.js +28 -1
  223. package/hooks/dist/gsd-workflow-guard.js +1 -1
  224. package/hooks/dist/gsd-worktree-path-guard.js +28 -1
  225. package/hooks/dist/lib/isolation-sentinel.js +44 -0
  226. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  227. package/hooks/gsd-agent-isolation-guard.js +14 -2
  228. package/hooks/gsd-cursor-subagent-start.js +8 -3
  229. package/hooks/gsd-path-export.js +95 -0
  230. package/hooks/gsd-secret-read-guard.js +244 -53
  231. package/hooks/gsd-statusline.js +35 -2
  232. package/hooks/gsd-validate-commit.sh +27 -5
  233. package/hooks/gsd-windsurf-pre-write.js +28 -1
  234. package/hooks/gsd-workflow-guard.js +1 -1
  235. package/hooks/gsd-worktree-path-guard.js +28 -1
  236. package/hooks/hooks.json +1 -0
  237. package/hooks/lib/isolation-sentinel.js +44 -0
  238. package/hooks/managed-hooks-registry.cjs +1 -0
  239. package/package.json +13 -6
  240. package/pi/gsd.cjs +1047 -10
  241. package/pi/package-agents/gsd-advisor-researcher.md +113 -0
  242. package/pi/package-agents/gsd-ai-researcher.md +111 -0
  243. package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
  244. package/pi/package-agents/gsd-code-fixer.md +769 -0
  245. package/pi/package-agents/gsd-code-reviewer.md +402 -0
  246. package/pi/package-agents/gsd-codebase-mapper.md +850 -0
  247. package/pi/package-agents/gsd-debug-session-manager.md +416 -0
  248. package/pi/package-agents/gsd-debugger.md +1274 -0
  249. package/pi/package-agents/gsd-doc-classifier.md +269 -0
  250. package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
  251. package/pi/package-agents/gsd-doc-verifier.md +212 -0
  252. package/pi/package-agents/gsd-doc-writer.md +626 -0
  253. package/pi/package-agents/gsd-dom-verifier.md +164 -0
  254. package/pi/package-agents/gsd-domain-researcher.md +144 -0
  255. package/pi/package-agents/gsd-eval-auditor.md +185 -0
  256. package/pi/package-agents/gsd-eval-planner.md +149 -0
  257. package/pi/package-agents/gsd-executor.md +894 -0
  258. package/pi/package-agents/gsd-framework-selector.md +159 -0
  259. package/pi/package-agents/gsd-integration-checker.md +476 -0
  260. package/pi/package-agents/gsd-intel-updater.md +338 -0
  261. package/pi/package-agents/gsd-mempalace-curator.md +49 -0
  262. package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
  263. package/pi/package-agents/gsd-pattern-mapper.md +340 -0
  264. package/pi/package-agents/gsd-phase-researcher.md +897 -0
  265. package/pi/package-agents/gsd-plan-checker.md +1077 -0
  266. package/pi/package-agents/gsd-planner.md +1003 -0
  267. package/pi/package-agents/gsd-project-researcher.md +611 -0
  268. package/pi/package-agents/gsd-research-synthesizer.md +259 -0
  269. package/pi/package-agents/gsd-roadmapper.md +779 -0
  270. package/pi/package-agents/gsd-security-auditor.md +171 -0
  271. package/pi/package-agents/gsd-ui-auditor.md +581 -0
  272. package/pi/package-agents/gsd-ui-checker.md +417 -0
  273. package/pi/package-agents/gsd-ui-researcher.md +469 -0
  274. package/pi/package-agents/gsd-user-profiler.md +174 -0
  275. package/pi/package-agents/gsd-verifier.md +963 -0
  276. package/pi/package-agents/manifest.json +39 -0
  277. package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
  278. package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
  279. package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
  280. package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
  281. package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
  282. package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
  283. package/pi/package-skills/gsd-capture/SKILL.md +68 -0
  284. package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
  285. package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
  286. package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
  287. package/pi/package-skills/gsd-config/SKILL.md +60 -0
  288. package/pi/package-skills/gsd-debug/SKILL.md +64 -0
  289. package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
  290. package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
  291. package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
  292. package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
  293. package/pi/package-skills/gsd-explore/SKILL.md +26 -0
  294. package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
  295. package/pi/package-skills/gsd-fast/SKILL.md +33 -0
  296. package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
  297. package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
  298. package/pi/package-skills/gsd-health/SKILL.md +36 -0
  299. package/pi/package-skills/gsd-help/SKILL.md +37 -0
  300. package/pi/package-skills/gsd-import/SKILL.md +46 -0
  301. package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
  302. package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
  303. package/pi/package-skills/gsd-manager/SKILL.md +46 -0
  304. package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
  305. package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
  306. package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
  307. package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
  308. package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
  309. package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
  310. package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
  311. package/pi/package-skills/gsd-next/SKILL.md +34 -0
  312. package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
  313. package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
  314. package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
  315. package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
  316. package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
  317. package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
  318. package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
  319. package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
  320. package/pi/package-skills/gsd-phase/SKILL.md +61 -0
  321. package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
  322. package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
  323. package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
  324. package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
  325. package/pi/package-skills/gsd-progress/SKILL.md +51 -0
  326. package/pi/package-skills/gsd-quick/SKILL.md +179 -0
  327. package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
  328. package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
  329. package/pi/package-skills/gsd-review/SKILL.md +50 -0
  330. package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
  331. package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
  332. package/pi/package-skills/gsd-settings/SKILL.md +30 -0
  333. package/pi/package-skills/gsd-ship/SKILL.md +27 -0
  334. package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
  335. package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
  336. package/pi/package-skills/gsd-spike/SKILL.md +53 -0
  337. package/pi/package-skills/gsd-stats/SKILL.md +22 -0
  338. package/pi/package-skills/gsd-surface/SKILL.md +177 -0
  339. package/pi/package-skills/gsd-thread/SKILL.md +29 -0
  340. package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
  341. package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
  342. package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
  343. package/pi/package-skills/gsd-undo/SKILL.md +38 -0
  344. package/pi/package-skills/gsd-update/SKILL.md +51 -0
  345. package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
  346. package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
  347. package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
  348. package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
  349. package/scripts/audit-workflow-script-paths.cjs +1 -1
  350. package/scripts/build-hooks.js +4 -0
  351. package/scripts/command-contract-helpers.cjs +1 -1
  352. package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
  353. package/scripts/docs-guard-registry.cjs +22 -7
  354. package/scripts/gen-health-docs.cjs +13 -1
  355. package/scripts/gen-pi-package-skills.cjs +217 -0
  356. package/scripts/lib/macos-conformance-tier.generated.cjs +19 -0
  357. package/scripts/lib/platform-conformance-tier.generated.cjs +41 -0
  358. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
  359. package/scripts/lint-phase-arg-assignment.cjs +2 -2
  360. package/scripts/lint-retired-runtime-name.cjs +5 -0
  361. package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
  362. package/scripts/npm-audit-baseline.cjs +41 -7
  363. package/scripts/release-tarball-smoke.cjs +1 -0
  364. package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
  365. package/scripts/select-docs-guards.cjs +1 -1
  366. package/scripts/self-test-report-policy.cjs +350 -0
  367. package/skills/gsd-code-review/SKILL.md +8 -0
  368. package/skills/gsd-pause-work/SKILL.md +1 -1
  369. package/vscode/package.json +1 -1
package/bin/install.js CHANGED
@@ -35,6 +35,7 @@ const {
35
35
  getGlobalSkillsBase,
36
36
  resolveKimiHooksTomlDir,
37
37
  isRegisteredRuntimeId,
38
+ resolveGsdHome,
38
39
  } = require('../gsd-core/bin/lib/runtime-homes.cjs');
39
40
  // #2870: the Install Scope Module — turns a bare 'global' | 'local' scope id
40
41
  // plus a runtime into one resolved value (configHome, settingsFile,
@@ -62,6 +63,8 @@ const { composeWorkflow } = require('../gsd-core/bin/lib/workflow-fragments.cjs'
62
63
  // #3072: THE shared composition-scope predicate (also consumed by the served
63
64
  // MCP catalog, src/mcp-catalog.cts) — see the comment at its call site below.
64
65
  const { shouldCompose } = require('../gsd-core/bin/lib/mcp-catalog.cjs');
66
+ // #5637: per-install terminator for the typed-text capture heredoc.
67
+ const { armArgsNonce, disarmArgsNonce, stampInstalledContent } =require('../gsd-core/bin/lib/args-terminator.cjs');
65
68
  const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
66
69
  const { escapeRegex: escapeRegExp } = require('../gsd-core/bin/lib/pattern.cjs');
67
70
  // #2873: cross-scope shadow detection — reports (never fails) when a
@@ -843,6 +846,9 @@ const {
843
846
  migrateLegacyDevPreferencesToSkill,
844
847
  USER_OWNED_ARTIFACTS,
845
848
  _snapshotDir,
849
+ createSkillDirOwnership,
850
+ _warnPreservedSkillDirs,
851
+ USER_OWNED_SKILL_DIRS,
846
852
  } = installEngine;
847
853
 
848
854
  // #2875 (epic #2866 Phase 6): durable on-disk staging for USER_OWNED_ARTIFACTS
@@ -1639,14 +1645,13 @@ const claudeToOpencodeTools = {
1639
1645
  const claudeToAntigravityTools = {
1640
1646
  // #4705: Antigravity-NATIVE tool names (see src/runtime-artifact-conversion.cts)
1641
1647
  Read: 'view_file',
1642
- Write: 'write_file',
1648
+ Write: 'write_to_file',
1643
1649
  Edit: 'replace_file_content',
1644
1650
  Bash: 'run_command',
1645
- Glob: 'glob',
1651
+ Glob: 'find_by_name',
1646
1652
  Grep: 'grep_search',
1647
- WebSearch: 'google_web_search',
1648
- WebFetch: 'web_fetch',
1649
- TodoWrite: 'write_todos',
1653
+ WebSearch: 'search_web',
1654
+ WebFetch: 'read_url_content',
1650
1655
  }
1651
1656
 
1652
1657
  // Tool name mapping from Claude/GSD agents to Kimi CLI module paths.
@@ -1715,13 +1720,16 @@ function convertAntigravityToolName(claudeTool) {
1715
1720
  // built-in tool; the lowercase fallback would emit an invalid
1716
1721
  // 'skill'/'slashcommand' name that fails frontmatter validation
1717
1722
  // (tools.N: Invalid tool name) and aborts the entire agent load (#1394).
1723
+ // TodoWrite: exclude — agy 1.3.5 registers no todo tool, and an unknown name
1724
+ // stops the agent from starting (#5447).
1718
1725
  if (
1719
1726
  claudeTool === 'Task' ||
1720
1727
  claudeTool === 'Agent' ||
1721
1728
  claudeTool === 'AskUserQuestion' ||
1722
1729
  claudeTool === 'ask_user' ||
1723
1730
  claudeTool === 'Skill' ||
1724
- claudeTool === 'SlashCommand'
1731
+ claudeTool === 'SlashCommand' ||
1732
+ claudeTool === 'TodoWrite'
1725
1733
  ) {
1726
1734
  return null;
1727
1735
  }
@@ -2155,43 +2163,11 @@ function parseKimiAgentSource(source) {
2155
2163
  };
2156
2164
  }
2157
2165
 
2166
+ // #5447: the tools list is read by the compiled module's parser (quoted entries,
2167
+ // `#` comments, inline and block lists), the one the runtime pipeline uses, so this
2168
+ // copy of the Antigravity and Kimi converters cannot diverge from it.
2158
2169
  function parseFrontmatterTools(frontmatter) {
2159
- if (!frontmatter) return [];
2160
- const lines = frontmatter.split(/\r?\n/);
2161
- const tools = [];
2162
- let collecting = false;
2163
-
2164
- for (const line of lines) {
2165
- const trimmed = line.trim();
2166
- if (!trimmed) continue;
2167
-
2168
- if (collecting) {
2169
- if (trimmed.startsWith('- ')) {
2170
- tools.push(trimmed.slice(2).trim());
2171
- continue;
2172
- }
2173
- collecting = false;
2174
- }
2175
-
2176
- if (trimmed === 'tools:' || trimmed === 'allowed-tools:') {
2177
- collecting = true;
2178
- continue;
2179
- }
2180
-
2181
- if (trimmed.startsWith('tools:') || trimmed.startsWith('allowed-tools:')) {
2182
- const value = trimmed.slice(trimmed.indexOf(':') + 1).trim();
2183
- if (value) {
2184
- for (const tool of value.split(',')) {
2185
- const name = tool.trim();
2186
- if (name) tools.push(name);
2187
- }
2188
- } else {
2189
- collecting = true;
2190
- }
2191
- }
2192
- }
2193
-
2194
- return tools;
2170
+ return runtimeArtifactConversion.parseFrontmatterTools(frontmatter);
2195
2171
  }
2196
2172
 
2197
2173
  function addKimiAgentDiagnostic(diagnostics, code, message, value, source = null) {
@@ -2499,10 +2475,11 @@ function convertClaudeAgentToAntigravityAgent(content, isGlobal = false) {
2499
2475
  const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
2500
2476
  const description = extractFrontmatterField(frontmatter, 'description') || '';
2501
2477
  const color = extractFrontmatterField(frontmatter, 'color');
2502
- const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || '';
2503
2478
 
2504
- // Map tools to Antigravity equivalents (reuse existing convertAntigravityToolName)
2505
- const claudeTools = toolsRaw.split(',').map(t => t.trim()).filter(Boolean);
2479
+ // Map tools to Antigravity equivalents (reuse existing convertAntigravityToolName).
2480
+ // parseFrontmatterTools reads both the inline comma list and the YAML block
2481
+ // list (e.g. agents/gsd-security-auditor.md) (#5447).
2482
+ const claudeTools = parseFrontmatterTools(frontmatter);
2506
2483
  const mappedTools = claudeTools.map(t => convertAntigravityToolName(t)).filter(Boolean);
2507
2484
 
2508
2485
  // #2876: quote description for the same reason as the skill variant.
@@ -3274,6 +3251,12 @@ function _stripCallArgument(spanText, matchStart, matchEnd, { consumeLeadingComm
3274
3251
  */
3275
3252
  function _projectRoleArgument(spanText, matchStart, matchEnd, roleValueExpr, toolConfig, canOrchestrate) {
3276
3253
  const { namedRoleParam, promptContentParam, structuralRoleParam, leafRoleValue } = toolConfig;
3254
+ // #5569: a primitive that resolves the role prompt itself (Pi `gsd_dispatch`)
3255
+ // takes the role name alone — no prompt-content argument, no structural role.
3256
+ // The argument's own separator is left in place.
3257
+ if (toolConfig.rolePromptResolution === 'tool') {
3258
+ return spanText.slice(0, matchStart) + `${namedRoleParam}=${roleValueExpr}` + spanText.slice(matchEnd);
3259
+ }
3277
3260
  const lineStart = spanText.lastIndexOf('\n', matchStart - 1) + 1;
3278
3261
  const startsOwnLine = /^[ \t]*$/.test(spanText.slice(lineStart, matchStart));
3279
3262
 
@@ -3405,6 +3388,20 @@ function _normalizeDispatchCallSpan(spanText, hasBraceWrapper, dispatch, toolCon
3405
3388
  }
3406
3389
  }
3407
3390
 
3391
+ // 1b. #5569: a tool-resolved primitive (`rolePromptResolution: 'tool'`) takes
3392
+ // `role` + `task` only. The task text arrives as `prompt`, so the key is
3393
+ // renamed; `description` has no counterpart and is dropped.
3394
+ if (toolConfig.rolePromptResolution === 'tool') {
3395
+ const pm = /\bprompt(\s*[=:])/.exec(_maskStringsAndComments(text));
3396
+ if (pm) {
3397
+ text = text.slice(0, pm.index) + toolConfig.taskParam + text.slice(pm.index + 'prompt'.length);
3398
+ }
3399
+ const dm = _callArgValueRe('description').exec(_maskStringsAndComments(text));
3400
+ if (dm) {
3401
+ text = _stripCallArgument(text, dm.index, dm.index + dm[0].length);
3402
+ }
3403
+ }
3404
+
3408
3405
  // 2. Per-call model argument (model= / model:) — stripped entirely when the
3409
3406
  // target has no per-call model-selection parameter (there is no
3410
3407
  // `dispatch` axis for this — it is inherent tool vocabulary, like the
@@ -3636,6 +3633,15 @@ function projectNamedDispatchToStructuralDelegate(content, dispatch, toolConfig)
3636
3633
  if (namedDispatch) {
3637
3634
  return `The \`${toolName}\` tool IS available in a top-level ${sessionName} session. Always dispatch ${rosterFlat} as separate \`${toolName}()\` calls.`;
3638
3635
  }
3636
+ if (toolConfig.rolePromptResolution === 'tool') {
3637
+ return (
3638
+ `The \`${toolName}\` tool IS available in a top-level ${sessionName} session. It takes a \`${toolConfig.namedRoleParam}\` ` +
3639
+ `(the installed gsd-* role name) and a \`${toolConfig.taskParam}\`, resolves the role prompt from the install itself, and ` +
3640
+ 'runs the role in an isolated child session; the child cannot dispatch further. FAIL CLOSED — surface the ' +
3641
+ `tool's error and stop — if it reports the role unresolved; never execute the role inline as a substitute. ` +
3642
+ `Always dispatch ${rosterFlat} as separate \`${toolName}\` calls.`
3643
+ );
3644
+ }
3639
3645
  return (
3640
3646
  `${sessionName} has no \`Agent\` tool. It exposes \`${toolName}\`, which dispatches by structural role — ` +
3641
3647
  'it has no concept of a named subagent identity. GSD projects each named gsd-* role onto this primitive ' +
@@ -3793,6 +3799,31 @@ function convertClaudeToHermesMarkdown(content, ctx) {
3793
3799
 
3794
3800
  // ── End Hermes converters ────────────────────────────────────────────────────
3795
3801
 
3802
+ // #5569: Pi projects host-neutral `Agent(...)` calls onto its `gsd_dispatch`
3803
+ // tool (pi/gsd.cjs). The tool resolves and verifies the role prompt itself, so
3804
+ // the projection only renames the arguments (`rolePromptResolution: 'tool'`).
3805
+ const PI_DISPATCH_TOOL_CONFIG = Object.freeze({
3806
+ toolName: 'gsd_dispatch',
3807
+ namedRoleParam: 'role',
3808
+ taskParam: 'task',
3809
+ promptContentParam: 'task',
3810
+ structuralRoleParam: 'role',
3811
+ leafRoleValue: 'leaf',
3812
+ backgroundParam: 'background',
3813
+ supportsPerCallModel: false,
3814
+ rolePromptResolution: 'tool',
3815
+ });
3816
+
3817
+ function convertClaudeToPiMarkdown(content, ctx) {
3818
+ const runtime = (ctx && ctx.runtime) || 'pi';
3819
+ const dispatch = _hostIntegrationDispatch(runtime);
3820
+ const toolConfig = Object.assign({}, PI_DISPATCH_TOOL_CONFIG, {
3821
+ availableRoles: _resolveAvailableGsdRoles(),
3822
+ runtime,
3823
+ });
3824
+ return projectNamedDispatchToStructuralDelegate(content, dispatch, toolConfig);
3825
+ }
3826
+
3796
3827
  function convertSlashCommandsToCodexSkillMentions(content) {
3797
3828
  // Colon-style /gsd: never appears as a filesystem path segment, so no boundary guard is needed (unlike the hyphen-style below).
3798
3829
  let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => {
@@ -4284,8 +4315,8 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
4284
4315
  * - Returns immediately if skillsDir does not exist (fails open).
4285
4316
  * - Only touches directories whose names start with "gsd-".
4286
4317
  * - Skips user-owned dirs (gsd-dev-preferences) — their agents/ content is
4287
- * never modified, mirroring the same USER_OWNED_SKILL_DIRS guard used by
4288
- * installOpencodeFamilySkills.
4318
+ * never modified, by the same USER_OWNED_SKILL_DIRS guard
4319
+ * installOpencodeFamilySkills uses.
4289
4320
  * - For each managed gsd-* dir, if agents/openai.yaml exists, deletes it.
4290
4321
  * - If agents/ is now empty, removes the directory; if it still contains
4291
4322
  * other files (e.g. user-added content), leaves it in place.
@@ -4295,15 +4326,17 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
4295
4326
  *
4296
4327
  * @param {string} skillsDir - Path to the skills/ directory (e.g. ~/.codex/skills)
4297
4328
  */
4298
- function cleanupCodexSkillMetadataSidecars(skillsDir) {
4329
+ function cleanupCodexSkillMetadataSidecars(skillsDir, owns) {
4299
4330
  if (!fs.existsSync(skillsDir)) return;
4300
- // Mirror the user-owned list from installOpencodeFamilySkills (#2973).
4301
- // We MUST skip these dirs — their contents are user-generated and must
4302
- // never be modified by GSD's install path.
4303
- const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']);
4331
+ // USER_OWNED_SKILL_DIRS (#2973) is the engine's one list. We MUST skip these
4332
+ // dirs — their contents are user-generated and must never be modified by
4333
+ // GSD's install path.
4304
4334
  for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
4305
4335
  if (!entry.isDirectory() || !entry.name.startsWith('gsd-')) continue;
4306
- if (_userOwnedSkillDirs.has(entry.name)) continue; // preserve user content
4336
+ if (USER_OWNED_SKILL_DIRS.includes(entry.name)) continue; // preserve user content
4337
+ // #5161: install keeps gsd-* dirs GSD does not own; their files are not
4338
+ // GSD's sidecars to clean.
4339
+ if (owns && !owns(entry.name)) continue;
4307
4340
  const agentsSubdir = path.join(skillsDir, entry.name, 'agents');
4308
4341
  const sidecarPath = path.join(agentsSubdir, 'openai.yaml');
4309
4342
  try {
@@ -4337,21 +4370,27 @@ function cleanupCodexSkillMetadataSidecars(skillsDir) {
4337
4370
  * - non-gsd-* dirs under .devin/skills/ (user-authored skills)
4338
4371
  * - gsd-dev-preferences/ (user-owned per #2973)
4339
4372
  * - any files (not dirs) under .devin/skills/
4373
+ * - with `owns` (#5161), any gsd-* dir GSD does not own, which is named by path
4340
4374
  *
4341
4375
  * @param {string} workspaceDir - workspace root (process.cwd() for local installs)
4376
+ * @param {(name: string) => boolean} [owns] ownership predicate (createSkillDirOwnership)
4342
4377
  * @returns {number} count of removed legacy gsd-* skill directories
4343
4378
  */
4344
- function cleanupWindsurfLegacyDevinSkills(workspaceDir) {
4379
+ function cleanupWindsurfLegacyDevinSkills(workspaceDir, owns) {
4345
4380
  const legacySkillsDir = path.join(workspaceDir, '.devin', 'skills');
4346
4381
  if (!fs.existsSync(legacySkillsDir)) return 0;
4347
4382
 
4348
- // Mirror the user-owned list from cleanupCodexSkillMetadataSidecars (#2973).
4349
- const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']);
4350
4383
  let removed = 0;
4351
4384
 
4352
- for (const entry of fs.readdirSync(legacySkillsDir, { withFileTypes: true })) {
4353
- if (!entry.isDirectory() || !entry.name.startsWith('gsd-')) continue;
4354
- if (_userOwnedSkillDirs.has(entry.name)) continue;
4385
+ // Decide ownership once, BEFORE deleting: the capability-marker leg reads the
4386
+ // dir itself, so asking after a removal would report the removed dir as kept.
4387
+ // USER_OWNED_SKILL_DIRS (#2973) dirs are never candidates.
4388
+ const candidates = fs.readdirSync(legacySkillsDir, { withFileTypes: true })
4389
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('gsd-') && !USER_OWNED_SKILL_DIRS.includes(entry.name));
4390
+ const notOwned = owns ? candidates.filter((entry) => !owns(entry.name)).map((entry) => entry.name) : [];
4391
+
4392
+ for (const entry of candidates) {
4393
+ if (notOwned.includes(entry.name)) continue;
4355
4394
 
4356
4395
  const dirToRemove = path.join(legacySkillsDir, entry.name);
4357
4396
  try {
@@ -4366,6 +4405,7 @@ function cleanupWindsurfLegacyDevinSkills(workspaceDir) {
4366
4405
  // Fail open — a single bad dir must not block the install.
4367
4406
  }
4368
4407
  }
4408
+ _warnPreservedSkillDirs(legacySkillsDir, notOwned);
4369
4409
 
4370
4410
  // If .devin/skills/ is now empty, prune it. If .devin/ itself is then empty,
4371
4411
  // prune that too — leaves the workspace clean for the new .windsurf/ layout.
@@ -4392,20 +4432,29 @@ function cleanupWindsurfLegacyDevinSkills(workspaceDir) {
4392
4432
  * relocating skills to ~/.agents/skills) orphans the pre-move dirs at
4393
4433
  * ~/.codex/skills. Only managed `<prefix>*` dirs are touched; user-owned content
4394
4434
  * (non-prefixed dirs, gsd-dev-preferences, symlinks) is preserved. Fail-open.
4435
+ *
4436
+ * #5161: with `owns`, a `<prefix>*` dir is removed only when GSD owns it; any
4437
+ * other is kept and named by path. Install passes it. Uninstall does not: it
4438
+ * removes every `gsd-*` skill dir, at either location.
4395
4439
  * @param {string} oldSkillsDir absolute path to the pre-move skills location
4396
4440
  * @param {string} prefix managed skill-dir prefix (e.g. 'gsd-')
4441
+ * @param {(name: string) => boolean} [owns] ownership predicate (createSkillDirOwnership)
4397
4442
  * @returns {number} count of stale dirs removed
4398
4443
  */
4399
- function cleanupMovedSkillsOldLocation(oldSkillsDir, prefix) {
4444
+ function cleanupMovedSkillsOldLocation(oldSkillsDir, prefix, owns) {
4400
4445
  if (!fs.existsSync(oldSkillsDir)) return 0;
4401
4446
 
4402
- // Mirror the user-owned list from cleanupCodexSkillMetadataSidecars (#2973).
4403
- const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']);
4404
4447
  let removed = 0;
4405
4448
 
4406
- for (const entry of fs.readdirSync(oldSkillsDir, { withFileTypes: true })) {
4407
- if (!entry.isDirectory() || !entry.name.startsWith(prefix)) continue;
4408
- if (_userOwnedSkillDirs.has(entry.name)) continue;
4449
+ // Decide ownership once, BEFORE deleting: the capability-marker leg reads the
4450
+ // dir itself, so asking after a removal would report the removed dir as kept.
4451
+ // USER_OWNED_SKILL_DIRS (#2973) dirs are never candidates.
4452
+ const candidates = fs.readdirSync(oldSkillsDir, { withFileTypes: true })
4453
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith(prefix) && !USER_OWNED_SKILL_DIRS.includes(entry.name));
4454
+ const notOwned = owns ? candidates.filter((entry) => !owns(entry.name)).map((entry) => entry.name) : [];
4455
+
4456
+ for (const entry of candidates) {
4457
+ if (notOwned.includes(entry.name)) continue;
4409
4458
 
4410
4459
  const dirToRemove = path.join(oldSkillsDir, entry.name);
4411
4460
  try {
@@ -4420,6 +4469,7 @@ function cleanupMovedSkillsOldLocation(oldSkillsDir, prefix) {
4420
4469
  // Fail open — a single bad dir must not block install/uninstall.
4421
4470
  }
4422
4471
  }
4472
+ _warnPreservedSkillDirs(oldSkillsDir, notOwned);
4423
4473
 
4424
4474
  // Prune the old skills dir if now empty — leaves the configHome clean.
4425
4475
  // Never remove a non-empty container (user may keep other content there).
@@ -6477,7 +6527,28 @@ function splitTopLevelKeys(content) {
6477
6527
  return { topLevel: content.trim(), rest: '' };
6478
6528
  }
6479
6529
 
6530
+ /**
6531
+ * #5301: the path a config write must target. atomicWriteFileSync ends in a
6532
+ * rename(2) onto its destination, which REPLACES a symlink at that path rather than
6533
+ * writing through it, so a dotfiles-managed `config.toml -> ~/dotfiles/codex.toml`
6534
+ * became a plain file and the user's copy never saw the merge. With
6535
+ * GSD_ALLOW_SYMLINKED_DEST the symlink is intentional: resolve it and write the
6536
+ * file it points at, leaving the link itself alone. Without the opt-in the path is
6537
+ * returned unchanged (the install-root symlink guard refuses such a layout before
6538
+ * any write), and so does a dangling link (the guard cannot resolve it), so only a
6539
+ * link whose target exists reaches the resolution below.
6540
+ */
6541
+ function resolveSymlinkWriteTarget(filePath) {
6542
+ if (!isSymlinkedDestOptIn()) return filePath;
6543
+ let stat;
6544
+ try { stat = fs.lstatSync(filePath); } catch (_) { return filePath; }
6545
+ if (!stat.isSymbolicLink()) return filePath;
6546
+ try { return fs.realpathSync(filePath); } catch (_) { return filePath; }
6547
+ }
6548
+
6480
6549
  function mergeCodexConfig(configPath, gsdBlock) {
6550
+ // #5301: both write sites below land on the symlink's target, never replace the link.
6551
+ configPath = resolveSymlinkWriteTarget(configPath);
6481
6552
  // Case 1: No config.toml — create fresh
6482
6553
  if (!fs.existsSync(configPath)) {
6483
6554
  atomicWriteFileSync(configPath, gsdBlock + '\n');
@@ -7033,7 +7104,7 @@ function recordDefaultsProvenance(gsdDir, runtime, defaults, setNow) {
7033
7104
  /** @returns {boolean} true when it removed or reverted something */
7034
7105
  function revertNonClaudeDefaults(runtime) {
7035
7106
  if (hostBehaviorsFor(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return false;
7036
- const gsdDir = path.join(os.homedir(), '.gsd');
7107
+ const gsdDir = path.join(resolveGsdHome(), '.gsd'); // #5017: GSD_HOME-aware, like the loader
7037
7108
  const provPath = path.join(gsdDir, DEFAULTS_PROVENANCE_NAME);
7038
7109
  if (!fs.existsSync(provPath)) return false;
7039
7110
  let releaseLock = null;
@@ -7086,7 +7157,7 @@ function revertNonClaudeDefaults(runtime) {
7086
7157
  */
7087
7158
  function writeNonClaudeDefaults(runtime) {
7088
7159
  if (hostBehaviorsFor(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return;
7089
- const gsdDir = path.join(os.homedir(), '.gsd');
7160
+ const gsdDir = path.join(resolveGsdHome(), '.gsd'); // #5017: GSD_HOME-aware, like the loader
7090
7161
  const defaultsPath = path.join(gsdDir, 'defaults.json');
7091
7162
  let releaseLock = null;
7092
7163
  try {
@@ -7690,6 +7761,23 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverrid
7690
7761
  // combinedFamilyInstall path (installOpencodeFamilyArtifacts) instead of the
7691
7762
  // bespoke inline block that used to call this function.
7692
7763
 
7764
+ /**
7765
+ * Count the gsd-* skill dirs in `skillsDir` for "Installed N skills" and the
7766
+ * non-empty check that detects a failed install (#5161): the dirs GSD owns,
7767
+ * plus any USER_OWNED_SKILL_DIRS entry present (gsd-dev-preferences), which
7768
+ * both counted before #5161. A gsd-* dir the prune kept because GSD does not
7769
+ * own it is not counted, so it can neither inflate the line nor stand in for
7770
+ * an install that wrote nothing.
7771
+ */
7772
+ function _countOwnedSkillDirs(runtime, configDir, skillsDir) {
7773
+ // A verification count fails closed: an unreadable install source must not
7774
+ // let a user's gsd-* dir stand in for an install that wrote nothing.
7775
+ const owns = createSkillDirOwnership(runtime, configDir, skillsDir, 'gsd-', { includeManifest: false, includeWritten: true });
7776
+ return fs.readdirSync(skillsDir, { withFileTypes: true })
7777
+ .filter((e) => e.isDirectory() && e.name.startsWith('gsd-') && (owns(e.name) || USER_OWNED_SKILL_DIRS.includes(e.name)))
7778
+ .length;
7779
+ }
7780
+
7693
7781
  function listCodexSkillNames(skillsDir, prefix = 'gsd-') {
7694
7782
  if (!fs.existsSync(skillsDir)) return [];
7695
7783
  const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
@@ -7921,6 +8009,10 @@ const RUNTIME_CONTENT_DISPATCH = {
7921
8009
  return content;
7922
8010
  },
7923
8011
  },
8012
+ pi: {
8013
+ // #5569: `Agent(...)` -> `gsd_dispatch(role=..., task=...)`.
8014
+ md: (content, ctx) => convertClaudeToPiMarkdown(content, ctx),
8015
+ },
7924
8016
  hermes: {
7925
8017
  // #2284: brand-swap alone left the false "Agent tool IS available"
7926
8018
  // assertion + literal `Agent(...)` call syntax installed verbatim — see
@@ -8040,6 +8132,8 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
8040
8132
  if (shouldCompose(srcPath)) {
8041
8133
  content = composeWorkflow(content, { sourcePath: srcPath });
8042
8134
  }
8135
+ // #5637: the typed-text capture heredoc ends at a terminator the typed text cannot know.
8136
+ content = stampInstalledContent(content);
8043
8137
 
8044
8138
  content = filterRuntimeNotesForTarget(content, runtime);
8045
8139
 
@@ -10158,7 +10252,16 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
10158
10252
  if (!hostBehaviorsFor(runtime).skipCodexSkillsManifest && fs.existsSync(codexSkillsDir)) {
10159
10253
  // All runtimes (including Hermes post-#947) use the canonical 'gsd-' prefix.
10160
10254
  const skillListPrefix = 'gsd-';
10255
+ // #5161: record only skill dirs GSD owns. A user's gsd-* dir the prune
10256
+ // preserved must not enter the manifest, or the next install reads it back
10257
+ // as GSD-owned and deletes it. includeManifest:false keeps the previous
10258
+ // manifest from vouching for itself; manifestIfSourceUnresolved keeps an
10259
+ // unreadable install source from dropping every GSD skill from the manifest
10260
+ // by carrying the previous manifest's entries forward instead of recording
10261
+ // every gsd-* dir present.
10262
+ const ownsSkillDir = createSkillDirOwnership(runtime, configDir, codexSkillsDir, skillListPrefix, { includeManifest: false, manifestIfSourceUnresolved: true, includeWritten: true });
10161
10263
  for (const skillName of listCodexSkillNames(codexSkillsDir, skillListPrefix)) {
10264
+ if (!ownsSkillDir(skillName) && !USER_OWNED_SKILL_DIRS.includes(skillName)) continue;
10162
10265
  const skillRoot = path.join(codexSkillsDir, skillName);
10163
10266
  const skillHashes = generateManifest(skillRoot);
10164
10267
  for (const [rel, hash] of Object.entries(skillHashes)) {
@@ -10747,7 +10850,8 @@ function reportLocalPatches(configDir, runtime = DEFAULT_RUNTIME) {
10747
10850
  if (meta.files && meta.files.length > 0) {
10748
10851
  const reapplyCommand = hostBehaviorsFor(runtime).reapplyCommand || '/gsd-update --reapply';
10749
10852
  console.log('');
10750
- console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
10853
+ const fromLabel = meta.from_version ? ' (from v' + meta.from_version + ')' : ' (from a first install)';
10854
+ console.log(' ' + yellow + 'Local patches detected' + reset + fromLabel + ':');
10751
10855
  for (const f of meta.files) {
10752
10856
  console.log(' ' + cyan + f + reset);
10753
10857
  }
@@ -10760,6 +10864,114 @@ function reportLocalPatches(configDir, runtime = DEFAULT_RUNTIME) {
10760
10864
  return meta.files || [];
10761
10865
  }
10762
10866
 
10867
+ /**
10868
+ * #5522: the first-time baseline reports a user's `skills/gsd-<stem>/SKILL.md` as preserved, but the
10869
+ * skills writer owns a dir by its first-party name and replaces it. Before the writers run, set aside a copy of
10870
+ * every baseline-preserved file in the shipped-name namespaces (bounded to BASELINE_SNAPSHOT_MAX_BYTES) in
10871
+ * gsd-migration-backups/ — an installer-internal dir, so it can never collide with a user's existing
10872
+ * gsd-local-patches/ content — so the user's content is on disk before anything can replace it.
10873
+ * backUpReplacedBaselineFiles then moves the copies of replaced files into gsd-local-patches/.
10874
+ * Returns Map<relPath, { fullPath, content, stagedPath }>.
10875
+ */
10876
+ const BASELINE_SNAPSHOT_MAX_BYTES = 1024 * 1024;
10877
+ const BASELINE_SNAPSHOT_DIR = path.join('gsd-migration-backups', 'first-install-5522');
10878
+ const SHIPPED_NAME_NAMESPACE = /^(skills|commands?)\/gsd([-/]|$)/;
10879
+ function snapshotBaselinePreservedFiles(configDir, migrationResult) {
10880
+ const snapshots = new Map();
10881
+ const plan = migrationResult && migrationResult.plan;
10882
+ const actions = plan && Array.isArray(plan.actions) ? plan.actions : [];
10883
+ const stagingDir = path.join(configDir, BASELINE_SNAPSHOT_DIR);
10884
+ for (const action of actions) {
10885
+ if (!action || action.type !== 'baseline-preserve-user' || typeof action.relPath !== 'string') continue;
10886
+ const ref = resolveInstallRelativePath(configDir, action.relPath);
10887
+ if (!ref || !SHIPPED_NAME_NAMESPACE.test(ref.relPath)) continue;
10888
+ try {
10889
+ const st = fs.lstatSync(ref.fullPath);
10890
+ if (!st.isFile()) continue;
10891
+ if (st.size > BASELINE_SNAPSHOT_MAX_BYTES) {
10892
+ console.warn(` ${yellow}⚠${reset} ${action.relPath} is larger than ${BASELINE_SNAPSHOT_MAX_BYTES} bytes, so it is not copied aside; if the install replaces it, the original is not kept`);
10893
+ continue;
10894
+ }
10895
+ const content = fs.readFileSync(ref.fullPath);
10896
+ const stagedRef = resolveInstallRelativePath(stagingDir, ref.relPath);
10897
+ if (!stagedRef) continue;
10898
+ fs.mkdirSync(path.dirname(stagedRef.fullPath), { recursive: true });
10899
+ fs.writeFileSync(stagedRef.fullPath, content);
10900
+ snapshots.set(ref.relPath, { fullPath: ref.fullPath, content, stagedPath: stagedRef.fullPath });
10901
+ } catch (err) {
10902
+ console.warn(` ${yellow}⚠${reset} Could not set aside ${action.relPath} before install (${err.message}); if the install replaces it, the original is not kept`);
10903
+ }
10904
+ }
10905
+ return snapshots;
10906
+ }
10907
+
10908
+ /**
10909
+ * #5522: after the artifact writers ran, a snapshotted file that is gone or different was replaced. Its
10910
+ * pre-install copy moves into gsd-local-patches/ (next to, never over, an existing backup) and is recorded in
10911
+ * backup-meta.json, so the standard "Local patches detected" report names it as it does on the update path.
10912
+ * The staging dir is always removed. Never throws: bookkeeping must not abort the install.
10913
+ * Returns [{ relPath, backupRel }] for the files kept.
10914
+ */
10915
+ function backUpReplacedBaselineFiles(configDir, snapshots) {
10916
+ const replaced = [];
10917
+ const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
10918
+ for (const [relPath, snap] of snapshots) {
10919
+ let current = null;
10920
+ try { current = fs.readFileSync(snap.fullPath); } catch { current = null; }
10921
+ if (current !== null && current.equals(snap.content)) continue;
10922
+ try {
10923
+ let backupRel = relPath;
10924
+ if (fs.existsSync(path.join(patchesDir, backupRel))) backupRel = `${relPath}.first-install`;
10925
+ const backupRef = resolveInstallRelativePath(patchesDir, backupRel);
10926
+ if (!backupRef) throw new Error('backup path escapes the config dir');
10927
+ fs.mkdirSync(path.dirname(backupRef.fullPath), { recursive: true });
10928
+ fs.writeFileSync(backupRef.fullPath, snap.content);
10929
+ replaced.push({ relPath, backupRel });
10930
+ } catch (err) {
10931
+ console.warn(` ${yellow}⚠${reset} Could not back up ${relPath} to ${PATCHES_DIR_NAME}/ (${err.message}); the original is kept in ${BASELINE_SNAPSHOT_DIR}/`);
10932
+ return replaced;
10933
+ }
10934
+ }
10935
+ if (replaced.length > 0) {
10936
+ try {
10937
+ const metaPath = path.join(patchesDir, 'backup-meta.json');
10938
+ let meta = {};
10939
+ try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')) || {}; } catch { meta = {}; }
10940
+ const files = Array.from(new Set([...(Array.isArray(meta.files) ? meta.files : []), ...replaced.map((r) => r.backupRel)]));
10941
+ fs.writeFileSync(metaPath, JSON.stringify({
10942
+ ...meta,
10943
+ backed_up_at: new Date().toISOString(),
10944
+ from_version: meta.from_version ?? null,
10945
+ files,
10946
+ pristine_hashes: { ...(meta.pristine_hashes || {}) },
10947
+ }, null, 2));
10948
+ } catch (err) {
10949
+ console.warn(` ${yellow}⚠${reset} Could not record ${PATCHES_DIR_NAME}/backup-meta.json (${err.message}); your copies are in ${PATCHES_DIR_NAME}/`);
10950
+ }
10951
+ }
10952
+ try { fs.rmSync(path.join(configDir, BASELINE_SNAPSHOT_DIR), { recursive: true, force: true }); } catch { /* leftover is harmless */ }
10953
+ return replaced;
10954
+ }
10955
+
10956
+ /** relPaths in the snapshot whose name is a shipped skill/command the writers will lay down (#5522). */
10957
+ function predictShippedNameReplacements(snapshots, skillsManifest) {
10958
+ const out = [];
10959
+ for (const relPath of snapshots.keys()) {
10960
+ const m = /^skills\/gsd-([^/]+)\//.exec(relPath) || /^commands?\/gsd\/([^/]+?)\.md$/.exec(relPath);
10961
+ if (m && skillsManifest.has(m[1])) out.push(relPath);
10962
+ }
10963
+ return out;
10964
+ }
10965
+
10966
+ /** A copy of the migration result without the baseline-preserve rows for paths the install replaced (#5522). */
10967
+ function withoutReplacedBaselineFiles(migrationResult, replaced) {
10968
+ if (!replaced || replaced.length === 0 || !migrationResult || !migrationResult.plan) return migrationResult;
10969
+ const norm = (p) => String(p).replace(/\\/g, '/').replace(/^\/+/, '');
10970
+ const gone = new Set(replaced.map((r) => norm(r.relPath)));
10971
+ const actions = (migrationResult.plan.actions || []).filter((a) => !(a && a.type === 'baseline-preserve-user' && gone.has(norm(a.relPath))));
10972
+ return { ...migrationResult, plan: { ...migrationResult.plan, actions } };
10973
+ }
10974
+
10763
10975
  function reportInstallerMigrationResult(result) {
10764
10976
  const summary = summarizeInstallerMigrationResult(result);
10765
10977
  if (!summary.hasReportableActions) return;
@@ -10893,6 +11105,20 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10893
11105
  ? process.cwd()
10894
11106
  : path.join(process.cwd(), dirName);
10895
11107
 
11108
+ // #5637: this install's typed-text heredoc terminator: the directory's existing nonce on a
11109
+ // reinstall (so it changes nothing), a fresh one otherwise.
11110
+ if (process.env.GSD_ARGS_NONCE_OFF !== '1') {
11111
+ // Only a global install reuses its nonce; a project-local dir may be a committed clone.
11112
+ armArgsNonce(targetDir, isGlobal);
11113
+ } else {
11114
+ disarmArgsNonce();
11115
+ console.warn(' GSD_ARGS_NONCE_OFF=1: the typed-text heredoc terminator is NOT randomized in this install.');
11116
+ }
11117
+
11118
+ // #5161: this install's record of the skill dirs it writes starts empty,
11119
+ // whichever install path it takes.
11120
+ installEngine.clearWrittenSkillDirs();
11121
+
10896
11122
  // #3664: a --config-dir destination holding foreign agent files gets an
10897
11123
  // explicit install-time warning — never a silent Claude-shaped emit.
10898
11124
  if (isGlobal) {
@@ -10962,8 +11188,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10962
11188
  const _isCoreProfileAlias = _activeProfileName === 'core';
10963
11189
  const _effectiveInstallMode = _isCoreProfileAlias ? 'minimal' : 'full';
10964
11190
  // Load the manifest and compute resolved profile for named profiles.
10965
- // For --minimal/core: use an empty manifest (core profile has no transitive
10966
- // deps) to produce a resolvedProfile with the core skill set. For core/
11191
+ // For --minimal/core: use an empty manifest for the SKILL closure (core profile has no
11192
+ // transitive skill deps) to produce a resolvedProfile with the core skill set; agents
11193
+ // come from the full manifest (#5503, see below). For core/
10967
11194
  // standard profiles, resolveProfile's `registry` arg IS consulted (via
10968
11195
  // _capabilitySkillsForMode) so tier:core/tier:standard capability skills are
10969
11196
  // unioned in when registered. #2322 correction: for the DEFAULT `full`
@@ -10976,10 +11203,15 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10976
11203
  // is passed so an INSTALLED third-party capability (not just a first-party
10977
11204
  // one) is honored on every profile, `full` included (#2322 blocker 2).
10978
11205
  const _commandsDir = path.join(src, 'commands', 'gsd');
10979
- const _skillsManifest = _isCoreProfileAlias ? new Map() : loadSkillsManifest(_commandsDir);
11206
+ // #5503: the empty manifest only keeps core's SKILL set at the eight core skills (no
11207
+ // `requires:` closure). The agents those skills spawn are still derived from the real
11208
+ // manifest, so a core install can run /gsd-plan-phase and friends.
11209
+ const _fullSkillsManifest = loadSkillsManifest(_commandsDir);
11210
+ const _skillsManifest = _isCoreProfileAlias ? new Map() : _fullSkillsManifest;
10980
11211
  let _resolvedProfile = resolveProfile({
10981
11212
  modes: [_activeProfileName],
10982
11213
  manifest: _skillsManifest,
11214
+ agentManifest: _fullSkillsManifest,
10983
11215
  registry: _installedCapabilityRegistry,
10984
11216
  });
10985
11217
  // Unified staging function: all profiles use stageSkillsForProfile with the
@@ -10988,9 +11220,13 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10988
11220
  return stageSkillsForProfile(commandsGsdDir, _resolvedProfile);
10989
11221
  }
10990
11222
  function _stageAgents(agentsDir) {
10991
- if (_isCoreProfileAlias) return agentsDir;
11223
+ // #5503: no core-alias shortcut — Codex gets the same profile-filtered agents as every runtime.
10992
11224
  return stageAgentsForProfile(agentsDir, _resolvedProfile);
10993
11225
  }
11226
+ // #5503: Codex registers agent roles in config.toml. That used to be gated on "minimal mode"
11227
+ // because minimal staged no agents; gate it on whether the resolved profile stages any.
11228
+ const _codexAgentRolesEnabled = () =>
11229
+ !isMinimalMode(_effectiveInstallMode) || (_resolvedProfile.agents instanceof Set && _resolvedProfile.agents.size > 0);
10994
11230
  const persistActiveProfileMarker = () => {
10995
11231
  try {
10996
11232
  writeActiveProfile(targetDir, _activeProfileName);
@@ -11365,7 +11601,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11365
11601
  // The full restoreCodexSnapshot() (defined inside the config block) additionally
11366
11602
  // handles config.toml and the staged hooks/ tree, which are not yet touched
11367
11603
  // at this point in the pipeline.
11368
- const _codexPreConfigRollback = !hostBehaviorsFor(runtime).tomlConfigInstall || isMinimalMode(_effectiveInstallMode) ? null : () => {
11604
+ const _codexPreConfigRollback = !hostBehaviorsFor(runtime).tomlConfigInstall || !_codexAgentRolesEnabled() ? null : () => {
11369
11605
  rollbackInstallerMigrations();
11370
11606
  // skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
11371
11607
  const _earlySkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
@@ -11467,6 +11703,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11467
11703
  // scope (outside the try block below) so it is accessible in the settings section later.
11468
11704
  // Absent VERSION = fresh install; present VERSION = upgrade/re-install.
11469
11705
  const priorInstallExisted = fs.existsSync(path.join(targetDir, 'gsd-core', 'VERSION'));
11706
+ // #5522: files the first-time baseline reported as preserved, snapshotted before the artifact writers run.
11707
+ let baselinePreservedSnapshots = new Map();
11470
11708
  try {
11471
11709
  installerMigrationResult = runInstallerMigrations({
11472
11710
  configDir: targetDir,
@@ -11520,8 +11758,18 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11520
11758
  };
11521
11759
  }
11522
11760
  }
11523
- reportInstallerMigrationResult(installerMigrationResult);
11761
+ if (Array.isArray(installerMigrationResult.blocked) && installerMigrationResult.blocked.length > 0) {
11762
+ reportInstallerMigrationResult(installerMigrationResult); // show what blocked before the assertion throws
11763
+ }
11524
11764
  assertInstallerMigrationsUnblocked(installerMigrationResult);
11765
+ baselinePreservedSnapshots = snapshotBaselinePreservedFiles(targetDir, installerMigrationResult);
11766
+ // #5522: the migration report stays ahead of materialization, but a baseline-preserved file the skills
11767
+ // writer is about to replace (its name is a shipped skill's) is not listed as preserved; the
11768
+ // replacement is reported once the writers have run.
11769
+ reportInstallerMigrationResult(withoutReplacedBaselineFiles(
11770
+ installerMigrationResult,
11771
+ predictShippedNameReplacements(baselinePreservedSnapshots, _fullSkillsManifest).map((relPath) => ({ relPath }))
11772
+ ));
11525
11773
 
11526
11774
  // Artifact install dispatcher — routes to layout-driven path for all
11527
11775
  // skills-based runtimes (both full and minimal/core profiles); keeps
@@ -11687,7 +11935,16 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11687
11935
  // in autocomplete. Cleaning them up fixes the duplication; SKILL.md alone is
11688
11936
  // sufficient for Codex discovery. User-owned dirs are never touched.
11689
11937
  if (hostBehaviorsFor(runtime).cleanupSkillSidecars) {
11690
- cleanupCodexSkillMetadataSidecars(_skillsRootDir);
11938
+ // #5161: no includeWritten here, unlike the count and writeManifest. The
11939
+ // record would add only skills staged from a command (a capability skill
11940
+ // carries the marker, which this predicate already owns). The writer prunes
11941
+ // each of those except gsd-dev-preferences and writes it fresh as a lone
11942
+ // SKILL.md, so it holds no sidecar for this cleanup to remove, and the
11943
+ // cleanup skips gsd-dev-preferences by name (USER_OWNED_SKILL_DIRS).
11944
+ cleanupCodexSkillMetadataSidecars(
11945
+ _skillsRootDir,
11946
+ createSkillDirOwnership(runtime, targetDir, _skillsRootDir, 'gsd-', { includeManifest: false }),
11947
+ );
11691
11948
  }
11692
11949
 
11693
11950
  // ADR-1239 split-home migration: when a runtime's skills kind moved to an
@@ -11695,10 +11952,16 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11695
11952
  // managed gsd-* skill dirs at the old configDir-rooted location
11696
11953
  // (~/.codex/skills). Reinstalling here writes the new location but would
11697
11954
  // otherwise orphan the old one — clean up the stale gsd-* dirs.
11955
+ // #5161: only the ones GSD owns; any other gsd-* dir is kept and named.
11956
+ // The manifest leg stays off: codex records its skills under `skills/`
11957
+ // whichever root they sit in, so a name the manifest recorded for the new
11958
+ // home would otherwise claim a same-named user dir at this old root that
11959
+ // GSD never wrote.
11698
11960
  {
11699
11961
  const _movedOldSkillsDir = _resolveMovedSkillsOldDir(runtime, targetDir, scope);
11700
11962
  if (_movedOldSkillsDir) {
11701
- const migrated = cleanupMovedSkillsOldLocation(_movedOldSkillsDir, 'gsd-');
11963
+ const migrated = cleanupMovedSkillsOldLocation(_movedOldSkillsDir, 'gsd-',
11964
+ createSkillDirOwnership(runtime, targetDir, _movedOldSkillsDir, 'gsd-', { includeManifest: false }));
11702
11965
  if (migrated > 0) {
11703
11966
  console.log(` ${green}✓${reset} Migrated ${migrated} skill dir(s) off the legacy ${_movedOldSkillsDir} location`);
11704
11967
  }
@@ -11712,8 +11975,13 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11712
11975
  // Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into
11713
11976
  // hostBehaviors.legacyDevinSkillsCleanup (windsurf is the only runtime that
11714
11977
  // declares it, so this is byte-parity).
11978
+ // #5161: only the gsd-* dirs GSD owns. The local manifest describes
11979
+ // .windsurf/, never .devin/, so the manifest leg stays off here: a name it
11980
+ // records under one root says nothing about a same-named dir under another.
11715
11981
  if (hostBehaviorsFor(runtime).legacyDevinSkillsCleanup && !isGlobal) {
11716
- const removedCount = cleanupWindsurfLegacyDevinSkills(process.cwd());
11982
+ const _devinSkillsDir = path.join(process.cwd(), '.devin', 'skills');
11983
+ const removedCount = cleanupWindsurfLegacyDevinSkills(process.cwd(),
11984
+ createSkillDirOwnership(runtime, targetDir, _devinSkillsDir, 'gsd-', { includeManifest: false }));
11717
11985
  if (removedCount > 0) {
11718
11986
  console.log(` ${green}✓${reset} Removed ${removedCount} legacy .devin/skills/gsd-* dir(s) (pre-#1615 Windsurf layout)`);
11719
11987
  }
@@ -11729,8 +11997,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11729
11997
  const hermesSkillsDir = path.join(targetDir, 'skills', 'gsd');
11730
11998
  if (fs.existsSync(hermesSkillsDir)) {
11731
11999
  // Hermes layout uses prefix: 'gsd-' (#947) — skill dirs have gsd-<stem> names
11732
- const count = fs.readdirSync(hermesSkillsDir, { withFileTypes: true })
11733
- .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
12000
+ const count = _countOwnedSkillDirs(runtime, targetDir, hermesSkillsDir);
11734
12001
  if (count > 0) {
11735
12002
  console.log(` ${green}✓${reset} Installed ${count} skills to skills/gsd/`);
11736
12003
  } else {
@@ -11743,8 +12010,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11743
12010
  const skillsDir = path.join(targetDir, 'skills');
11744
12011
  const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml');
11745
12012
  if (fs.existsSync(skillsDir)) {
11746
- const count = fs.readdirSync(skillsDir, { withFileTypes: true })
11747
- .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
12013
+ const count = _countOwnedSkillDirs(runtime, targetDir, skillsDir);
11748
12014
  if (count > 0) {
11749
12015
  console.log(` ${green}✓${reset} Installed ${count} Kimi skills to skills/`);
11750
12016
  } else {
@@ -11783,8 +12049,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11783
12049
  } else {
11784
12050
  const skillsDir = _skillsRootDir;
11785
12051
  if (fs.existsSync(skillsDir)) {
11786
- const count = fs.readdirSync(skillsDir, { withFileTypes: true })
11787
- .filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
12052
+ const count = _countOwnedSkillDirs(runtime, targetDir, skillsDir);
11788
12053
  if (count > 0) {
11789
12054
  console.log(` ${green}✓${reset} Installed ${count} skills to skills/`);
11790
12055
  } else {
@@ -11844,6 +12109,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11844
12109
  // command/ directories), so writing flat gsd-<cmd>.md files here would be
11845
12110
  // dead weight the extension never reads. Skip the flat-commands fallback
11846
12111
  // entirely for pluginOnlyInstall runtimes.
12112
+ // NOTE (#5547): pi now declares a skills artifactLayout, so it takes the
12113
+ // `_isSkillsRuntime` branch above; this branch is reached only if that layout
12114
+ // is ever emptied again.
11847
12115
  console.log(` ${green}✓${reset} pi: /gsd registered via native extension (no declarative command files)`);
11848
12116
  } else {
11849
12117
  // Claude Code local: flat gsd-<cmd>.md layout — Claude Code registers
@@ -11920,28 +12188,40 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11920
12188
  }
11921
12189
  }
11922
12190
 
11923
- // Clean up any stale skills/ from a previous local install
12191
+ // Clean up any stale skills/ from a previous local install. #5161: only
12192
+ // dirs GSD owns — .claude/skills/ is also where a project keeps its own
12193
+ // skills, so a user's gsd-* dir there is preserved and named instead.
11924
12194
  const staleSkillsDir = path.join(targetDir, 'skills');
11925
12195
  if (fs.existsSync(staleSkillsDir)) {
11926
- const staleGsd = fs.readdirSync(staleSkillsDir, { withFileTypes: true })
11927
- .filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
12196
+ // Decide ownership once, BEFORE deleting: the capability-marker leg reads
12197
+ // the dir itself, so asking again afterwards reports a deleted dir as kept.
12198
+ // A symlink is never removed here (as before) but is named when unowned.
12199
+ const ownsSkillDir = createSkillDirOwnership(runtime, targetDir, staleSkillsDir, 'gsd-');
12200
+ const prefixed = fs.readdirSync(staleSkillsDir, { withFileTypes: true })
12201
+ .filter(e => (e.isDirectory() || e.isSymbolicLink()) && e.name.startsWith('gsd-'))
12202
+ .map(e => ({ name: e.name, isDir: e.isDirectory(), owned: ownsSkillDir(e.name) }));
12203
+ // gsd-dev-preferences is user content even when an old manifest records
12204
+ // it, and nothing here restores it (#2973), so it is never removed.
12205
+ const staleGsd = prefixed.filter(e => e.isDir && e.owned && !USER_OWNED_SKILL_DIRS.includes(e.name));
11928
12206
  for (const e of staleGsd) {
11929
12207
  fs.rmSync(path.join(staleSkillsDir, e.name), { recursive: true });
11930
12208
  }
11931
12209
  if (staleGsd.length > 0) {
11932
12210
  console.log(` ${green}✓${reset} Removed ${staleGsd.length} stale GSD skill(s) from skills/`);
11933
12211
  }
12212
+ _warnPreservedSkillDirs(staleSkillsDir, prefixed.filter(e => !e.owned).map(e => e.name));
11934
12213
  }
11935
12214
  }
11936
12215
 
11937
12216
  // Native-extension/plugin staging for runtimes OUTSIDE the layout-driven
11938
- // _isSkillsRuntime branch above (ADR-1239 / #2102 Stage 1: pi). OpenCode/Kilo
12217
+ // _isSkillsRuntime branch above (ADR-1239 / #2102 Stage 1). OpenCode/Kilo
11939
12218
  // already get their nativePlugin file from installOpencodeFamilyArtifacts
11940
12219
  // (called inside the _isSkillsRuntime branch, since both declare a non-empty
11941
12220
  // artifactLayout) — guard on `!_isSkillsRuntime` so this standalone call never
11942
- // double-stages their plugin file. A runtime like pi, whose artifactLayout is
11943
- // intentionally empty for both scopes (`_isSkillsRuntime` is false), still
11944
- // needs its declared hostBehaviors.nativePlugin file copied into targetDir.
12221
+ // double-stages their plugin file. pi (#5547) now declares a skills layout, so
12222
+ // its extension is staged by installRuntimeArtifacts' own native-plugin
12223
+ // post-step; this standalone call remains for any runtime that declares a
12224
+ // nativePlugin with an intentionally empty artifactLayout.
11945
12225
  if (!_isSkillsRuntime && hostBehaviorsFor(runtime).nativePlugin) {
11946
12226
  _installNativePluginIfDeclared(runtime, targetDir, hostBehaviorsFor(runtime), src);
11947
12227
  }
@@ -12024,10 +12304,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12024
12304
  // here explicitly to cover them (install-engine.cts's own doc comment
12025
12305
  // explains why; a regression here was caught by the install-tree golden
12026
12306
  // fixture, tests/fixtures/install-tree/claude-local.json). It is a no-op
12027
- // for pi: its capability.json declares an EMPTY artifactLayout for both
12028
- // scopes (programmatic dispatch, no named-dispatch subagent toolkit, no
12029
- // host-read markdown surface), so the resolved layout has no `agents` kind
12030
- // to stage and the function returns `null` without writing anything.
12307
+ // for pi: its capability.json declares a skills-only artifactLayout for both
12308
+ // scopes (#5547; programmatic dispatch, no named-dispatch subagent toolkit),
12309
+ // so the resolved layout has no `agents` kind to stage and the function
12310
+ // returns `null` without writing anything.
12031
12311
  if (hostBehaviorsFor(runtime).pluginOnlyInstall) {
12032
12312
  console.log(` ${green}✓${reset} pi: no subagent files (programmatic dispatch, no named-dispatch toolkit)`);
12033
12313
  } else if (_isSkillsRuntime) {
@@ -12063,7 +12343,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12063
12343
  // advertising the old full agent surface even though the descriptor-driven
12064
12344
  // write above already skipped writing the .md files for a minimal-tier
12065
12345
  // resolvedProfile. Reuse the same helper that powers `--uninstall`.
12066
- if (isMinimalMode(_effectiveInstallMode) && hostBehaviorsFor(runtime).tomlConfigInstall) {
12346
+ if (!_codexAgentRolesEnabled() && hostBehaviorsFor(runtime).tomlConfigInstall) {
12067
12347
  const codexConfigPath = path.join(targetDir, 'config.toml');
12068
12348
  if (fs.existsSync(codexConfigPath)) {
12069
12349
  const existing = fs.readFileSync(codexConfigPath, 'utf8');
@@ -12472,6 +12752,13 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12472
12752
  }
12473
12753
  }
12474
12754
 
12755
+ // #5522: keep the user's copy of any baseline-preserved file the writers replaced (the update path's
12756
+ // gsd-local-patches/ backup), then report the migrations with those paths no longer listed as preserved.
12757
+ const replacedBaselineFiles = backUpReplacedBaselineFiles(targetDir, baselinePreservedSnapshots);
12758
+ for (const { relPath, backupRel } of replacedBaselineFiles) {
12759
+ console.log(` ${yellow}i${reset} ${relPath} was replaced by the install (it has the name of a shipped GSD file) — your copy is backed up to ${PATCHES_DIR_NAME}/${backupRel}`);
12760
+ }
12761
+
12475
12762
  if (failures.length > 0) {
12476
12763
  console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
12477
12764
  process.exit(1);
@@ -12565,10 +12852,56 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12565
12852
  }
12566
12853
  }
12567
12854
  }
12855
+ // #5123: the Copilot and Antigravity converters write their DEFAULT home
12856
+ // into `@` includes and Kimi's prompt converter leaves a prose "GSD core/"
12857
+ // path, none of which exist when the payload lives somewhere else. Point
12858
+ // them at the real install root (the same `$HOME/…` / absolute prefix the
12859
+ // other runtimes use). Global installs only: a local install resolves
12860
+ // against its project, which the converters already handle.
12861
+ const includeRetarget = hostBehaviorsFor(runtime);
12862
+ if (isGlobal && (includeRetarget.defaultHomeIncludeRoot || includeRetarget.retargetsGsdCoreProsePath === true)) {
12863
+ const includePrefix = runtimeArtifactConversion._computePathPrefix({
12864
+ isGlobal: true,
12865
+ isOpencode: false,
12866
+ resolvedTarget: targetDir,
12867
+ homeDir: os.homedir(),
12868
+ });
12869
+ for (const relPath of manifestFiles) {
12870
+ const fileName = path.basename(relPath);
12871
+ if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
12872
+ if (fileName === 'CHANGELOG.md') continue;
12873
+ const retargetPath = path.join(targetDir, relPath);
12874
+ let retargetContent;
12875
+ try {
12876
+ retargetContent = fs.readFileSync(retargetPath, 'utf8');
12877
+ } catch (_retargetReadErr) {
12878
+ continue; // inaccessible or missing — never fail the install over this pass
12879
+ }
12880
+ const retargeted = runtimeArtifactConversion.retargetInstalledIncludes(retargetContent, includeRetarget, includePrefix);
12881
+ if (retargeted !== retargetContent) {
12882
+ try {
12883
+ fs.writeFileSync(retargetPath, retargeted);
12884
+ } catch (_retargetWriteErr) {
12885
+ continue;
12886
+ }
12887
+ }
12888
+ }
12889
+ }
12568
12890
  for (const relPath of manifestFiles) {
12569
12891
  const fileName = path.basename(relPath);
12570
12892
  if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
12571
12893
  if (fileName === 'CHANGELOG.md') continue;
12894
+ // #5547: the Runtime Surface corpus is the package's canonical source
12895
+ // bytes, kept byte-identical so a refresh can restore them
12896
+ // (tests/install-runtime-artifacts.test.cjs); the converted artifacts
12897
+ // are what the host reads. Counting the source form as a "leak" made
12898
+ // every skills-runtime install print a warning about files nothing reads.
12899
+ // Windsurf is the exception: its workflow wrappers delegate to the
12900
+ // path-rewritten bodies at gsd-core/commands/gsd/ (#1629), so there they
12901
+ // ARE host-read artifacts and stay in the scan.
12902
+ const posixRelPath = relPath.replace(/\\/g, '/');
12903
+ if (!hostBehaviorsFor(runtime).installsCommandBodiesForWorkflowDelegation
12904
+ && (posixRelPath.startsWith('gsd-core/commands/') || posixRelPath.startsWith('gsd-core/agents/'))) continue;
12572
12905
  const fullPath = path.join(targetDir, relPath);
12573
12906
  let content;
12574
12907
  try {
@@ -12631,9 +12964,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12631
12964
  // #2695: this branch runs for BOTH `core` (minimal) and `full` profiles.
12632
12965
  // Hooks are lightweight infrastructure (update-check + context monitor), not the
12633
12966
  // "full agent surface" that `core` deliberately omits. The config.toml / agent
12634
- // generation below is still gated by its own inner `!isMinimalMode` guard, so
12635
- // `core` enters the branch to receive the hook-file copy + hooks.json wiring but
12636
- // does NOT get agent roles generated. Before #2695 the outer `!isMinimalMode`
12967
+ // generation below is gated by its own inner `_codexAgentRolesEnabled()` guard (#5503:
12968
+ // it used to be `!isMinimalMode`, which kept `core` from registering the agents its
12969
+ // skills spawn), so `core` enters the branch to receive the hook-file copy + hooks.json
12970
+ // wiring AND the agent roles its own skills spawn. Before #2695 the outer `!isMinimalMode`
12637
12971
  // here skipped the whole branch for `core`, so the registered parent hook pointed
12638
12972
  // at a worker/registry the same installer never delivered.
12639
12973
  if (plan.installSurface === 'codex-toml') {
@@ -12804,7 +13138,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12804
13138
  };
12805
13139
 
12806
13140
  let agentCount = 0;
12807
- if (!isMinimalMode(_effectiveInstallMode)) {
13141
+ if (_codexAgentRolesEnabled()) {
12808
13142
  // #2834: write ~/.gsd/defaults.json (resolve_model_ids + runtime) BEFORE generating
12809
13143
  // agent TOMLs — installCodexConfig reads defaults.json at generation time, so on a
12810
13144
  // clean first install the runtime-aware model resolver must already know the runtime.
@@ -12823,7 +13157,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12823
13157
  // not include agents/gsd-*.toml because those files did not yet exist.
12824
13158
  writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: _installScopeId });
12825
13159
  } else {
12826
- console.log(` ${dim}↳${reset} Skipping Codex agent config generation (minimal install)`);
13160
+ console.log(` ${dim}↳${reset} Skipping Codex agent config generation (${_resolvedProfile.name} profile stages no agents)`);
12827
13161
  }
12828
13162
 
12829
13163
  // Copy only the hook files that Codex actually registers via its hook configuration (#2153).
@@ -13022,7 +13356,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13022
13356
  // existing config; the snapshot restore below is a second line of
13023
13357
  // defense if even the rename fails.
13024
13358
  try {
13025
- atomicWriteFileSync(configPath, configContent, 'utf-8');
13359
+ // #5301: write through an opted-in symlink instead of replacing it.
13360
+ atomicWriteFileSync(resolveSymlinkWriteTarget(configPath), configContent, 'utf-8');
13026
13361
  } catch (writeErr) {
13027
13362
  // #2760 CR4 finding 1 — write failure must be loud and fatal. Wrap
13028
13363
  // with a `post-write` prefix the outer catch recognises so install
@@ -13628,10 +13963,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13628
13963
  // bare basename so an unrelated user command that merely mentions the same
13629
13964
  // filename can't false-positive into GSD's validated set.
13630
13965
  configuredEntrypoints.push(
13631
- ...settingsEntrypoints.filter(entry => {
13632
- const hooksSegment = '/hooks/' + path.basename(entry.scriptPath);
13633
- return registeredHookCommands.some(command => command.includes(hooksSegment));
13634
- }),
13966
+ ...selectConfiguredEntrypointsForValidation(settingsEntrypoints, registeredHookCommands),
13635
13967
  );
13636
13968
  const statuslineEntrypoints = settingsEntrypoints.filter(entry => entry.command === statuslineCommand);
13637
13969
  const updateBannerEntrypoints = settingsEntrypoints.filter(entry => entry.command === updateBannerCommand);
@@ -13727,6 +14059,25 @@ function describeEntrypointConsequence(invalidRuntime) {
13727
14059
  return 'not persisted: this runtime writes its config after this check';
13728
14060
  }
13729
14061
 
14062
+ // #5100: select which tracked entrypoints reach the validation gate. A named
14063
+ // export (see module.exports) so tests can drive the fresh-vs-upgrade split
14064
+ // directly; the install() call site is covered by a child-process test that
14065
+ // runs the installer with win32 emulated.
14066
+ function selectConfiguredEntrypointsForValidation(settingsEntrypoints, registeredHookCommands) {
14067
+ return (settingsEntrypoints || []).filter(entry => {
14068
+ // A command-less entry never produced a registered command, so the
14069
+ // segment match below can never keep it — but it is exactly the
14070
+ // unresolved-bash entry the gate must see (buildHookCommand pushes it
14071
+ // without a `command` when no Git Bash is found). Exempt it: without this
14072
+ // a fresh install silently drops it (gate passes) while an upgrade keeps
14073
+ // it via the persisted command (gate fails) — same host, opposite
14074
+ // verdicts. Surface it in both cases and let the gate decide loudly.
14075
+ if (entry.command === undefined) return true;
14076
+ const hooksSegment = '/hooks/' + path.basename(entry.scriptPath);
14077
+ return registeredHookCommands.some(command => command.includes(hooksSegment));
14078
+ });
14079
+ }
14080
+
13730
14081
  function assertConfiguredEntrypoints(entries) {
13731
14082
  // #4249: some writers push the same (configPath, scriptPath) pair more than
13732
14083
  // once (e.g. Kimi's context-monitor hook registered under several events,
@@ -14913,6 +15264,8 @@ module.exports = {
14913
15264
  install,
14914
15265
  installAllRuntimes,
14915
15266
  uninstall,
15267
+ // #5100 — installer entrypoint-selection seam, exported for direct tests
15268
+ selectConfiguredEntrypointsForValidation,
14916
15269
  // #3023 — shared hook bundle directory name, descriptor-driven
14917
15270
  SHARED_HOOKS_DIR_DEFAULT,
14918
15271
  resolveSharedHooksDirName,
@@ -14978,6 +15331,8 @@ module.exports = {
14978
15331
  convertClaudeAgentToClineAgent,
14979
15332
  // #2284 — Hermes named-dispatch → delegate_task projection
14980
15333
  convertClaudeToHermesMarkdown,
15334
+ convertClaudeToPiMarkdown,
15335
+ PI_DISPATCH_TOOL_CONFIG,
14981
15336
  projectNamedDispatchToStructuralDelegate,
14982
15337
  _hostIntegrationDispatch,
14983
15338
  _resolveAvailableGsdRoles,