@opengsd/gsd-core 1.9.0 → 1.10.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 (223) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +131 -34
  5. package/agents/gsd-debugger.md +12 -246
  6. package/agents/gsd-executor.md +7 -5
  7. package/agents/gsd-integration-checker.md +3 -0
  8. package/agents/gsd-plan-checker.md +9 -0
  9. package/agents/gsd-planner.md +5 -8
  10. package/agents/gsd-roadmapper.md +21 -3
  11. package/agents/gsd-verifier.md +14 -70
  12. package/bin/install.js +503 -341
  13. package/commands/gsd/mempalace-capture.md +1 -1
  14. package/commands/gsd/new-milestone.md +1 -1
  15. package/commands/gsd/plan-phase.md +1 -1
  16. package/gsd-core/bin/gsd-tools.cjs +607 -63
  17. package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
  18. package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
  19. package/gsd-core/bin/lib/api-coverage.cjs +120 -0
  20. package/gsd-core/bin/lib/audit.cjs +89 -1
  21. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  22. package/gsd-core/bin/lib/capability-registry.cjs +96 -110
  23. package/gsd-core/bin/lib/capability-validator.cjs +12 -2
  24. package/gsd-core/bin/lib/check-command-router.cjs +43 -1
  25. package/gsd-core/bin/lib/command-aliases.cjs +72 -0
  26. package/gsd-core/bin/lib/commands.cjs +26 -25
  27. package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
  28. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  29. package/gsd-core/bin/lib/config.cjs +12 -1
  30. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  31. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  32. package/gsd-core/bin/lib/core-utils.cjs +91 -12
  33. package/gsd-core/bin/lib/docs.cjs +3 -2
  34. package/gsd-core/bin/lib/external-job.cjs +19 -4
  35. package/gsd-core/bin/lib/frontmatter.cjs +84 -12
  36. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  37. package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
  38. package/gsd-core/bin/lib/graphify.cjs +142 -27
  39. package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
  40. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  41. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  42. package/gsd-core/bin/lib/init.cjs +1021 -57
  43. package/gsd-core/bin/lib/install-engine.cjs +64 -10
  44. package/gsd-core/bin/lib/install-profiles.cjs +27 -1
  45. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  47. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  48. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  49. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  50. package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
  51. package/gsd-core/bin/lib/io.cjs +28 -3
  52. package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
  53. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  54. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  55. package/gsd-core/bin/lib/milestone.cjs +106 -51
  56. package/gsd-core/bin/lib/phase-id.cjs +63 -0
  57. package/gsd-core/bin/lib/phase-locator.cjs +138 -45
  58. package/gsd-core/bin/lib/phase.cjs +260 -25
  59. package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
  60. package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
  61. package/gsd-core/bin/lib/project-root.cjs +48 -0
  62. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  63. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
  64. package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
  65. package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
  66. package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
  67. package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
  68. package/gsd-core/bin/lib/roadmap.cjs +37 -7
  69. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
  70. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
  71. package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
  72. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
  73. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  74. package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
  75. package/gsd-core/bin/lib/smart-entry.cjs +12 -0
  76. package/gsd-core/bin/lib/state-transition.cjs +73 -8
  77. package/gsd-core/bin/lib/state.cjs +151 -62
  78. package/gsd-core/bin/lib/surface.cjs +12 -1
  79. package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
  80. package/gsd-core/bin/lib/uat.cjs +320 -21
  81. package/gsd-core/bin/lib/unusable-input.cjs +9 -0
  82. package/gsd-core/bin/lib/verification.cjs +29 -12
  83. package/gsd-core/bin/lib/verify.cjs +29 -5
  84. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  85. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
  86. package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
  87. package/gsd-core/bin/lib/workstream.cjs +6 -0
  88. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  89. package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
  90. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  91. package/gsd-core/references/artifact-types.md +10 -3
  92. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  93. package/gsd-core/references/debugger-techniques.md +255 -0
  94. package/gsd-core/references/research-documentation-lookup.md +5 -3
  95. package/gsd-core/references/specless-probe-fallback.md +7 -6
  96. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  97. package/gsd-core/references/worktree-branch-check.md +2 -2
  98. package/gsd-core/templates/summary-complex.md +2 -0
  99. package/gsd-core/templates/summary-minimal.md +2 -0
  100. package/gsd-core/templates/summary-standard.md +2 -0
  101. package/gsd-core/templates/summary.md +2 -0
  102. package/gsd-core/workflows/audit-milestone.md +3 -0
  103. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  104. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  105. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  106. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  107. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  108. package/gsd-core/workflows/autonomous.md +32 -69
  109. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  110. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
  111. package/gsd-core/workflows/code-review.md +42 -145
  112. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  113. package/gsd-core/workflows/complete-milestone.md +23 -81
  114. package/gsd-core/workflows/debug.md +9 -12
  115. package/gsd-core/workflows/diagnose-issues.md +22 -0
  116. package/gsd-core/workflows/discovery-phase.md +4 -4
  117. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  118. package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
  119. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  120. package/gsd-core/workflows/docs-update.md +8 -51
  121. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
  122. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  123. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  124. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  126. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  127. package/gsd-core/workflows/execute-phase.md +65 -137
  128. package/gsd-core/workflows/execute-plan.md +1 -1
  129. package/gsd-core/workflows/help/modes/full.md +6 -1
  130. package/gsd-core/workflows/ingest-docs.md +2 -1
  131. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  132. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  133. package/gsd-core/workflows/new-milestone.md +21 -38
  134. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  135. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  136. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  137. package/gsd-core/workflows/new-project.md +13 -226
  138. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  139. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  140. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  141. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  142. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  143. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  144. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  145. package/gsd-core/workflows/plan-phase.md +49 -193
  146. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  147. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  148. package/gsd-core/workflows/progress.md +11 -153
  149. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  150. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  151. package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
  152. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  153. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  154. package/gsd-core/workflows/quick.md +20 -390
  155. package/gsd-core/workflows/resume-project.md +3 -0
  156. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  157. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  158. package/gsd-core/workflows/review.md +15 -8
  159. package/gsd-core/workflows/section-manifest.json +219 -0
  160. package/gsd-core/workflows/sketch.md +1 -1
  161. package/gsd-core/workflows/spec-phase.md +17 -14
  162. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  163. package/gsd-core/workflows/spike.md +50 -16
  164. package/gsd-core/workflows/sync-skills.md +49 -11
  165. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  166. package/gsd-core/workflows/transition.md +8 -21
  167. package/gsd-core/workflows/ui-phase.md +8 -7
  168. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  169. package/gsd-core/workflows/update.md +18 -7
  170. package/gsd-core/workflows/verify-phase.md +4 -7
  171. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  172. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  173. package/gsd-core/workflows/verify-work.md +8 -58
  174. package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
  175. package/hooks/dist/gsd-check-update-worker.js +14 -5
  176. package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
  177. package/hooks/dist/gsd-read-injection-scanner.js +7 -0
  178. package/hooks/dist/gsd-statusline.js +72 -6
  179. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  180. package/hooks/dist/gsd-write-guard.js +359 -0
  181. package/hooks/dist/lib/isolation-sentinel.js +268 -0
  182. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  183. package/hooks/gsd-agent-isolation-guard.js +428 -0
  184. package/hooks/gsd-check-update-worker.js +14 -5
  185. package/hooks/gsd-cursor-subagent-start.js +532 -26
  186. package/hooks/gsd-read-injection-scanner.js +7 -0
  187. package/hooks/gsd-statusline.js +72 -6
  188. package/hooks/gsd-worktree-path-guard.js +2 -1
  189. package/hooks/gsd-write-guard.js +359 -0
  190. package/hooks/hooks.json +12 -0
  191. package/hooks/lib/isolation-sentinel.js +268 -0
  192. package/hooks/managed-hooks-registry.cjs +2 -0
  193. package/package.json +14 -5
  194. package/pi/gsd.cjs +57 -12
  195. package/scripts/build-hooks.js +9 -0
  196. package/scripts/changeset/lint.cjs +9 -2
  197. package/scripts/changeset/serialize.cjs +5 -1
  198. package/scripts/gen-capability-matrix.cjs +1 -1
  199. package/scripts/gen-context-index.cjs +448 -0
  200. package/scripts/gen-inventory-manifest.cjs +101 -1
  201. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  202. package/scripts/gen-registry.cjs +39 -15
  203. package/scripts/gen-section-manifest.cjs +638 -0
  204. package/scripts/generate-package-identity.cjs +4 -2
  205. package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
  206. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  207. package/scripts/lint-docs-command-form.cjs +195 -0
  208. package/scripts/lint-docs-required.cjs +9 -1
  209. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  210. package/scripts/lint-example-parser-parity.cjs +395 -0
  211. package/scripts/lint-test-file-count.allowlist.json +27 -1
  212. package/scripts/mutation-matrix.cjs +13 -0
  213. package/scripts/prompt-injection-scan.sh +27 -6
  214. package/scripts/registry-schema.cjs +323 -94
  215. package/scripts/run-tests.cjs +3 -2
  216. package/scripts/validate-registry.cjs +10 -6
  217. package/skills/gsd-autonomous/SKILL.md +1 -1
  218. package/skills/gsd-execute-phase/SKILL.md +1 -1
  219. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  220. package/skills/gsd-new-milestone/SKILL.md +1 -1
  221. package/skills/gsd-plan-phase/SKILL.md +2 -2
  222. package/vscode/package.json +1 -1
  223. package/scripts/gen-emitted-baseline.cjs +0 -145
@@ -28,8 +28,10 @@ const runtimeArtifactInstallPlan = require("./runtime-artifact-install-plan.cjs"
28
28
  const runtimeNamePolicy = require("./runtime-name-policy.cjs");
29
29
  const installProfiles = require("./install-profiles.cjs");
30
30
  const installerMigrations = require("./installer-migrations.cjs");
31
+ const retiredArtifactCleanup = require("./retired-artifact-cleanup.cjs");
31
32
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
32
33
  const external_descriptor_trust_cjs_1 = require("./external-descriptor-trust.cjs");
34
+ const commonjs_marker_cjs_1 = require("./commonjs-marker.cjs");
33
35
  const { processAttribution } = runtimeArtifactConversion;
34
36
  // resolveRuntimeArtifactLayout: accessed via module ref (not destructured) so
35
37
  // test stubs that monkeypatch the module's exports are seen at call time.
@@ -315,13 +317,25 @@ function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = '
315
317
  if (!saved || !saved.has('dev-preferences.md'))
316
318
  return false;
317
319
  let skillDir;
320
+ // #2911: the actual install root the skill dir resolves under — defaults to
321
+ // targetDir, but a skills-kind `home` override (e.g. Codex -> $HOME/.agents)
322
+ // moves it entirely outside targetDir. Every confinement/guard check below
323
+ // must confine against installRoot, not targetDir, or it would flag the
324
+ // legitimate override destination as an escape.
325
+ let installRoot = targetDir;
318
326
  if (runtime) {
319
327
  const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, targetDir, scope);
320
328
  const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills');
321
329
  if (!skillsKindEntry)
322
330
  return false; // runtime has no skills layout at this scope (e.g. cline local)
323
331
  const stemName = skillsKindEntry.prefix === '' ? 'dev-preferences' : 'gsd-dev-preferences';
324
- skillDir = node_path_1.default.join(runtimeArtifactInstallPlan.assertDestWithinConfigHome(targetDir, skillsKindEntry.destSubpath), stemName);
332
+ // #2911: same destination-root defect as _copyStaged/applySurface — honor
333
+ // skillsKindEntry.home as a FALLBACK-preferred override (e.g. Codex skills
334
+ // -> $HOME/.agents) instead of always resolving against targetDir, so a
335
+ // legacy dev-preferences migration lands in the SAME tree the installer
336
+ // and surface-apply use. Runtimes with no `home` override are unaffected.
337
+ installRoot = skillsKindEntry.home ?? targetDir;
338
+ skillDir = node_path_1.default.join(runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath), stemName);
325
339
  }
326
340
  else {
327
341
  // Legacy fallback for callers that have not yet been updated to pass runtime
@@ -330,11 +344,11 @@ function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = '
330
344
  const skillFile = node_path_1.default.join(skillDir, 'SKILL.md');
331
345
  if (node_fs_1.default.existsSync(skillFile))
332
346
  return false;
333
- // Symlink-escape guard: reject if any path component between targetDir and
334
- // skillDir is a symlink that would redirect writes outside the config root.
347
+ // Symlink-escape guard: reject if any path component between installRoot and
348
+ // skillDir is a symlink that would redirect writes outside the install root.
335
349
  // #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
336
- if (hasExistingSymlinkBetween(node_path_1.default.resolve(targetDir), skillDir, { allowOptInFollow: isSymlinkedDestOptIn() })) {
337
- throw new Error(`migrateLegacyDevPreferencesToSkill: skillDir "${skillDir}" contains a symlink the install root "${targetDir}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`);
350
+ if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), skillDir, { allowOptInFollow: isSymlinkedDestOptIn() })) {
351
+ throw new Error(`migrateLegacyDevPreferencesToSkill: skillDir "${skillDir}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`);
338
352
  }
339
353
  try {
340
354
  node_fs_1.default.mkdirSync(skillDir, { recursive: true });
@@ -675,6 +689,10 @@ function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
675
689
  * (fail closed), matching the layout resolver's own optional-registry contract.
676
690
  */
677
691
  function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined, capabilityRegistry) {
692
+ // A removed descriptor kind is no longer visited by the layout loop, so it
693
+ // cannot prune its own previous output. Clean manifest-proven retired files
694
+ // before materializing the current layout (#2644).
695
+ retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(runtime, configDir);
678
696
  // Combined-family runtimes (OpenCode/Kilo, ADR-1239 / #2087): route through
679
697
  // the dedicated combined commands+skills+plugin orchestrator instead of the
680
698
  // generic layout-driven loop below, mirroring the bespoke install path that
@@ -849,12 +867,19 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
849
867
  if (!converter) {
850
868
  throw new TypeError(`installOpencodeFamilySkills: unknown skills converter '${String(converterName)}' for runtime '${runtime}'`);
851
869
  }
852
- const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(targetDir, skillsKindEntry.destSubpath);
853
- // Symlink-escape guard: reject if any path component between targetDir and
854
- // dest is a symlink that would redirect writes outside the config root.
870
+ // #2911: same destination-root defect as _copyStaged/migrateLegacyDevPreferencesToSkill
871
+ // — honor skillsKindEntry.home as a FALLBACK-preferred override (e.g. Codex skills
872
+ // -> $HOME/.agents) instead of always resolving against targetDir, so this bespoke
873
+ // OpenCode/Kilo writer lands in the SAME tree the installer and surface-apply use.
874
+ // Runtimes with no `home` override (opencode, kilo today) are unaffected. Must stay
875
+ // in lockstep with the sibling writers — the destination-parity test enforces it.
876
+ const installRoot = skillsKindEntry.home ?? targetDir;
877
+ const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath);
878
+ // Symlink-escape guard: reject if any path component between installRoot and
879
+ // dest is a symlink that would redirect writes outside the install root.
855
880
  // #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
856
- if (hasExistingSymlinkBetween(node_path_1.default.resolve(targetDir), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
857
- throw new Error(`installOpencodeFamilySkills: destDir "${dest}" contains a symlink the install root "${targetDir}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`);
881
+ if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
882
+ throw new Error(`installOpencodeFamilySkills: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`);
858
883
  }
859
884
  node_fs_1.default.mkdirSync(dest, { recursive: true });
860
885
  // Preserve user-owned GSD-prefixed skill dirs across the gsd-* prune.
@@ -1037,6 +1062,30 @@ function _installNativePluginIfDeclared(runtime, configDir, behaviors, src) {
1037
1062
  const destPath = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, node_path_1.default.join(np.dir, np.file));
1038
1063
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(destPath), { recursive: true });
1039
1064
  node_fs_1.default.copyFileSync(pluginSrc, destPath);
1065
+ // #2544: the staged adapter is a `.js` file, so Node decides its module
1066
+ // type by walking up for the nearest package.json. It used to find the
1067
+ // marker the installer wrote at the config root — the write that
1068
+ // clobbered user-authored files. Pin it from the plugin's own directory
1069
+ // instead, leaving the config root alone. The marker cannot disturb
1070
+ // plugin discovery: OpenCode auto-discovers `plugins/*.{ts,js}` and pi's
1071
+ // isExtensionFile() accepts only `.ts`/`.js` (see installer-migration
1072
+ // 006), so a package.json here is never treated as a plugin. Never
1073
+ // written over a package.json GSD does not own — but when one is already
1074
+ // there, say so: the adapter is CommonJS and will not load under a
1075
+ // foreign `"type": "module"`, and a silent no-op would leave every guard
1076
+ // the adapter spawns dead with no diagnostic (the #2305 failure shape).
1077
+ const markerOutcome = (0, commonjs_marker_cjs_1.ensureCommonJsMarker)(node_path_1.default.dirname(destPath));
1078
+ if (markerOutcome === 'preserved-foreign') {
1079
+ console.warn(` ⚠ ${np.dir}/package.json is not GSD's CommonJS marker — left untouched. `
1080
+ + `If it declares "type": "module", ${np.file} will not load.`);
1081
+ }
1082
+ else if (markerOutcome === 'failed') {
1083
+ // Best-effort, never fatal: an unwritable plugin dir must not abort the
1084
+ // install. Same warn-and-continue posture as the foreign-marker branch —
1085
+ // the adapter is staged either way, it just may not resolve as CommonJS.
1086
+ console.warn(` ⚠ Could not write ${np.dir}/package.json (CommonJS marker) — install continued. `
1087
+ + `If the config root declares "type": "module", ${np.file} will not load.`);
1088
+ }
1040
1089
  }
1041
1090
  }
1042
1091
  }
@@ -1180,6 +1229,11 @@ function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfi
1180
1229
  * @param scope
1181
1230
  */
1182
1231
  function uninstallRuntimeArtifacts(runtime, configDir, scope) {
1232
+ // A retired descriptor kind is absent from the current uninstall plan, just
1233
+ // as it is absent from the install plan. Sweep manifest-proven output from
1234
+ // retired kinds before removing the current layout so a direct uninstall
1235
+ // cannot leave stale runtime surfaces behind (#2644).
1236
+ retiredArtifactCleanup.pruneRetiredRuntimeArtifacts(runtime, configDir);
1183
1237
  // Legacy cleanup before layout-driven removal (scope-aware to avoid
1184
1238
  // removing Claude local commands/gsd/ which is the primary install dir).
1185
1239
  // Returns saved user artifacts so we can migrate AFTER layout removal
@@ -20,6 +20,13 @@ const external_descriptor_trust_cjs_1 = require("./external-descriptor-trust.cjs
20
20
  // eslint-disable-next-line @typescript-eslint/no-require-imports
21
21
  const conversionModule = require("./runtime-artifact-conversion.cjs");
22
22
  const { applyAgentPathRewrites: _applyAgentPathRewrites, processAttribution: _processAttribution, normalizeAgentBodyForRuntime: _normalizeAgentBodyForRuntime, readGsdCommandNames: _readGsdCommandNames, } = conversionModule;
23
+ // #2995 (epic #1671 Phase 6.4): agent bodies join the fragment model. Markers are
24
+ // stripped at emit BEFORE any path rewrite or converter runs, so a `.claude/` ->
25
+ // `.windsurf/` regex can never reach inside a marker attribute and corrupt it —
26
+ // the same ordering #2930 established for workflows.
27
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
28
+ const workflowFragmentsModule = require("./workflow-fragments.cjs");
29
+ const { composeWorkflow: _composeWorkflow } = workflowFragmentsModule;
23
30
  // ---------------------------------------------------------------------------
24
31
  // Profile definitions
25
32
  // ---------------------------------------------------------------------------
@@ -329,6 +336,19 @@ function stageSkillsForProfile(srcDir, resolvedProfile) {
329
336
  * For tiered profiles, copies only agents whose full stem (e.g. 'gsd-planner')
330
337
  * is in resolvedProfile.agents — which is populated by resolveProfile() from
331
338
  * the _calls_agents_* entries in the manifest.
339
+ *
340
+ * ⚠️ RAW STAGER — ITS OUTPUT IS NOT EMISSION-READY (#2995). This stager performs a
341
+ * plain `fs.copyFileSync` and — under the default `full` profile — short-circuits
342
+ * and returns the real source directory unstaged. It does NOT strip `gsd:section`
343
+ * markers. It is still called, by `bin/install.js`'s `_stageAgents`, whose output
344
+ * feeds the inline agent loop and `installCodexConfig`; both of those compose the
345
+ * content themselves before writing, so the raw output never reaches disk. What
346
+ * changed in #2995 is that `agentsKind` and `kimiAgentsKind` no longer use it —
347
+ * they route through `stageAgentsForRuntimeWithConverter`, which composes.
348
+ *
349
+ * The invariant to preserve: anything that takes this function's output and WRITES
350
+ * it as a runtime artifact must call `composeWorkflow` on each file first, or it
351
+ * ships markers verbatim.
332
352
  */
333
353
  function stageAgentsForProfile(srcAgentsDir, resolvedProfile) {
334
354
  if (resolvedProfile.skills === '*')
@@ -796,7 +816,13 @@ function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, conve
796
816
  continue;
797
817
  }
798
818
  }
799
- let content = node_fs_1.default.readFileSync(node_path_1.default.join(srcAgentsDir, entry.name), 'utf8');
819
+ const agentSourcePath = node_path_1.default.join(srcAgentsDir, entry.name);
820
+ let content = node_fs_1.default.readFileSync(agentSourcePath, 'utf8');
821
+ // #2995: strip gsd:section markers FIRST — before path rewrites, attribution,
822
+ // and the per-runtime converter. Byte-identical (no-op) for an unmarked agent;
823
+ // throws loudly naming the file for a malformed marker, never emitting a
824
+ // half-composed agent.
825
+ content = _composeWorkflow(content, { sourcePath: agentSourcePath });
800
826
  if (agentCtx) {
801
827
  // ADR-1235 §1: pre-converter cross-cutting (matches inline loop order exactly)
802
828
  // Step 1: path rewrites (4 base ~/.claude/ regexes; skipped for copilot/antigravity)
@@ -109,7 +109,9 @@ function validateInstallerMigrationActions(actions, migration) {
109
109
  // Ownership and runtime-contract evidence are required by
110
110
  // docs/installer-migrations.md#action-types and
111
111
  // docs/adr/0008-installer-migration-module.md#runtime-contract-decision.
112
- if (actType === 'remove-managed' || actType === 'rewrite-json') {
112
+ // `remove-empty-dir` carries the same evidence bar as `remove-managed`: it is
113
+ // still a destructive removal, just of a directory node instead of a file.
114
+ if (actType === 'remove-managed' || actType === 'rewrite-json' || actType === 'remove-empty-dir') {
113
115
  requireActionEvidence(act, 'ownershipEvidence', migration);
114
116
  }
115
117
  if (actType === 'rewrite-json') {
@@ -32,6 +32,7 @@ const VALID_CHOICES = ['keep', 'remove'];
32
32
  // on-disk `hooks/` directory in both directions: whitelist-but-missing
33
33
  // AND shipped-but-not-whitelisted both fail CI.
34
34
  exports.BUNDLED_GSD_HOOK_FILES = Object.freeze(new Set([
35
+ 'hooks/gsd-agent-isolation-guard.js',
35
36
  'hooks/gsd-check-update-worker.js',
36
37
  'hooks/gsd-check-update.js',
37
38
  'hooks/gsd-config-reload.js',
@@ -57,6 +58,7 @@ exports.BUNDLED_GSD_HOOK_FILES = Object.freeze(new Set([
57
58
  'hooks/gsd-validate-commit.sh',
58
59
  'hooks/gsd-workflow-guard.js',
59
60
  'hooks/gsd-worktree-path-guard.js',
61
+ 'hooks/gsd-write-guard.js',
60
62
  ]));
61
63
  // ── Internal helpers ──────────────────────────────────────────────────────────
62
64
  function installerMigrationActionLabel(action) {
@@ -74,6 +76,8 @@ function installerMigrationActionLabel(action) {
74
76
  return 'preserved';
75
77
  if (action.type === 'preserve-user')
76
78
  return 'preserved';
79
+ if (action.type === 'remove-empty-dir')
80
+ return 'removed';
77
81
  if (action.type === 'prompt-user')
78
82
  return 'blocked';
79
83
  return 'skipped';
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ /**
3
+ * Installer migration: retire the config-root `{"type":"commonjs"}` marker that
4
+ * pre-#2544 installs wrote over `<configRoot>/package.json`.
5
+ *
6
+ * What old artifact is being retired?
7
+ * `package.json` at the runtime config root. Before #2544,
8
+ * `installSharedHooksBundle(destRootDir)` wrote `{"type":"commonjs"}` there
9
+ * unconditionally on every install and every re-install, to pin GSD's staged
10
+ * `.js` hook scripts to CommonJS via Node's ancestor walk. #2544 moved that
11
+ * marker into the directories GSD actually fills (`hooks/`, and the
12
+ * `nativePlugin.dir` for runtimes declaring one) and stopped writing the
13
+ * config root at all. Without this migration an upgrader keeps BOTH markers:
14
+ * the new one under `hooks/` and the stale one at the root, so the config
15
+ * root stays pinned to CommonJS and the PR's own claim — that GSD no longer
16
+ * writes the shared config root — is false for every install made since the
17
+ * marker was introduced, until the user uninstalls.
18
+ *
19
+ * How do we prove it is GSD-owned?
20
+ * By exact content match, NOT by the manifest. The config-root marker was
21
+ * never recorded in `gsd-file-manifest.json` — `writeManifest()` records
22
+ * `hooks/`, `agents/`, `commands/`, `scripts/`, and the native plugin, and
23
+ * has never had a `manifest.files['package.json']` entry — so
24
+ * `classifyArtifact('package.json')` answers `unknown` and the planner's
25
+ * own guard would downgrade a `remove-managed` to `preserve-user`.
26
+ * This migration therefore supplies the "purpose-built detector for an old
27
+ * GSD-owned shape" that `docs/installer-migrations.md#remove-managed`
28
+ * sanctions, and declares the resulting classification on the action: the
29
+ * file is removed only when its bytes are exactly the marker GSD writes
30
+ * (`{"type":"commonjs"}`, trailing whitespace tolerated). That is the same
31
+ * predicate `removeCommonJsMarker` has always used on the uninstall side, so
32
+ * install, uninstall, and migration cannot drift apart.
33
+ *
34
+ * What happens if the user modified it?
35
+ * Then it is not the marker, and this migration does not touch it. Any
36
+ * `package.json` carrying a `name`, `dependencies`, `scripts`, or any key
37
+ * beyond the single `type` — i.e. every file the #2544 defect destroyed —
38
+ * fails the exact-content test and is left exactly as found. There is
39
+ * deliberately no `backup-and-remove` branch: a modified file here is not a
40
+ * patched GSD artifact, it is somebody else's file.
41
+ *
42
+ * What happens if it is missing?
43
+ * No actions. Fresh post-#2544 installs never wrote it, already-migrated
44
+ * installs no longer have it, and the executor additionally journals a
45
+ * `missing` outcome if it disappears between plan and apply. Idempotent.
46
+ *
47
+ * What runtime and scope does it affect?
48
+ * Every runtime whose config root received the marker — i.e. every runtime
49
+ * not excluded from `installSharedHooksBundle(targetDir)` by
50
+ * `hostBehaviors.skipSharedHooksInstall` and not Codex: antigravity,
51
+ * augment, claude, claude-local, codebuddy, hermes, qwen, kilo, opencode,
52
+ * and pi. The `runtimes` field is OMITTED — the framework's "all runtimes" —
53
+ * rather than carrying that hand-list: a runtime that never received the
54
+ * marker simply has no file to match, so enumerating them would add a second
55
+ * place for the set to drift out of date without changing behavior. Note it
56
+ * must be omitted and not `[]`; see the field's own comment below.
57
+ *
58
+ * ONE DELIBERATE CARVE-OUT — kimi. Kimi's marker was written to its native
59
+ * hook root (`~/.kimi`, `resolveKimiHooksTomlDir`), which is NOT under
60
+ * kimi's `configDir` (its generic Agent-Skills root). Migration relPaths are
61
+ * structurally confined to `configDir` (`validateSafeRelPath` /
62
+ * `ensureInsideConfig`), so this framework cannot address that path at all.
63
+ * Kimi's stale root marker is retired by the installer instead, at the same
64
+ * call site that writes its replacement — see the `kimi-hooks-toml` branch
65
+ * in `bin/install.js`. Named here so the gap is not mistaken for an
66
+ * oversight.
67
+ *
68
+ * Is the action safe in non-interactive install?
69
+ * Yes. `remove-managed` is non-interactive and journaled, the executor takes
70
+ * a rollback snapshot before unlinking, and no branch of this migration can
71
+ * emit `prompt-user`. A file that is not byte-identical to GSD's marker
72
+ * produces no action at all.
73
+ *
74
+ * See docs/installer-migrations.md#shipped-migrations and #action-types.
75
+ */
76
+ var __importDefault = (this && this.__importDefault) || function (mod) {
77
+ return (mod && mod.__esModule) ? mod : { "default": mod };
78
+ };
79
+ const node_fs_1 = __importDefault(require("node:fs"));
80
+ const node_path_1 = __importDefault(require("node:path"));
81
+ const node_crypto_1 = __importDefault(require("node:crypto"));
82
+ /** The config-root path the pre-#2544 installer wrote, relative to configDir. */
83
+ const STALE_ROOT_MARKER = 'package.json';
84
+ /**
85
+ * The exact marker content GSD wrote. Duplicated as a literal rather than
86
+ * imported from `src/commonjs-marker.cts` on purpose: a migration record is a
87
+ * frozen historical statement about what a PAST version installed, and it must
88
+ * keep matching those bytes even if the live module's constant is ever changed.
89
+ * Importing would silently re-point this detector at a future value.
90
+ */
91
+ const LEGACY_MARKER_CONTENT = '{"type":"commonjs"}';
92
+ const OWNERSHIP_EVIDENCE = 'file content is byte-identical to the {"type":"commonjs"} marker pre-#2544 installs '
93
+ + 'wrote at the config root (installSharedHooksBundle); same exact-content predicate '
94
+ + 'removeCommonJsMarker uses on uninstall. Never manifest-recorded, so this is the '
95
+ + 'purpose-built detector permitted by docs/installer-migrations.md#remove-managed';
96
+ const REASON = 'superseded by the hooks/ and plugin-dir markers (#2544); leaving it pins the shared '
97
+ + 'config root to CommonJS and keeps GSD occupying a file it no longer writes';
98
+ const migration = {
99
+ id: '2026-07-28-retire-config-root-commonjs-marker',
100
+ title: 'Retire the pre-#2544 config-root CommonJS marker',
101
+ description: 'Remove <configRoot>/package.json when it is exactly the {"type":"commonjs"} marker '
102
+ + 'pre-#2544 installs wrote there, now superseded by markers scoped to the directories '
103
+ + 'GSD owns. A package.json with any other content is left untouched.',
104
+ introducedIn: '1.8.0',
105
+ scopes: ['global', 'local'],
106
+ destructive: true,
107
+ plan: (ctx) => {
108
+ const markerPath = node_path_1.default.join(ctx.configDir, STALE_ROOT_MARKER);
109
+ let stat;
110
+ try {
111
+ // lstat, not existsSync: existsSync follows symlinks and reports false for
112
+ // a dangling one. A symlink here is not something GSD wrote, and removing
113
+ // it is never ours to do — mirrors classifyMarker's fail-closed posture.
114
+ stat = node_fs_1.default.lstatSync(markerPath);
115
+ }
116
+ catch {
117
+ return [];
118
+ }
119
+ if (!stat.isFile())
120
+ return [];
121
+ let content;
122
+ try {
123
+ content = node_fs_1.default.readFileSync(markerPath, 'utf8');
124
+ }
125
+ catch {
126
+ // Present but unreadable never downgrades to the permissive answer.
127
+ return [];
128
+ }
129
+ if (content.trim() !== LEGACY_MARKER_CONTENT)
130
+ return [];
131
+ const hash = node_crypto_1.default.createHash('sha256').update(content).digest('hex');
132
+ return [
133
+ {
134
+ type: 'remove-managed',
135
+ relPath: STALE_ROOT_MARKER,
136
+ reason: REASON,
137
+ ownershipEvidence: OWNERSHIP_EVIDENCE,
138
+ // Declared, not derived: classifyArtifact answers 'unknown' for this
139
+ // never-manifested path, and the planner downgrades a remove-managed on
140
+ // an 'unknown' classification to preserve-user. The exact-content match
141
+ // above IS the ownership proof, so the classification is stated here.
142
+ classification: 'managed-pristine',
143
+ originalHash: hash,
144
+ currentHash: hash,
145
+ },
146
+ ];
147
+ },
148
+ };
149
+ module.exports = migration;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ /**
3
+ * Installer migration: retire Cursor's duplicate commands/ surface (#2644).
4
+ *
5
+ * Cursor discovers GSD skills as slash-menu entries while also keeping them
6
+ * model-invocable. Older GSD releases installed the same workflows again as
7
+ * commands/gsd-*.md, so every action appeared twice. The skills remain the
8
+ * sole workflow surface; this migration removes only old command files proven
9
+ * managed by gsd-file-manifest.json. Modified files are backed up first and
10
+ * unmanifested files are preserved.
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ const node_fs_1 = __importDefault(require("node:fs"));
16
+ const node_path_1 = __importDefault(require("node:path"));
17
+ const COMMANDS_DIR = 'commands';
18
+ const REASON = 'Cursor exposes skills directly in the slash menu; the parallel command file duplicated the same GSD action (#2644)';
19
+ const OWNERSHIP_EVIDENCE = 'pre-#2644 Cursor installs record commands/gsd-*.md in gsd-file-manifest.json';
20
+ const migration = {
21
+ id: '2026-07-29-cursor-retire-commands-surface',
22
+ title: 'Retire Cursor duplicate commands surface',
23
+ description: 'Remove manifest-managed Cursor commands/gsd-*.md files now that skills are the single slash-menu and model-invocation surface.',
24
+ introducedIn: '1.8.1',
25
+ runtimes: ['cursor'],
26
+ scopes: ['global', 'local'],
27
+ destructive: true,
28
+ plan: (ctx) => {
29
+ const commandsDir = node_path_1.default.join(ctx.configDir, COMMANDS_DIR);
30
+ let entries;
31
+ try {
32
+ if (!node_fs_1.default.existsSync(commandsDir) || node_fs_1.default.lstatSync(commandsDir).isSymbolicLink())
33
+ return [];
34
+ entries = node_fs_1.default.readdirSync(commandsDir, { withFileTypes: true });
35
+ }
36
+ catch {
37
+ return [];
38
+ }
39
+ const actions = [];
40
+ for (const entry of entries) {
41
+ if (!entry.isFile() || !entry.name.startsWith('gsd-') || !entry.name.endsWith('.md'))
42
+ continue;
43
+ const relPath = node_path_1.default.posix.join(COMMANDS_DIR, entry.name);
44
+ const artifact = ctx.classifyArtifact(relPath);
45
+ if (artifact.classification === 'managed-pristine') {
46
+ actions.push({ type: 'remove-managed', relPath, reason: REASON, ownershipEvidence: OWNERSHIP_EVIDENCE });
47
+ }
48
+ else if (artifact.classification === 'managed-modified') {
49
+ actions.push({ type: 'backup-and-remove', relPath, reason: REASON, ownershipEvidence: OWNERSHIP_EVIDENCE });
50
+ }
51
+ }
52
+ return actions;
53
+ },
54
+ };
55
+ module.exports = migration;
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ /**
3
+ * Installer migration: retire pi's legacy `<piConfigDir>/hooks/` directory
4
+ * after GSD's shared hook bundle moved to `<piConfigDir>/gsd-hooks/` (#3023).
5
+ *
6
+ * What old artifact is being retired?
7
+ * `hooks/` (and its `hooks/lib/` subdirectory) at the pi config root. pi
8
+ * reserves that exact name as its own deprecated extension directory and
9
+ * warns on every startup whenever it exists — pi's
10
+ * `checkDeprecatedExtensionDirs()` fires on mere PATH EXISTENCE, not on the
11
+ * directory having contents (unlike the sibling `tools/` check, which does
12
+ * `readdir` first). GSD used to install its shared hook bundle at exactly
13
+ * that reserved path, so every pi install carried the warning permanently.
14
+ * The fix moved the install target to `gsd-hooks/`
15
+ * (`hostBehaviors.sharedHooksDirName`), but an EXISTING install that
16
+ * upgrades still has the old `hooks/` tree sitting on disk — nothing
17
+ * removes it on its own, so the warning would persist forever without this
18
+ * migration.
19
+ *
20
+ * How do we prove it is GSD-owned?
21
+ * Per file, by manifest membership — the same `classifyArtifact()` check
22
+ * every other migration in this directory uses. Pre-#3023 pi installs
23
+ * record the shared hook bundle under `hooks/…` keys in
24
+ * `gsd-file-manifest.json`; the new install target writes `gsd-hooks/…`
25
+ * keys instead, which this migration structurally never sees because it
26
+ * only ever walks the `hooks/` subtree.
27
+ *
28
+ * What happens if the user modified it?
29
+ * `backup-and-remove` instead of `remove-managed`, so a locally patched
30
+ * hook script is recoverable from the backup rather than silently
31
+ * destroyed — mirrors migration 006.
32
+ *
33
+ * What happens to files the manifest never recorded?
34
+ * Nothing. An unmanifested file under `hooks/` (classification `unknown`)
35
+ * is left exactly where it is, and — because its presence keeps the
36
+ * directory non-empty — it also keeps the directory itself from being
37
+ * retired. That is a deliberate consequence of directory removal being
38
+ * gated on emptiness, not a special case.
39
+ *
40
+ * What happens to the directory itself?
41
+ * `hooks/lib/` and then `hooks/` each get a `remove-empty-dir` action (see
42
+ * `evaluateRemoveEmptyDir` in `../installer-migrations.cts`). That action
43
+ * only ever calls `fs.rmdirSync` — never a recursive removal — and
44
+ * re-checks emptiness immediately before doing so, so a directory that
45
+ * still holds anything (an unmanifested file, or a file-level action that
46
+ * failed to apply) is left in place rather than assumed empty. Actions are
47
+ * emitted deepest-first (`hooks/lib` before `hooks`) so the parent has a
48
+ * chance to become empty in the same pass.
49
+ *
50
+ * What happens if it is missing?
51
+ * No actions. A fresh post-#3023 pi install never creates `hooks/` at all,
52
+ * and an already-migrated install has nothing left to retire — both plan
53
+ * empty, so the migration is idempotent.
54
+ *
55
+ * What runtime and scope does it affect?
56
+ * pi only, global and local. No other runtime's install is affected:
57
+ * `hostBehaviors.sharedHooksDirName` defaults to `'hooks'` for every other
58
+ * runtime, and none of them reserve that name the way pi does, so a
59
+ * claude/kimi/opencode/etc. `hooks/` directory is a live, in-use install
60
+ * surface that must never be touched here. The runtime check is the FIRST
61
+ * thing `plan()` does, ahead of even checking whether the directory exists,
62
+ * as defense in depth beyond the `runtimes: ['pi']` record-level filter the
63
+ * framework itself already enforces.
64
+ *
65
+ * Is the action safe in non-interactive install?
66
+ * Yes. Every emitted action type (`remove-managed`, `backup-and-remove`,
67
+ * `remove-empty-dir`) is non-interactive and journaled; none requires a
68
+ * user choice, and unknown files never produce an action.
69
+ *
70
+ * See docs/installer-migrations.md#shipped-migrations, the pi row of
71
+ * docs/installer-migrations.md#runtime-configuration-contract-registry, and
72
+ * the 2026-08-07 amendment to docs/adr/0008-installer-migration-module.md.
73
+ */
74
+ var __importDefault = (this && this.__importDefault) || function (mod) {
75
+ return (mod && mod.__esModule) ? mod : { "default": mod };
76
+ };
77
+ const node_fs_1 = __importDefault(require("node:fs"));
78
+ const node_path_1 = __importDefault(require("node:path"));
79
+ /** pi's reserved (and, pre-#3023, GSD-populated) legacy hook directory name. */
80
+ const HOOKS_DIR = 'hooks';
81
+ const FILE_REASON = "pi's startup check warns whenever hooks/ exists (checkDeprecatedExtensionDirs), and GSD's shared " +
82
+ 'hook bundle now installs at gsd-hooks/ instead (#3023), so the legacy files are superseded';
83
+ const FILE_OWNERSHIP_EVIDENCE = 'pre-#3023 pi installs record the shared hook bundle under hooks/… keys in gsd-file-manifest.json; ' +
84
+ 'the new install target is gsd-hooks/…, which this migration never touches because it only walks the ' +
85
+ 'hooks/ subtree';
86
+ const DIR_REASON = "pi's checkDeprecatedExtensionDirs() warns on hooks/'s mere existence, not its contents (#3023); the " +
87
+ 'reserved container is retired once every GSD-owned entry inside it is gone';
88
+ const DIR_OWNERSHIP_EVIDENCE = 'hooks/ and hooks/lib/ are GSD-installed container directories under the pi config root (the pre-#3023 ' +
89
+ 'default of hostBehaviors.sharedHooksDirName); removal is gated on emptiness by the shared ' +
90
+ 'remove-empty-dir action, so a directory that still holds an unmanifested user file — or any file-level ' +
91
+ 'action that failed to apply — is left in place rather than assumed empty';
92
+ /**
93
+ * Recursively collect files and directories under `relDir`, never following a
94
+ * symlink (whether it names a file or a directory) and never emitting a path
95
+ * that resolves outside `baseResolved`. Mirrors the traversal guard in
96
+ * migration 003 (`walkLegacyFiles`).
97
+ */
98
+ function walkPiHooksTree(root, relDir, baseResolved, files, dirs) {
99
+ const dir = node_path_1.default.join(root, relDir);
100
+ const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
101
+ for (const entry of entries) {
102
+ // Never follow a symlink into or through: it must not be traversed,
103
+ // hashed, or removed, regardless of what it points at.
104
+ if (entry.isSymbolicLink())
105
+ continue;
106
+ const relPath = node_path_1.default.posix.join(relDir, entry.name);
107
+ const resolved = node_path_1.default.resolve(root, relPath);
108
+ if (resolved !== baseResolved && !resolved.startsWith(baseResolved + node_path_1.default.sep))
109
+ continue;
110
+ if (entry.isDirectory()) {
111
+ dirs.push(relPath);
112
+ walkPiHooksTree(root, relPath, baseResolved, files, dirs);
113
+ }
114
+ else if (entry.isFile()) {
115
+ files.push(relPath);
116
+ }
117
+ }
118
+ }
119
+ const migration = {
120
+ id: '2026-08-07-pi-retire-reserved-hooks-dir',
121
+ title: "Retire pi's reserved hooks/ directory",
122
+ description: 'Remove manifest-managed files under <piConfigDir>/hooks/ and, once empty, the directory itself ' +
123
+ '(and its hooks/lib/ subdirectory), now that the shared hook bundle installs at gsd-hooks/ instead. pi ' +
124
+ 'reserves hooks/ as its own deprecated extension directory and warns on every startup while it exists (#3023).',
125
+ introducedIn: '1.9.2',
126
+ runtimes: ['pi'],
127
+ scopes: ['global', 'local'],
128
+ destructive: true,
129
+ plan: (ctx) => {
130
+ // Defense in depth ahead of the framework's own runtimes filter: a
131
+ // claude/kimi/opencode/etc. hooks/ directory is a live install surface,
132
+ // never a retirement target.
133
+ if (ctx.runtime !== 'pi')
134
+ return [];
135
+ const hooksRoot = node_path_1.default.join(ctx.configDir, HOOKS_DIR);
136
+ let rootLstat;
137
+ try {
138
+ rootLstat = node_fs_1.default.lstatSync(hooksRoot);
139
+ }
140
+ catch {
141
+ return []; // absent -> nothing to retire, idempotent
142
+ }
143
+ // Never follow a symlinked hooks/ root: walking through it could plan
144
+ // actions against paths outside the pi config directory entirely.
145
+ if (rootLstat.isSymbolicLink())
146
+ return [];
147
+ if (!rootLstat.isDirectory())
148
+ return [];
149
+ const baseResolved = node_path_1.default.resolve(ctx.configDir);
150
+ const files = [];
151
+ const dirs = [];
152
+ try {
153
+ walkPiHooksTree(ctx.configDir, HOOKS_DIR, baseResolved, files, dirs);
154
+ }
155
+ catch {
156
+ // Unreadable directory: nothing safe to plan.
157
+ return [];
158
+ }
159
+ const actions = [];
160
+ for (const relPath of files) {
161
+ const { classification } = ctx.classifyArtifact(relPath);
162
+ if (classification === 'managed-pristine') {
163
+ actions.push({ type: 'remove-managed', relPath, reason: FILE_REASON, ownershipEvidence: FILE_OWNERSHIP_EVIDENCE });
164
+ }
165
+ else if (classification === 'managed-modified') {
166
+ actions.push({ type: 'backup-and-remove', relPath, reason: FILE_REASON, ownershipEvidence: FILE_OWNERSHIP_EVIDENCE });
167
+ }
168
+ // 'unknown' (user-added, not manifest-recorded): no action, preserved.
169
+ // 'missing' / 'managed-missing': impossible here — relPath was just
170
+ // discovered by walking the live filesystem, so it currently exists.
171
+ }
172
+ // Deepest directories first, so a child has already been evaluated (and
173
+ // possibly removed) before its parent's own emptiness is re-checked by
174
+ // the executor. `hooks/` itself is appended last, unconditionally: the
175
+ // executor's own emptiness re-check is what actually decides whether it
176
+ // goes, not this ordering — this ordering only gives it the chance to.
177
+ const orderedDirs = [...dirs].sort((a, b) => b.split('/').length - a.split('/').length);
178
+ orderedDirs.push(HOOKS_DIR);
179
+ for (const relPath of orderedDirs) {
180
+ actions.push({
181
+ type: 'remove-empty-dir',
182
+ relPath,
183
+ reason: DIR_REASON,
184
+ ownershipEvidence: DIR_OWNERSHIP_EVIDENCE,
185
+ // Declared, not derived: classifyArtifact() hashes file contents via
186
+ // sha256File(), which throws EISDIR against a directory path. These
187
+ // relPaths name directories, so classification is stated directly
188
+ // (never 'unknown', so the planner's unknown-classification block
189
+ // never fires for them) rather than routed through the file
190
+ // classifier.
191
+ classification: 'managed-pristine',
192
+ originalHash: null,
193
+ currentHash: null,
194
+ });
195
+ }
196
+ return actions;
197
+ },
198
+ };
199
+ module.exports = migration;