@opengsd/gsd-core 1.7.0-rc.5 → 1.7.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 (112) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-executor.md +2 -1
  4. package/agents/gsd-security-auditor.md +13 -15
  5. package/agents/gsd-ui-checker.md +2 -0
  6. package/agents/gsd-ui-researcher.md +1 -0
  7. package/bin/install.js +975 -196
  8. package/commands/gsd/mempalace-capture.md +27 -1
  9. package/commands/gsd/surface.md +6 -6
  10. package/gsd-core/bin/gsd-tools.cjs +63 -2
  11. package/gsd-core/bin/lib/api-coverage.cjs +3 -4
  12. package/gsd-core/bin/lib/audit.cjs +7 -6
  13. package/gsd-core/bin/lib/capability-registry.cjs +503 -87
  14. package/gsd-core/bin/lib/capability-validator.cjs +56 -18
  15. package/gsd-core/bin/lib/check-command-router.cjs +1 -1
  16. package/gsd-core/bin/lib/clock.cjs +19 -0
  17. package/gsd-core/bin/lib/commands.cjs +48 -9
  18. package/gsd-core/bin/lib/config-loader.cjs +6 -2
  19. package/gsd-core/bin/lib/config.cjs +12 -0
  20. package/gsd-core/bin/lib/core-utils.cjs +8 -2
  21. package/gsd-core/bin/lib/drift.cjs +4 -4
  22. package/gsd-core/bin/lib/frontmatter.cjs +22 -0
  23. package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
  24. package/gsd-core/bin/lib/host-integration.cjs +33 -8
  25. package/gsd-core/bin/lib/init.cjs +60 -53
  26. package/gsd-core/bin/lib/install-engine.cjs +93 -22
  27. package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
  28. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  29. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  30. package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
  31. package/gsd-core/bin/lib/markdown-table.cjs +698 -0
  32. package/gsd-core/bin/lib/mcp-server.cjs +18 -7
  33. package/gsd-core/bin/lib/milestone.cjs +217 -31
  34. package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
  35. package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
  36. package/gsd-core/bin/lib/phase-locator.cjs +23 -2
  37. package/gsd-core/bin/lib/phase.cjs +436 -61
  38. package/gsd-core/bin/lib/plan-scan.cjs +3 -0
  39. package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
  40. package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
  41. package/gsd-core/bin/lib/roadmap.cjs +100 -49
  42. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +242 -44
  43. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
  44. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -14
  45. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
  46. package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
  47. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +526 -29
  48. package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
  49. package/gsd-core/bin/lib/schema-detect.cjs +2 -1
  50. package/gsd-core/bin/lib/security.cjs +7 -37
  51. package/gsd-core/bin/lib/shell-command-projection.cjs +176 -27
  52. package/gsd-core/bin/lib/smart-entry.cjs +4 -3
  53. package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
  54. package/gsd-core/bin/lib/state-transition.cjs +100 -45
  55. package/gsd-core/bin/lib/state.cjs +391 -126
  56. package/gsd-core/bin/lib/surface.cjs +12 -8
  57. package/gsd-core/bin/lib/template.cjs +2 -1
  58. package/gsd-core/bin/lib/uat.cjs +54 -8
  59. package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
  60. package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
  61. package/gsd-core/bin/lib/verify.cjs +4 -3
  62. package/gsd-core/bin/lib/workstream.cjs +3 -2
  63. package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
  64. package/gsd-core/bin/lib/write-set.cjs +38 -0
  65. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  66. package/gsd-core/bin/shared/model-catalog.json +8 -3
  67. package/gsd-core/references/checkpoints.md +12 -0
  68. package/gsd-core/references/ui-consideration-probe.md +73 -0
  69. package/gsd-core/templates/UI-SPEC.md +25 -0
  70. package/gsd-core/templates/VALIDATION.md +2 -0
  71. package/gsd-core/workflows/add-tests.md +1 -1
  72. package/gsd-core/workflows/audit-milestone.md +7 -4
  73. package/gsd-core/workflows/debug.md +2 -0
  74. package/gsd-core/workflows/execute-phase.md +5 -3
  75. package/gsd-core/workflows/fast.md +8 -22
  76. package/gsd-core/workflows/plan-phase.md +6 -0
  77. package/gsd-core/workflows/progress.md +2 -2
  78. package/gsd-core/workflows/quick.md +2 -0
  79. package/gsd-core/workflows/review.md +42 -3
  80. package/gsd-core/workflows/secure-phase.md +1 -1
  81. package/gsd-core/workflows/settings-advanced.md +7 -4
  82. package/gsd-core/workflows/ship.md +8 -2
  83. package/gsd-core/workflows/spec-phase.md +1 -1
  84. package/gsd-core/workflows/transition.md +1 -1
  85. package/gsd-core/workflows/ui-phase.md +146 -1
  86. package/gsd-core/workflows/validate-phase.md +2 -2
  87. package/hooks/dist/gsd-statusline.js +164 -14
  88. package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
  89. package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
  90. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  91. package/hooks/gsd-statusline.js +164 -14
  92. package/hooks/gsd-windsurf-pre-command.js +275 -0
  93. package/hooks/gsd-windsurf-pre-write.js +132 -0
  94. package/hooks/managed-hooks-registry.cjs +2 -0
  95. package/package.json +10 -4
  96. package/pi/gsd.cjs +354 -0
  97. package/scripts/build-hooks.js +3 -0
  98. package/scripts/ci-test-scope.cjs +39 -1
  99. package/scripts/gen-golden-install-parity-zcode.cjs +35 -35
  100. package/scripts/gen-install-tree-fixtures.cjs +75 -0
  101. package/scripts/gen-registry.cjs +128 -0
  102. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  103. package/scripts/lint-table-schema-drift.cjs +157 -0
  104. package/scripts/lint-test-file-count.allowlist.json +2 -1
  105. package/scripts/registry-schema.cjs +565 -0
  106. package/scripts/validate-registry.cjs +117 -0
  107. package/skills/gsd-mempalace-capture/SKILL.md +27 -1
  108. package/skills/gsd-surface/SKILL.md +6 -6
  109. package/vscode/browser.js +197 -0
  110. package/vscode/extension.js +383 -0
  111. package/vscode/host-binding.js +113 -0
  112. package/vscode/package.json +96 -0
package/bin/install.js CHANGED
@@ -32,6 +32,7 @@ const {
32
32
  resolveAntigravityGlobalDir,
33
33
  getGlobalConfigDir,
34
34
  getGlobalSkillsBase,
35
+ resolveKimiHooksTomlDir,
35
36
  } = require('../gsd-core/bin/lib/runtime-homes.cjs');
36
37
  // getDirName (runtime -> local config dir name) is relocated out of this
37
38
  // installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the
@@ -58,25 +59,20 @@ const INSTALLED_HOOK_FILES = new Set(_HOOKS_TO_COPY);
58
59
  const hooksSurface = require('../gsd-core/bin/lib/runtime-hooks-surface.cjs');
59
60
 
60
61
  /**
61
- * Runtimes that register hyphen-form `name:` per #2808 AND copy agent bodies
62
- * verbatim (only branding swaps, no namespace conversion), so retired
63
- * `/gsd:<cmd>` colon refs leak into installed agent prose. Sibling fixes
62
+ * #3677 predicate — true when an agent body needs `/gsd:<cmd>` → `/gsd-<cmd>`
63
+ * normalization at install time. Descriptor-driven
64
+ * (capabilities/<runtime>/capability.json -> runtime.hostBehaviors.hyphenNameAgentBody)
65
+ * instead of a hardcoded runtime allow-list (ADR-1239 / #2086). Sibling fixes
64
66
  * #3583 / #3629 covered SKILL.md bodies, #3584 / #3606 covered runtime
65
67
  * emissions — this is the agent-body surface (#3677).
66
68
  *
67
- * Explicit allow-list rather than deny-list so unknown / future runtimes
68
- * default to "no rewrite" (better to leak than to mangle a runtime whose
69
- * namespace behavior we haven't verified).
70
- */
71
- const HYPHEN_NAME_AGENT_RUNTIMES = new Set(['claude', 'qwen', 'hermes']);
72
-
73
- /**
74
- * #3677 predicate — true when an agent body needs `/gsd:<cmd>` → `/gsd-<cmd>`
75
- * normalization at install time.
69
+ * Unknown / future runtimes that don't declare the flag default to "no
70
+ * rewrite" (better to leak than to mangle a runtime whose namespace
71
+ * behavior we haven't verified).
76
72
  */
77
73
  function shouldNormalizeHyphenNamespaceInAgentBody(runtime) {
78
74
  if (typeof runtime !== 'string' || runtime === '') return false;
79
- return HYPHEN_NAME_AGENT_RUNTIMES.has(runtime);
75
+ return _hostBehaviors(runtime).hyphenNameAgentBody === true;
80
76
  }
81
77
 
82
78
  /**
@@ -282,6 +278,28 @@ const GSD_CURSOR_HOOK_SCRIPTS = [
282
278
  // Marker comment embedded in managed hook entries so GSD can find+remove them.
283
279
  const GSD_CURSOR_HOOK_MARKER = 'gsd-managed';
284
280
 
281
+ // #2100 Stage 2 — Windsurf/Cascade lifecycle hook constants.
282
+ // Windsurf/Cascade reads hook configs from <project-root>/.windsurf/hooks.json
283
+ // (local) or ~/.codeium/windsurf/hooks.json (global) with the shape
284
+ // { hooks: { <event>: [ { command, ... } ] } } — note: no top-level `version`
285
+ // field, and each entry carries a bare `command` shell string (no `type`
286
+ // field), unlike Cursor's hooks.json. GSD registers two managed BLOCKING
287
+ // hooks (exit code 2 to block, vs. Cursor's stdout-JSON form):
288
+ // pre_write_code → gsd-windsurf-pre-write.js (write-path guard)
289
+ // pre_run_command → gsd-windsurf-pre-command.js (destructive-command guard)
290
+ // Cascade has no context-injection channel, so the 4 advisory hooks GSD
291
+ // registers on Cursor (sessionStart, postToolUse, stop, subagentStart/Stop)
292
+ // have no Windsurf counterpart and are deliberately NOT ported.
293
+ // Cascade hooks docs (reference): https://docs.windsurf.com/llms-full.txt ,
294
+ // https://docs.devin.ai/desktop/cascade/hooks
295
+ const GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT = 'gsd-windsurf-pre-write.js';
296
+ const GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT = 'gsd-windsurf-pre-command.js';
297
+ // All GSD-managed Windsurf hook scripts (used by uninstall cleanup).
298
+ const GSD_WINDSURF_HOOK_SCRIPTS = [
299
+ GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT,
300
+ GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT,
301
+ ];
302
+
285
303
  // GSD-managed files under hooks/lib/ (helpers required by gsd-*.sh hooks).
286
304
  // git-cmd.js does not start with "gsd-" (shared classifier for #3129), gsd-graphify-rebuild.sh does.
287
305
  const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh'];
@@ -386,7 +404,16 @@ const FALLBACK_HOST_BEHAVIORS = Object.freeze({
386
404
  settingsFileByScope: Object.freeze({ local: 'settings.local.json', global: 'settings.json' }),
387
405
  permissionsSchema: 'claude',
388
406
  sourceMarkerFile: '.gsd-source',
407
+ hyphenNameAgentBody: true,
408
+ legacyCommandsGsdInstallMigration: true,
409
+ legacyCommandsGsdUninstall: 'global',
389
410
  }),
411
+ // antigravity's global config dir is resolved dynamically (env-overridable,
412
+ // multi-segment) via resolveAntigravityGlobalDir in getConfigDirFromHome. If the
413
+ // registry fails to load, this floor keeps that routing intact instead of
414
+ // silently falling through to the generic getGlobalConfigHomeFragment default
415
+ // (which would return the wrong '.claude' fragment). (ADR-1239 / #2096)
416
+ antigravity: Object.freeze({ globalDirResolver: 'antigravity' }),
390
417
  });
391
418
 
392
419
  /**
@@ -480,6 +507,7 @@ const {
480
507
  installRuntimeArtifacts,
481
508
  uninstallRuntimeArtifacts,
482
509
  installOpencodeFamilySkills,
510
+ _installNativePluginIfDeclared,
483
511
  _copyStaged,
484
512
  hasExistingSymlinkBetween,
485
513
  preserveUserArtifacts,
@@ -529,7 +557,7 @@ if (hasMinimal && _profileArgRaw) {
529
557
 
530
558
  function selectRuntimesFromArgs(runtimeArgs) {
531
559
  if (runtimeArgs.includes('--all')) {
532
- return ['claude', 'kimi', 'kilo', 'opencode', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'zcode'];
560
+ return ['claude', 'kimi', 'kilo', 'opencode', 'pi', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'zcode'];
533
561
  }
534
562
  if (runtimeArgs.includes('--both')) {
535
563
  return ['claude', 'opencode'];
@@ -538,6 +566,7 @@ function selectRuntimesFromArgs(runtimeArgs) {
538
566
  const selected = [];
539
567
  if (runtimeArgs.includes('--claude')) selected.push('claude');
540
568
  if (runtimeArgs.includes('--opencode')) selected.push('opencode');
569
+ if (runtimeArgs.includes('--pi')) selected.push('pi');
541
570
  if (runtimeArgs.includes('--kilo')) selected.push('kilo');
542
571
  if (runtimeArgs.includes('--codex')) selected.push('codex');
543
572
  if (runtimeArgs.includes('--copilot')) selected.push('copilot');
@@ -641,7 +670,15 @@ function getConfigDirFromHome(runtime, isGlobal) {
641
670
  // multi-segment via resolveAntigravityGlobalDir + path.relative) — not a table
642
671
  // entry. (The prior inner `if (!isGlobal) return "'.agents'"` was unreachable:
643
672
  // !isGlobal returns at the top of this function.)
644
- if (runtime === 'antigravity') {
673
+ // Descriptor-driven (ADR-1239 / #2096): folded from a hardcoded
674
+ // `runtime === 'antigravity'` literal into a read of the runtime's
675
+ // `hostBehaviors.globalDirResolver` descriptor field (via _hostBehaviors, which
676
+ // also degrades to FALLBACK_HOST_BEHAVIORS on registry-load failure). This is
677
+ // antigravity-unique: unlike `configHome.kind === 'dot-home-nested'` (which
678
+ // windsurf also declares — see capabilities/windsurf/capability.json — and
679
+ // would wrongly route windsurf's global dir through
680
+ // resolveAntigravityGlobalDir), `globalDirResolver` is only set by antigravity.
681
+ if (_hostBehaviors(runtime).globalDirResolver === 'antigravity') {
645
682
  const antigravityDir = resolveAntigravityGlobalDir();
646
683
  const rel = path.relative(os.homedir(), antigravityDir);
647
684
  const segments = rel.split(path.sep).filter(Boolean);
@@ -675,7 +712,7 @@ const banner = '\n' +
675
712
  ' GSD Core ' + dim + 'v' + pkg.version + reset + '\n' +
676
713
  ' Git. Ship. Done.\n' +
677
714
  ' A meta-prompting, context engineering and spec-driven\n' +
678
- ' development workflows for Claude Code, OpenCode, Kimi CLI, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline, CodeBuddy and ZCode.\n';
715
+ ' development workflows for Claude Code, OpenCode, Kimi CLI, Kilo, Codex, Copilot, Antigravity, Cursor, Windsurf, Augment, Trae, Qwen Code, Hermes Agent, Cline, CodeBuddy, ZCode and pi.\n';
679
716
 
680
717
  // Pure seam: parse --config-dir / -c from an arbitrary args array.
681
718
  // Returns the path string, '' for an empty equals-form value, or null when the
@@ -756,6 +793,10 @@ const referencesHook = hooksSurface.referencesHook;
756
793
  // applySettingsJsonHooks: mutates settings.hooks.* in place with all GSD-managed
757
794
  // hook registrations for settings.json-surface runtimes (ADR-857 phase 5f-1b).
758
795
  const applySettingsJsonHooks = hooksSurface.applySettingsJsonHooks;
796
+ // writeKimiHooksToml / removeKimiHooksToml: kimi's native config.toml [[hooks]]
797
+ // surface (#2095 EoS/kimi Upgrade 1) — separate from settings.json entirely.
798
+ const writeKimiHooksToml = hooksSurface.writeKimiHooksToml;
799
+ const removeKimiHooksToml = hooksSurface.removeKimiHooksToml;
759
800
  // processAttribution: pure Co-Authored-By content transform, relocated to the
760
801
  // conversion module (ADR-1508 / #1510 Phase 1). Bound here so install.js
761
802
  // callers continue to work and there is a single implementation. (All call
@@ -1098,9 +1139,10 @@ function writeSettings(settingsPath, settings) {
1098
1139
  * Used by Codex TOML and OpenCode agent file generators to embed per-agent
1099
1140
  * model assignments so that model_overrides is respected on non-Claude runtimes (#2256).
1100
1141
  */
1101
- function readGsdGlobalModelOverrides() {
1142
+ function readGsdGlobalModelOverrides(options = {}) {
1102
1143
  try {
1103
- const defaultsPath = path.join(os.homedir(), '.gsd', 'defaults.json');
1144
+ const home = options.homedir ? options.homedir() : os.homedir();
1145
+ const defaultsPath = path.join(home, '.gsd', 'defaults.json');
1104
1146
  if (!fs.existsSync(defaultsPath)) return null;
1105
1147
  const raw = fs.readFileSync(defaultsPath, 'utf-8');
1106
1148
  const parsed = JSON.parse(raw);
@@ -1137,8 +1179,8 @@ function readGsdGlobalModelOverrides() {
1137
1179
  * Returns a plain `{ agentName: modelId }` object, or `null` when neither
1138
1180
  * source defines `model_overrides`.
1139
1181
  */
1140
- function readGsdEffectiveModelOverrides(targetDir = null) {
1141
- const global = readGsdGlobalModelOverrides();
1182
+ function readGsdEffectiveModelOverrides(targetDir = null, options = {}) {
1183
+ const global = readGsdGlobalModelOverrides(options);
1142
1184
 
1143
1185
  let projectOverrides = null;
1144
1186
  if (targetDir) {
@@ -1852,10 +1894,12 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
1852
1894
  // Track GSD's package version so Hermes' skill_view() reports a stable
1853
1895
  // identifier per install.
1854
1896
  if (_hostBehaviors(runtime).skillFrontmatterVersion) fm += `version: ${yamlQuote(pkg.version)}\n`;
1855
- // #778 (b) — Qwen-only numeric priority for /skills ordering. Scoped to qwen
1856
- // so Claude/Hermes skill frontmatter is unchanged (they ignore the field, but
1857
- // we keep their output byte-stable). skillName is the `gsd-<stem>` dir name.
1858
- if (runtime === 'qwen') {
1897
+ // #778 (b) — numeric priority for /skills ordering, declared on the runtime
1898
+ // descriptor (runtime.hostBehaviors.skillPriorityFrontmatter). Scoped to
1899
+ // runtimes that declare the flag so Claude/Hermes skill frontmatter is
1900
+ // unchanged (they ignore the field, but we keep their output byte-stable).
1901
+ // skillName is the `gsd-<stem>` dir name. (ADR-1239 / #2086)
1902
+ if (_hostBehaviors(runtime).skillPriorityFrontmatter) {
1859
1903
  const stem = typeof skillName === 'string' && skillName.startsWith('gsd-')
1860
1904
  ? skillName.slice(4)
1861
1905
  : skillName;
@@ -1900,6 +1944,14 @@ function convertGsdCommandReferencesToKimiSkillInvocations(content, cmdNames) {
1900
1944
  .replace(hyphenPattern, (_, cmd) => `/skill:gsd-${cmd}`);
1901
1945
  }
1902
1946
 
1947
+ // DEFECT.GENERATIVE-FIX: this body is mirrored in
1948
+ // src/runtime-artifact-conversion.cts's convertClaudeCommandToKimiSkill (dead
1949
+ // for the live skills-install path, which routes here via
1950
+ // install-engine.cts's SKILLS_CONVERTER_REGISTRY through the kimi capability
1951
+ // descriptor's artifactLayout `converter: "convertClaudeCommandToKimiSkill"`;
1952
+ // kept for bin/install.js's own module-level export/test surface). Neither
1953
+ // copy re-exports the other — mirror any behavior change into both. Guarded
1954
+ // by the output-parity test in tests/runtime-converters.test.cjs (#2095).
1903
1955
  function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cmdNames = null) {
1904
1956
  const { frontmatter, body } = extractFrontmatterAndBody(content);
1905
1957
  const kimiSkillName = normalizeKimiSkillName(skillName);
@@ -2044,6 +2096,16 @@ function buildKimiSubagentYaml({ name, description, tools }) {
2044
2096
  return `${lines.join('\n')}\n`;
2045
2097
  }
2046
2098
 
2099
+ // DEFECT.GENERATIVE-FIX: this body is mirrored in
2100
+ // src/runtime-artifact-conversion.cts's buildKimiAgentArtifacts (dead for the
2101
+ // live install path, which routes here via runtime-artifact-layout.cts's
2102
+ // kimiAgentsKind — see its `conversionExports['buildKimiAgentArtifacts']`
2103
+ // dynamic lookup against the compiled runtime-artifact-conversion.cjs; kept
2104
+ // for bin/install.js's own module-level export/test surface). Neither copy
2105
+ // re-exports the other — mirror any behavior change into both, including the
2106
+ // kimi_cli.tools.agent:Agent grant that enables background dispatch
2107
+ // (#2095 Upgrade 2). Guarded by the output-parity test in
2108
+ // tests/runtime-converters.test.cjs (#2095).
2047
2109
  function buildKimiAgentArtifacts({
2048
2110
  rootAgent = '',
2049
2111
  subagents = [],
@@ -2569,14 +2631,6 @@ function convertClaudeAgentToWindsurfAgent(content) {
2569
2631
  // Augment uses a tool set similar to Cursor/Windsurf.
2570
2632
  // Config lives in .augment/ (local) and ~/.augment/ (global).
2571
2633
 
2572
- const claudeToAugmentTools = {
2573
- Bash: 'launch-process',
2574
- Edit: 'str-replace-editor',
2575
- AskUserQuestion: null,
2576
- SlashCommand: null,
2577
- TodoWrite: 'add_tasks',
2578
- };
2579
-
2580
2634
  // #1675 (ADR-1508): the augment converter family below was a byte-identical
2581
2635
  // duplicate of runtime-artifact-conversion.cjs:
2582
2636
  // convertSlashCommandsToAugmentSkillMentions, convertClaudeToAugmentMarkdown,
@@ -2618,6 +2672,14 @@ function convertClaudeToTraeMarkdown(content) {
2618
2672
  return converted;
2619
2673
  }
2620
2674
 
2675
+ // DEFECT.GENERATIVE-FIX: this body is mirrored in
2676
+ // src/runtime-artifact-conversion.cts's convertClaudeCommandToTraeSkill (used
2677
+ // by src/install-engine.cts's skills-install path via
2678
+ // SKILLS_CONVERTER_REGISTRY). This bin/install.js copy is dead for the live
2679
+ // skills-install path — kept for this file's own module-level export/test
2680
+ // surface. Neither copy re-exports the other — mirror any behavior change
2681
+ // into both. Guarded by the output-parity test in
2682
+ // tests/runtime-converters.test.cjs (#2094).
2621
2683
  function convertClaudeCommandToTraeSkill(content, skillName) {
2622
2684
  const converted = convertClaudeToTraeMarkdown(content);
2623
2685
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
@@ -2632,7 +2694,16 @@ function convertClaudeCommandToTraeSkill(content, skillName) {
2632
2694
  const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
2633
2695
  // #2876: quote so YAML flow indicators (`[BETA] …`) don't break Trae's
2634
2696
  // frontmatter parser.
2635
- return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n${body}`;
2697
+ let fm = `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n`;
2698
+ // #2094: emit `stage:` so Trae's SOLO agent can auto-invoke GSD skills at
2699
+ // the corresponding stage (docs.trae.ai/ide/agent). The field name/schema
2700
+ // is not formally documented (thin SPA docs) — descriptor-driven, single
2701
+ // fixed GSD-side value (runtime.hostBehaviors.soloStageMetadata), inferred/
2702
+ // best-effort.
2703
+ const soloStage = _hostBehaviors('trae').soloStageMetadata;
2704
+ if (soloStage) fm += `stage: ${soloStage}\n`;
2705
+ fm += '---';
2706
+ return `${fm}\n${body}`;
2636
2707
  }
2637
2708
 
2638
2709
  function convertClaudeAgentToTraeAgent(content) {
@@ -5773,6 +5844,37 @@ function removeCursorHooksJson(targetDir) {
5773
5844
  return hooksSurface.removeCursorHooksJson(targetDir);
5774
5845
  }
5775
5846
 
5847
+ /**
5848
+ * #2100 Stage 2 — Write GSD-managed Windsurf/Cascade lifecycle hooks into
5849
+ * <targetDir>/hooks.json. Both managed hook scripts
5850
+ * (gsd-windsurf-pre-write.js, gsd-windsurf-pre-command.js) are copied from
5851
+ * the GSD hooks/ source to <targetDir>/hooks/ first, so the hooks.json
5852
+ * entries never reference a script that wasn't installed. Mirrors
5853
+ * writeCursorHooksJson's structure; Cascade's blocking protocol (exit code 2)
5854
+ * and entry shape (bare `command` string, no `type` field) are distinct from
5855
+ * Cursor's.
5856
+ *
5857
+ * @param {string} targetDir - The Windsurf config dir (global: ~/.codeium/windsurf; local: .windsurf)
5858
+ * @param {string} src - The GSD install source root (for copying hook scripts)
5859
+ * @param {{ platform?: string }} opts
5860
+ * @returns {{ hooksJsonPath: string, changed: boolean }}
5861
+ */
5862
+ function writeWindsurfHooksJson(targetDir, src, opts) {
5863
+ return hooksSurface.writeWindsurfHooksJson(targetDir, src, opts);
5864
+ }
5865
+
5866
+ /**
5867
+ * Remove all GSD-managed Windsurf/Cascade lifecycle hook entries from
5868
+ * hooks.json. User-owned entries are preserved. If the file becomes empty,
5869
+ * it is removed.
5870
+ *
5871
+ * @param {string} targetDir - The Windsurf config dir
5872
+ * @returns {{ changed: boolean }}
5873
+ */
5874
+ function removeWindsurfHooksJson(targetDir) {
5875
+ return hooksSurface.removeWindsurfHooksJson(targetDir);
5876
+ }
5877
+
5776
5878
  /**
5777
5879
  * #786 — Build the GSD-managed GitHub Copilot lifecycle hook config object.
5778
5880
  *
@@ -6077,7 +6179,12 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
6077
6179
  }
6078
6180
 
6079
6181
  // Kilo CLI — same conversion logic as OpenCode, different config paths.
6080
- function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
6182
+ // DEFECT.GENERATIVE-FIX: this body is mirrored in
6183
+ // src/runtime-artifact-conversion.cts's convertClaudeToKiloFrontmatter (used by
6184
+ // src/install-engine.cts's install path). Neither copy re-exports the other —
6185
+ // mirror any behavior change into both. Guarded by the output-parity test in
6186
+ // tests/runtime-converters.test.cjs (#2093).
6187
+ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverride = null } = {}) {
6081
6188
  // Replace tool name references in content (applies to all files)
6082
6189
  let convertedContent = content;
6083
6190
  convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
@@ -6236,6 +6343,13 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
6236
6343
  // For agents: add required Kilo agent fields
6237
6344
  if (isAgent) {
6238
6345
  newLines.push('mode: subagent');
6346
+ // Embed model override from ~/.gsd/defaults.json so model_overrides is
6347
+ // respected on Kilo (which uses static agent frontmatter, not inline
6348
+ // Task() model parameters) — mirrors convertClaudeToOpencodeFrontmatter's
6349
+ // model emission exactly (#2093 UPGRADE 2 / ADR-1239). See #2256.
6350
+ if (modelOverride) {
6351
+ newLines.push(['model:', modelOverride].join(' '));
6352
+ }
6239
6353
  newLines.push(...buildKiloAgentPermissionBlock(agentTools));
6240
6354
  }
6241
6355
 
@@ -6456,33 +6570,53 @@ const RUNTIME_CONTENT_DISPATCH = {
6456
6570
  return content;
6457
6571
  },
6458
6572
  },
6573
+ // qwen/hermes: brand VALUES are descriptor-driven (ADR-1239 / #2092) via
6574
+ // _hostBehaviors(ctx.runtime).brandingRewrites — EXACT regexes/ordering
6575
+ // preserved from the prior hardcoded-literal versions (including the
6576
+ // qwen-specific `.claude/skills/` -> `.qwen/skills/` pre-rewrite, whose
6577
+ // target is derived as `${b['.claude/']}skills/`).
6459
6578
  qwen: {
6460
- md: (content) => {
6461
- content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
6462
- content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
6463
- content = content.replace(/\.claude\//g, '.qwen/');
6579
+ md: (content, ctx) => {
6580
+ // Guarded (post-review #2092): degrade closed to a no-op if the
6581
+ // registry fails to load, instead of throwing on `b['CLAUDE.md']`.
6582
+ const b = _hostBehaviors(ctx.runtime).brandingRewrites;
6583
+ if (b) {
6584
+ content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
6585
+ content = content.replace(/\bClaude Code\b/g, b['Claude Code']);
6586
+ content = content.replace(/\.claude\//g, b['.claude/']);
6587
+ }
6464
6588
  return content;
6465
6589
  },
6466
- js: (content) => {
6467
- content = content.replace(/\.claude\/skills\//g, '.qwen/skills/');
6468
- content = content.replace(/\.claude\//g, '.qwen/');
6469
- content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
6470
- content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
6590
+ js: (content, ctx) => {
6591
+ const b = _hostBehaviors(ctx.runtime).brandingRewrites;
6592
+ if (b) {
6593
+ content = content.replace(/\.claude\/skills\//g, `${b['.claude/']}skills/`);
6594
+ content = content.replace(/\.claude\//g, b['.claude/']);
6595
+ content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
6596
+ content = content.replace(/\bClaude Code\b/g, b['Claude Code']);
6597
+ }
6471
6598
  return content;
6472
6599
  },
6473
6600
  },
6474
6601
  hermes: {
6475
- md: (content) => {
6476
- content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
6477
- content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
6478
- content = content.replace(/\.claude\//g, '.hermes/');
6602
+ md: (content, ctx) => {
6603
+ // Guarded (post-review #2092): see qwen entry above.
6604
+ const b = _hostBehaviors(ctx.runtime).brandingRewrites;
6605
+ if (b) {
6606
+ content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
6607
+ content = content.replace(/\bClaude Code\b/g, b['Claude Code']);
6608
+ content = content.replace(/\.claude\//g, b['.claude/']);
6609
+ }
6479
6610
  return content;
6480
6611
  },
6481
- js: (content) => {
6482
- content = content.replace(/\.claude\/skills\//g, '.hermes/skills/');
6483
- content = content.replace(/\.claude\//g, '.hermes/');
6484
- content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
6485
- content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
6612
+ js: (content, ctx) => {
6613
+ const b = _hostBehaviors(ctx.runtime).brandingRewrites;
6614
+ if (b) {
6615
+ content = content.replace(/\.claude\/skills\//g, `${b['.claude/']}skills/`);
6616
+ content = content.replace(/\.claude\//g, b['.claude/']);
6617
+ content = content.replace(/CLAUDE\.md/g, b['CLAUDE.md']);
6618
+ content = content.replace(/\bClaude Code\b/g, b['Claude Code']);
6619
+ }
6486
6620
  return content;
6487
6621
  },
6488
6622
  },
@@ -6741,25 +6875,21 @@ function validateHookFields(settings) {
6741
6875
  * GSD hook filenames removed during uninstall.
6742
6876
  * Module-level so tests can assert structurally instead of regex-parsing source
6743
6877
  * (retires pending-migration-to-typed-ir on hooks-opt-in.test.cjs, per #455).
6878
+ *
6879
+ * Derived from _HOOKS_TO_COPY (scripts/build-hooks.js — the SAME single source
6880
+ * of truth INSTALLED_HOOK_FILES uses for manifest-tracking above) instead of a
6881
+ * separately hand-maintained literal array. The hand-maintained array had
6882
+ * silently drifted out of sync with the install-time set — missing
6883
+ * gsd-check-update-worker.js, gsd-ensure-canonical-path.js,
6884
+ * managed-hooks-registry.cjs, gsd-cursor-pre-tool.js, gsd-cursor-stop.js,
6885
+ * gsd-cursor-subagent-start.js, gsd-cursor-subagent-stop.js, and
6886
+ * gsd-worktree-path-guard.js — so every one of those files (and the hooks/ dir
6887
+ * itself, via the non-empty-dir rmdir guard) was left behind on uninstall for
6888
+ * every settings-json-hook runtime. `gsd-check-update.cmd` is added on top: a
6889
+ * Windows-only SessionStart shim generated at install time (not copied from
6890
+ * hooks/dist/, so it is not in _HOOKS_TO_COPY).
6744
6891
  */
6745
- const GSD_UNINSTALL_HOOKS = [
6746
- 'gsd-statusline.js',
6747
- 'gsd-check-update.js',
6748
- 'gsd-check-update.cmd',
6749
- 'gsd-config-reload.js',
6750
- 'gsd-context-monitor.js',
6751
- 'gsd-cursor-session-start.js',
6752
- 'gsd-cursor-post-tool.js',
6753
- 'gsd-prompt-guard.js',
6754
- 'gsd-read-guard.js',
6755
- 'gsd-read-injection-scanner.js',
6756
- 'gsd-update-banner.js',
6757
- 'gsd-workflow-guard.js',
6758
- 'gsd-session-state.sh',
6759
- 'gsd-validate-commit.sh',
6760
- 'gsd-phase-boundary.sh',
6761
- 'gsd-graphify-update.sh',
6762
- ];
6892
+ const GSD_UNINSTALL_HOOKS = [..._HOOKS_TO_COPY, 'gsd-check-update.cmd'];
6763
6893
 
6764
6894
  /**
6765
6895
  * Uninstall GSD from the specified directory for a specific runtime
@@ -6768,7 +6898,19 @@ const GSD_UNINSTALL_HOOKS = [
6768
6898
  * @param {string} runtime - Target runtime ('claude', 'opencode', 'codex', 'copilot')
6769
6899
  */
6770
6900
  function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
6771
- const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
6901
+ // #2093: isKilo dropped — the Kilo permission-cleanup branch below is
6902
+ // descriptor-driven (resolveInstallPlan(runtime).finishPermissionWriter),
6903
+ // not gated on this flag.
6904
+ // #2094: isTrae dropped — unused in this function after the
6905
+ // skipSharedHooksInstall fold (was never referenced here besides the
6906
+ // destructure). #2095: isKimi likewise dropped — kimi is now a hooks/
6907
+ // consumer, so its former `&& !isKimi` uninstall guards were removed.
6908
+ // #2096: isAntigravity dropped — unused in this function.
6909
+ // #2098: isCodebuddy dropped — unused in this function.
6910
+ // #2099: isCopilot dropped — both Copilot side-effect branches below are now
6911
+ // gated on resolveInstallPlan(runtime).installSurface === 'copilot-instructions'.
6912
+ // #2100: isWindsurf dropped — unused in this function.
6913
+ const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
6772
6914
  const dirName = getDirName(runtime);
6773
6915
 
6774
6916
  // Get the target directory based on runtime and install type. Cline local
@@ -6797,7 +6939,13 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
6797
6939
  // #786: AGENTS.md lives at the repo root (outside targetDir) for local Copilot
6798
6940
  // installs, so its cleanup must run even when .github (targetDir) was already
6799
6941
  // removed — i.e. BEFORE the "target directory missing" early-return below.
6800
- if (isCopilot && !isGlobal) {
6942
+ // #2099: descriptor-driven via resolveInstallPlan(runtime).installSurface ===
6943
+ // 'copilot-instructions' (was hardcoded `isCopilot`). Mirrors the install-time
6944
+ // gate at the 'copilot-instructions' branch below (~line 10471 equivalent),
6945
+ // which writes this same repo-root AGENTS.md only for local ('!isGlobal')
6946
+ // installs — 'copilot-instructions' is unique to copilot's descriptor, so
6947
+ // this is byte-parity.
6948
+ if (resolveInstallPlan(runtime).installSurface === 'copilot-instructions' && !isGlobal) {
6801
6949
  const agentsMdPath = path.join(process.cwd(), 'AGENTS.md');
6802
6950
  if (fs.existsSync(agentsMdPath)) {
6803
6951
  const content = fs.readFileSync(agentsMdPath, 'utf8');
@@ -6907,8 +7055,84 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
6907
7055
  }
6908
7056
  }
6909
7057
 
7058
+ // 1a-kimi. Non-layout Kimi side-effect (#2095 EoS/kimi Upgrade 1): kimi's
7059
+ // native config.toml lives outside targetDir entirely (resolveKimiHooksTomlDir
7060
+ // resolves ~/.kimi, a sibling of targetDir's ~/.config/agents), so its
7061
+ // cleanup can't be driven by anything under targetDir the way every other
7062
+ // hook surface above is.
7063
+ if (resolveInstallPlan(runtime).hooksSurface === 'kimi-hooks-toml') {
7064
+ const kimiHooksRoot = resolveKimiHooksTomlDir();
7065
+ const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
7066
+ const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath);
7067
+ if (kimiHooksCleanup.changed) {
7068
+ removedCount++;
7069
+ console.log(` ${green}✓${reset} Removed GSD hooks from ${kimiHooksTomlPath}`);
7070
+ }
7071
+
7072
+ // Kimi's shared hook scripts + CommonJS package.json marker are installed
7073
+ // into this SAME ~/.kimi root (installSharedHooksBundle, install()'s
7074
+ // kimi-hooks-toml branch) rather than under targetDir — mirror steps "4.
7075
+ // Remove GSD hooks" / "5. Remove GSD package.json" below, but scoped to
7076
+ // kimiHooksRoot. ~/.kimi is Kimi's own native config home (shared space —
7077
+ // may hold the user's real config.toml/providers), so only the exact
7078
+ // GSD-owned filenames are removed, and directories are pruned only if left
7079
+ // empty by that removal.
7080
+ const kimiHooksDir = path.join(kimiHooksRoot, 'hooks');
7081
+ if (fs.existsSync(kimiHooksDir)) {
7082
+ let kimiHookCount = 0;
7083
+ for (const hook of GSD_UNINSTALL_HOOKS) {
7084
+ const hookPath = path.join(kimiHooksDir, hook);
7085
+ if (fs.existsSync(hookPath)) {
7086
+ fs.unlinkSync(hookPath);
7087
+ kimiHookCount++;
7088
+ }
7089
+ }
7090
+ if (kimiHookCount > 0) {
7091
+ removedCount++;
7092
+ console.log(` ${green}✓${reset} Removed ${kimiHookCount} GSD hooks from ${kimiHooksDir}`);
7093
+ }
7094
+
7095
+ const kimiHooksLibDir = path.join(kimiHooksDir, 'lib');
7096
+ if (fs.existsSync(kimiHooksLibDir)) {
7097
+ let removedKimiLibFiles = 0;
7098
+ for (const file of GSD_HOOK_LIB_FILES) {
7099
+ try {
7100
+ fs.unlinkSync(path.join(kimiHooksLibDir, file));
7101
+ removedKimiLibFiles++;
7102
+ } catch (_) { /* best-effort */ }
7103
+ }
7104
+ try { fs.rmdirSync(kimiHooksLibDir); } catch (_) { /* not empty or other error — leave it */ }
7105
+ if (removedKimiLibFiles > 0) {
7106
+ removedCount++;
7107
+ console.log(` ${green}✓${reset} Removed ${removedKimiLibFiles} hooks/lib/ helper(s) from ${kimiHooksLibDir}`);
7108
+ }
7109
+ }
7110
+
7111
+ try {
7112
+ if (fs.readdirSync(kimiHooksDir).length === 0) fs.rmdirSync(kimiHooksDir);
7113
+ } catch (_) { /* not empty — leave it */ }
7114
+ }
7115
+
7116
+ const kimiPkgJsonPath = path.join(kimiHooksRoot, 'package.json');
7117
+ if (fs.existsSync(kimiPkgJsonPath)) {
7118
+ try {
7119
+ const content = fs.readFileSync(kimiPkgJsonPath, 'utf8').trim();
7120
+ if (content === '{"type":"commonjs"}') {
7121
+ fs.unlinkSync(kimiPkgJsonPath);
7122
+ removedCount++;
7123
+ console.log(` ${green}✓${reset} Removed GSD package.json from ${kimiHooksRoot}`);
7124
+ }
7125
+ } catch (e) {
7126
+ // Ignore read errors
7127
+ }
7128
+ }
7129
+ }
7130
+
6910
7131
  // 1b. Non-layout Copilot side-effect: copilot-instructions.md cleanup
6911
- if (isCopilot) {
7132
+ // #2099: descriptor-driven via resolveInstallPlan(runtime).installSurface ===
7133
+ // 'copilot-instructions' (was hardcoded `isCopilot`), mirroring the same
7134
+ // gate used at the install-time 'copilot-instructions' branch.
7135
+ if (resolveInstallPlan(runtime).installSurface === 'copilot-instructions') {
6912
7136
  const instructionsPath = path.join(targetDir, 'copilot-instructions.md');
6913
7137
  if (fs.existsSync(instructionsPath)) {
6914
7138
  const content = fs.readFileSync(instructionsPath, 'utf8');
@@ -7023,6 +7247,38 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
7023
7247
  } catch { /* best-effort */ }
7024
7248
  }
7025
7249
 
7250
+ // 1b-windsurf. Descriptor-driven hook-bus cleanup (ADR-1239 / #2100 Stage 2):
7251
+ // remove GSD-managed Cascade hook entries from hooks.json and clean up the
7252
+ // managed hook scripts. Gated on resolveInstallPlan(runtime).hooksSurface
7253
+ // === 'windsurf-hooks-json' (mirrors the kimi-hooks-toml gate above) —
7254
+ // NOT the shared hostBehaviors.hooksJsonSurface flag the Cursor block above
7255
+ // uses, since that flag drives Cursor's own remove function + script list
7256
+ // and is not (and must not be) set for Windsurf.
7257
+ if (resolveInstallPlan(runtime).hooksSurface === 'windsurf-hooks-json') {
7258
+ const windsurfHooksJsonCleanup = removeWindsurfHooksJson(targetDir);
7259
+ if (windsurfHooksJsonCleanup.changed) {
7260
+ removedCount++;
7261
+ console.log(` ${green}✓${reset} Removed GSD-managed Windsurf hooks from hooks.json`);
7262
+ }
7263
+ // Remove all GSD-managed hook scripts (pre_write_code, pre_run_command).
7264
+ const windsurfHooksDir = path.join(targetDir, 'hooks');
7265
+ for (const script of GSD_WINDSURF_HOOK_SCRIPTS) {
7266
+ const p = path.join(windsurfHooksDir, script);
7267
+ try {
7268
+ if (fs.existsSync(p)) {
7269
+ fs.unlinkSync(p);
7270
+ removedCount++;
7271
+ }
7272
+ } catch { /* best-effort */ }
7273
+ }
7274
+ // Prune hooks/ if empty.
7275
+ try {
7276
+ if (fs.existsSync(windsurfHooksDir) && fs.readdirSync(windsurfHooksDir).length === 0) {
7277
+ fs.rmdirSync(windsurfHooksDir);
7278
+ }
7279
+ } catch { /* best-effort */ }
7280
+ }
7281
+
7026
7282
  // 1c. Claude local: remove flat gsd-*.md commands from commands/ (current layout,
7027
7283
  // #1367 fix). Also remove legacy commands/gsd/ subdirectory from prior installs.
7028
7284
  if (!isGlobal && _hostBehaviors(runtime).localInstallStyle === 'legacy-flat') {
@@ -7067,7 +7323,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
7067
7323
  // removes the directory; we must preserve/restore user artifacts before that path.
7068
7324
  // This block runs AFTER uninstallRuntimeArtifacts, so we check if the directory
7069
7325
  // was already removed and skip if so (idempotent).
7070
- if (isQwen || _hostBehaviors(runtime).legacyCommandsGsdCleanup === true) {
7326
+ if (_hostBehaviors(runtime).legacyCommandsGsdCleanup === true) {
7071
7327
  // dev-preferences may have survived in skills/ as SKILL.md — nothing to do for
7072
7328
  // that case. If a stale commands/gsd/ still exists (e.g. legacy was not removed),
7073
7329
  // attempt migration. In practice _runLegacyUninstallCleanup removes it first,
@@ -7177,9 +7433,11 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
7177
7433
  }
7178
7434
  }
7179
7435
 
7180
- // 4z. Remove the OpenCode native plugin adapter (#1914). Only GSD's own
7181
- // plugin file is removed; the plugins/ dir is pruned only if it becomes
7182
- // empty, preserving any user-authored OpenCode plugins.
7436
+ // 4z. Remove the native plugin adapter (#1914, extended to Kilo by #2093).
7437
+ // Descriptor-driven via hostBehaviors.nativePlugin — covers every runtime
7438
+ // that declares the block (OpenCode, Kilo, ...), not just OpenCode. Only
7439
+ // GSD's own plugin file is removed; the plugins/ dir is pruned only if it
7440
+ // becomes empty, preserving any user-authored plugins for that host.
7183
7441
  const _np = _hostBehaviors(runtime).nativePlugin;
7184
7442
  if (_np) {
7185
7443
  const pluginsDir = path.join(targetDir, _np.dir);
@@ -7188,7 +7446,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
7188
7446
  try {
7189
7447
  fs.unlinkSync(pluginPath);
7190
7448
  removedCount++;
7191
- console.log(` ${green}✓${reset} Removed OpenCode plugin`);
7449
+ console.log(` ${green}✓${reset} Removed native plugin adapter (${runtime})`);
7192
7450
  } catch (_) { /* best-effort */ }
7193
7451
  try { fs.rmdirSync(pluginsDir); } catch (_) { /* not empty — user plugins present */ }
7194
7452
  }
@@ -7355,6 +7613,47 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
7355
7613
  }
7356
7614
  }
7357
7615
 
7616
+ // #2096 Phase B Upgrade 1 — Remove GSD-owned Antigravity permissions.allow
7617
+ // rules from settings.json. Symmetric to the Claude branch above: filters
7618
+ // only the exact GSD-owned rule strings (regenerated from the current
7619
+ // configDir) to preserve any user-added allow entries and all deny/ask.
7620
+ if (resolveInstallPlan(runtime).finishPermissionWriter === 'antigravity' && settings.permissions) {
7621
+ let antigravityPermissionsModified = false;
7622
+ if (Array.isArray(settings.permissions.allow)) {
7623
+ const gsdRules = new Set(buildAntigravityAllowRules(targetDir));
7624
+ const before = settings.permissions.allow.length;
7625
+ settings.permissions.allow = settings.permissions.allow.filter((e) => !gsdRules.has(e));
7626
+ if (settings.permissions.allow.length !== before) {
7627
+ antigravityPermissionsModified = true;
7628
+ }
7629
+ if (settings.permissions.allow.length === 0) {
7630
+ delete settings.permissions.allow;
7631
+ }
7632
+ }
7633
+ if (Object.keys(settings.permissions).length === 0) {
7634
+ delete settings.permissions;
7635
+ }
7636
+ if (antigravityPermissionsModified) {
7637
+ settingsModified = true;
7638
+ console.log(` ${green}✓${reset} Removed GSD permissions from settings.json`);
7639
+ }
7640
+ }
7641
+
7642
+ // #2097 UPGRADE 3 — Remove the MCP companion entry from settings.json for
7643
+ // runtimes that host MCP there (Augment), symmetric to the mcp_config.json
7644
+ // removal for Antigravity below. Only the GSD-owned mcpServers.gsd key is
7645
+ // removed — any other user-configured MCP servers are preserved.
7646
+ if (_hostBehaviors(runtime).mcpCompanion === 'settings-json' &&
7647
+ settings.mcpServers && typeof settings.mcpServers === 'object' &&
7648
+ settings.mcpServers.gsd !== undefined) {
7649
+ delete settings.mcpServers.gsd;
7650
+ if (Object.keys(settings.mcpServers).length === 0) {
7651
+ delete settings.mcpServers;
7652
+ }
7653
+ settingsModified = true;
7654
+ console.log(` ${green}✓${reset} Removed GSD MCP companion server from settings.json`);
7655
+ }
7656
+
7358
7657
  if (settingsModified) {
7359
7658
  writeSettings(settingsPath, settings);
7360
7659
  removedCount++;
@@ -7403,7 +7702,9 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
7403
7702
  }
7404
7703
 
7405
7704
  // 7. For Kilo, clean up permissions from kilo.json or kilo.jsonc
7406
- if (isKilo) {
7705
+ // #2093: descriptor-driven via resolveInstallPlan(runtime).finishPermissionWriter,
7706
+ // mirroring the OpenCode branch above (was hardcoded `isKilo`).
7707
+ if (resolveInstallPlan(runtime).finishPermissionWriter === 'kilo') {
7407
7708
  const configPath = resolveKiloConfigPath(targetDir);
7408
7709
  if (fs.existsSync(configPath)) {
7409
7710
  try {
@@ -7443,6 +7744,29 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
7443
7744
  }
7444
7745
  }
7445
7746
 
7747
+ // 8. For Antigravity, remove the MCP companion entry from mcp_config.json
7748
+ // (#2096 Phase B Upgrade 2). Only the GSD-owned mcpServers.gsd key is
7749
+ // removed — any other user-configured MCP servers are preserved.
7750
+ if (resolveInstallPlan(runtime).finishPermissionWriter === 'antigravity') {
7751
+ const mcpConfigPath = path.join(targetDir, 'mcp_config.json');
7752
+ if (fs.existsSync(mcpConfigPath)) {
7753
+ try {
7754
+ const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
7755
+ if (mcpConfig && typeof mcpConfig === 'object' && mcpConfig.mcpServers && mcpConfig.mcpServers.gsd !== undefined) {
7756
+ delete mcpConfig.mcpServers.gsd;
7757
+ if (Object.keys(mcpConfig.mcpServers).length === 0) {
7758
+ delete mcpConfig.mcpServers;
7759
+ }
7760
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
7761
+ removedCount++;
7762
+ console.log(` ${green}✓${reset} Removed GSD MCP companion server from mcp_config.json`);
7763
+ }
7764
+ } catch (e) {
7765
+ // Ignore JSON parse errors
7766
+ }
7767
+ }
7768
+ }
7769
+
7446
7770
  // Remove the file manifest that the installer wrote at install time.
7447
7771
  // Without this step the metadata file persists after uninstall (#1908).
7448
7772
  const manifestPath = path.join(targetDir, MANIFEST_NAME);
@@ -7696,6 +8020,193 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
7696
8020
  console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
7697
8021
  }
7698
8022
 
8023
+ /**
8024
+ * Convert an absolute path to a `~`-relative form when it lives under the
8025
+ * user's home directory (generalizes configureKiloPermissions'
8026
+ * single-default-dir shorthand to Antigravity's three probed sibling config
8027
+ * dirs — antigravity/antigravity-ide/antigravity-cli under ~/.gemini — none of
8028
+ * which is a single fixed "default").
8029
+ */
8030
+ function toTildePosixPath(absPath) {
8031
+ const posixPath = absPath.replace(/\\/g, '/');
8032
+ const posixHome = os.homedir().replace(/\\/g, '/');
8033
+ return posixPath === posixHome || posixPath.startsWith(`${posixHome}/`)
8034
+ ? `~${posixPath.slice(posixHome.length)}`
8035
+ : posixPath;
8036
+ }
8037
+
8038
+ /**
8039
+ * Antigravity permission rule strings this installer contributes.
8040
+ * Schema: antigravity.google/docs/cli/permissions — "action(target)" rule
8041
+ * strings in permissions.{allow,deny,ask}, evaluated deny > ask > allow. GSD
8042
+ * only ever contributes to `allow` — never deny/ask (those are user-owned risk
8043
+ * decisions this installer has no business making).
8044
+ */
8045
+ function buildAntigravityAllowRules(configDir) {
8046
+ const gsdPath = toTildePosixPath(configDir);
8047
+ return [
8048
+ `read_file(${gsdPath}/gsd-core/*)`,
8049
+ `read_file(${gsdPath}/agents/gsd-*)`,
8050
+ `read_file(${gsdPath}/skills/gsd-*)`,
8051
+ `command(node ${gsdPath}/hooks/*)`,
8052
+ ];
8053
+ }
8054
+
8055
+ /**
8056
+ * Configure Antigravity permissions to allow reading/executing GSD's installed
8057
+ * tree without per-call approval prompts (#2096 Phase B Upgrade 1 — mirrors
8058
+ * configureKiloPermissions/configureOpencodePermissions).
8059
+ *
8060
+ * Antigravity's permission schema (antigravity.google/docs/cli/permissions) is
8061
+ * `{"permissions":{"allow":[...],"deny":[...],"ask":[...]}}`, living in the
8062
+ * SAME settings.json GSD's own hook registration writes for this runtime
8063
+ * (installSurface: 'settings-json', writesSharedSettings: true) — unlike
8064
+ * Kilo/OpenCode, which write a separate native config file. This function
8065
+ * re-reads the file (already containing GSD's hooks by the time finishInstall
8066
+ * reaches this call) and only appends to permissions.allow.
8067
+ *
8068
+ * Non-destructive + idempotent: only `permissions.allow` is touched; an
8069
+ * existing user permissions block (including any deny/ask entries, or
8070
+ * unrelated allow entries) is preserved untouched.
8071
+ *
8072
+ * @param {boolean} isGlobal - Whether this is a global or local install
8073
+ * @param {string|null} configDir - Resolved config directory when already known
8074
+ */
8075
+ function configureAntigravityPermissions(isGlobal = true, configDir = null) {
8076
+ // For local installs, use ./.agents/ (GSD's antigravity localConfigDir)
8077
+ // For global installs, use the resolved ~/.gemini/antigravity{,-ide,-cli}
8078
+ const antigravityConfigDir = configDir || (isGlobal
8079
+ ? getGlobalConfigDir('antigravity', explicitConfigDir)
8080
+ : path.join(process.cwd(), '.agents'));
8081
+ // Ensure config directory exists
8082
+ fs.mkdirSync(antigravityConfigDir, { recursive: true });
8083
+
8084
+ const configPath = path.join(antigravityConfigDir, 'settings.json');
8085
+
8086
+ // Read existing settings.json (readSettings tolerates JSONC + missing file;
8087
+ // returns null — and warns — only when the file exists but fails to parse).
8088
+ const config = readSettings(configPath);
8089
+ if (config === null) {
8090
+ // Cannot parse — DO NOT overwrite user's config (readSettings already warned).
8091
+ return;
8092
+ }
8093
+
8094
+ // Ensure permission structure exists
8095
+ if (!config.permissions || typeof config.permissions !== 'object' || Array.isArray(config.permissions)) {
8096
+ config.permissions = {};
8097
+ }
8098
+ if (!Array.isArray(config.permissions.allow)) {
8099
+ config.permissions.allow = [];
8100
+ }
8101
+
8102
+ let modified = false;
8103
+ for (const rule of buildAntigravityAllowRules(antigravityConfigDir)) {
8104
+ if (!config.permissions.allow.includes(rule)) {
8105
+ config.permissions.allow.push(rule);
8106
+ modified = true;
8107
+ }
8108
+ }
8109
+
8110
+ if (!modified) {
8111
+ return; // Already configured
8112
+ }
8113
+
8114
+ writeSettings(configPath, config);
8115
+ console.log(` ${green}✓${reset} Configured Antigravity permissions for GSD paths`);
8116
+ }
8117
+
8118
+ /**
8119
+ * Configure Antigravity's MCP companion server config (#2096 Phase B
8120
+ * Upgrade 2).
8121
+ *
8122
+ * Antigravity CLI manages MCP servers via standalone `mcp_config.json`
8123
+ * profiles rather than nesting them in settings.json (antigravity.google/docs/
8124
+ * cli/gcli-migration: "Antigravity CLI uses standalone mcp_config.json
8125
+ * profiles in ~/.gemini/config/ for global servers and .agents/mcp_config.json
8126
+ * for workspace servers"). The raw schema for the Antigravity IDE surface
8127
+ * itself is unpublished (docs are JS-rendered), so this follows the CLI's
8128
+ * documented standalone-profile convention plus the standard Gemini/MCP
8129
+ * `mcpServers` shape.
8130
+ *
8131
+ * BEST-EFFORT PATH CHOICE: rather than the CLI doc's separate `~/.gemini/config/`
8132
+ * directory for global scope, this writes `<configDir>/mcp_config.json` — the
8133
+ * SAME resolved configDir as settings.json (configureAntigravityPermissions) —
8134
+ * because (1) GSD's own antigravity configDir resolution already varies
8135
+ * per-user across three sibling dirs (antigravity/antigravity-ide/
8136
+ * antigravity-cli — see resolveAntigravityGlobalDir), so a hardcoded separate
8137
+ * shared path would not track that resolution, and (2) it matches the doc's
8138
+ * OWN workspace-scope convention exactly (`.agents/mcp_config.json`, which IS
8139
+ * GSD's local configDir for antigravity), keeping global/local symmetric and
8140
+ * consistent with the configDir-relative convention every other GSD
8141
+ * permission writer (kilo/opencode) already uses.
8142
+ *
8143
+ * Non-destructive + idempotent: only adds mcpServers.gsd when entirely absent;
8144
+ * any other user-configured mcpServers entries (or a user's OWN "gsd" override)
8145
+ * are preserved untouched (Hyrum's Law — mirrors OpenCode's config.mcp.gsd guard).
8146
+ *
8147
+ * @param {boolean} isGlobal - Whether this is a global or local install
8148
+ * @param {string|null} configDir - Resolved config directory when already known
8149
+ */
8150
+ function configureAntigravityMcpConfig(isGlobal = true, configDir = null) {
8151
+ const antigravityConfigDir = configDir || (isGlobal
8152
+ ? getGlobalConfigDir('antigravity', explicitConfigDir)
8153
+ : path.join(process.cwd(), '.agents'));
8154
+ fs.mkdirSync(antigravityConfigDir, { recursive: true });
8155
+
8156
+ const configPath = path.join(antigravityConfigDir, 'mcp_config.json');
8157
+
8158
+ let config = {};
8159
+ if (fs.existsSync(configPath)) {
8160
+ try {
8161
+ const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
8162
+ config = (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {};
8163
+ } catch (e) {
8164
+ // Cannot parse - DO NOT overwrite user's config
8165
+ console.log(` ${yellow}⚠${reset} Could not parse mcp_config.json - skipping MCP companion config`);
8166
+ console.log(` ${dim}Reason: ${e.message}${reset}`);
8167
+ console.log(` ${dim}Your config was NOT modified. Fix the syntax manually if needed.${reset}`);
8168
+ return;
8169
+ }
8170
+ }
8171
+
8172
+ if (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
8173
+ config.mcpServers = {};
8174
+ }
8175
+
8176
+ if (config.mcpServers.gsd !== undefined) {
8177
+ return; // Already configured (or a user-owned override) — never clobber.
8178
+ }
8179
+
8180
+ config.mcpServers.gsd = {
8181
+ command: 'npx',
8182
+ args: ['-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'],
8183
+ };
8184
+
8185
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
8186
+ console.log(` ${green}✓${reset} Configured Antigravity MCP companion server (gsd)`);
8187
+ }
8188
+
8189
+ /**
8190
+ * #2097 (ADR-1239 transport:mcp): register the GSD companion MCP server inside a
8191
+ * runtime's settings.json (Augment hosts MCP in settings.json.mcpServers, unlike
8192
+ * Antigravity's standalone mcp_config.json). Mutates the in-memory settings object
8193
+ * that finishInstall already writes — non-destructive + idempotent: only sets
8194
+ * mcpServers.gsd, preserving any user-defined servers (a user's own `gsd` override
8195
+ * is respected — Hyrum's Law).
8196
+ * @param {object} settings - the in-memory settings object finishInstall will write
8197
+ */
8198
+ function mergeGsdMcpServerIntoSettings(settings) {
8199
+ if (!settings.mcpServers || typeof settings.mcpServers !== 'object' || Array.isArray(settings.mcpServers)) {
8200
+ settings.mcpServers = {};
8201
+ }
8202
+ if (settings.mcpServers.gsd === undefined) {
8203
+ settings.mcpServers.gsd = {
8204
+ command: 'npx',
8205
+ args: ['-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'],
8206
+ };
8207
+ }
8208
+ }
8209
+
7699
8210
  /**
7700
8211
  * Verify a directory exists and contains files
7701
8212
  */
@@ -7805,7 +8316,18 @@ function resolveInstallRelativePath(baseDir, relPath) {
7805
8316
  * Write file manifest after installation for future modification detection
7806
8317
  */
7807
8318
  function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
7808
- const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
8319
+ // #2093: isKilo dropped — unused in this function.
8320
+ // #2094: isTrae dropped — was only used in the hooks-tracking conditional
8321
+ // above, now covered by hostBehaviors.skipSharedHooksInstall.
8322
+ // #2095: isKimi dropped — kimi is now a hooks/ consumer like every other
8323
+ // settings-json-adjacent runtime, so the `&& !isKimi` term below was removed.
8324
+ // #2096: isAntigravity dropped — unused in this function.
8325
+ // #2098: isCodebuddy dropped — unused in this function.
8326
+ // #2099: isCopilot dropped — was only used in the hooks-tracking conditional
8327
+ // above, now covered by hostBehaviors.skipSharedHooksInstall.
8328
+ // #2100: isWindsurf dropped — was only used in the hooks-tracking conditional
8329
+ // above, now covered by hostBehaviors.skipSharedHooksInstall.
8330
+ const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
7809
8331
  const gsdDir = path.join(configDir, 'gsd-core');
7810
8332
  // #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
7811
8333
  // Claude local uses flatCommandsDir instead for manifest recording.
@@ -7879,7 +8401,7 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
7879
8401
  }
7880
8402
  }
7881
8403
  }
7882
- if (isKimi && fs.existsSync(agentsDir)) {
8404
+ if (_hostBehaviors(runtime).agentManifestStyle === 'kimi-nested' && fs.existsSync(agentsDir)) {
7883
8405
  const agentHashes = generateManifest(agentsDir);
7884
8406
  for (const [rel, hash] of Object.entries(agentHashes)) {
7885
8407
  const isRootAgent = rel === 'gsd.yaml' || rel === 'gsd.md';
@@ -7913,7 +8435,15 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
7913
8435
  // Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline)
7914
8436
  // Descriptor-driven (ADR-1239 / #2089+#2090): cline's exclusion is via
7915
8437
  // hostBehaviors.skipSharedHooksInstall (was hardcoded !isCline).
7916
- if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isTrae && !isKimi) {
8438
+ // #2094: Trae's exclusion is likewise descriptor-driven (trae declares
8439
+ // skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed.
8440
+ // #2095: kimi is now a hooks/ consumer (native config.toml [[hooks]] bus) —
8441
+ // the redundant `&& !isKimi` was removed so its hook files are tracked too.
8442
+ // #2099: Copilot's exclusion is likewise descriptor-driven (copilot declares
8443
+ // skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed.
8444
+ // #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares
8445
+ // skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed.
8446
+ if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) {
7917
8447
  const hooksDir = path.join(configDir, 'hooks');
7918
8448
  if (fs.existsSync(hooksDir)) {
7919
8449
  // Drive from INSTALLED_HOOK_FILES (the canonical HOOKS_TO_COPY set from
@@ -8274,11 +8804,7 @@ function reportLocalPatches(configDir, runtime = DEFAULT_RUNTIME) {
8274
8804
  try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
8275
8805
 
8276
8806
  if (meta.files && meta.files.length > 0) {
8277
- const reapplyCommand = _hostBehaviors(runtime).reapplyCommand
8278
- ? _hostBehaviors(runtime).reapplyCommand
8279
- : runtime === 'kimi'
8280
- ? '/skill:gsd-update --reapply'
8281
- : '/gsd-update --reapply';
8807
+ const reapplyCommand = _hostBehaviors(runtime).reapplyCommand || '/gsd-update --reapply';
8282
8808
  console.log('');
8283
8809
  console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
8284
8810
  for (const f of meta.files) {
@@ -8305,12 +8831,44 @@ function reportInstallerMigrationResult(result) {
8305
8831
  }
8306
8832
 
8307
8833
  function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
8308
- const { isOpencode, isKilo, isZcode, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
8834
+ // #2093: isKilo dropped — Kilo's agent/model-override handling below reads
8835
+ // _hostBehaviors(runtime).frontmatterDialect === 'kilo' instead of this flag.
8836
+ // #2095: isKimi dropped — kimi is now a hooks/ consumer like every other
8837
+ // settings-json-adjacent runtime; the two `&& !isKimi` hooks-copy guards
8838
+ // below were removed, leaving isKimi unused in this function (the kimi
8839
+ // local-install-deferred branch above already reads
8840
+ // _hostBehaviors(runtime).localInstallDeferred instead of this flag).
8841
+ // #2096: isAntigravity dropped — antigravity is in
8842
+ // _DESCRIPTOR_AGENTS_RUNTIMES below, so its two legacy-agent-loop branches
8843
+ // (the path-rewrite skip and the converter dispatch) were unreachable dead
8844
+ // code; both were removed rather than re-gated on hostBehaviors.
8845
+ // #2098: isCodebuddy dropped — codebuddy is also in
8846
+ // _DESCRIPTOR_AGENTS_RUNTIMES below, so its legacy converter-dispatch branch
8847
+ // (the `isCodebuddy` arm calling convertClaudeAgentToCodebuddyAgent) was
8848
+ // unreachable dead code and was removed rather than re-gated.
8849
+ // #2099: isCopilot dropped — copilot is also in _DESCRIPTOR_AGENTS_RUNTIMES
8850
+ // below, so its three legacy-agent-loop branches (the path-rewrite skip,
8851
+ // the converter dispatch, and the .agent.md destName ternary) were
8852
+ // unreachable dead code and were removed rather than re-gated; the
8853
+ // .agent.md suffix now lives on hostBehaviors.agentFileExtension in
8854
+ // src/install-engine.cts, and the skipSharedHooksInstall check above no
8855
+ // longer needs `&& !isCopilot`.
8856
+ // #2100: isWindsurf dropped — its four former isWindsurf-gated branches
8857
+ // (legacy .devin/skills/gsd-* cleanup, the #1629 command-bodies copy, the
8858
+ // workflow-verification report, and the shared-hooks-install exclusion) are
8859
+ // now descriptor-driven via hostBehaviors.legacyDevinSkillsCleanup,
8860
+ // hostBehaviors.installsCommandBodiesForWorkflowDelegation,
8861
+ // hostBehaviors.verificationStyle === 'windsurf-workflows', and
8862
+ // hostBehaviors.skipSharedHooksInstall respectively; its legacy-agent-loop
8863
+ // converter arm was likewise unreachable dead code (windsurf is in
8864
+ // _DESCRIPTOR_AGENTS_RUNTIMES) and was removed above.
8865
+ // #2101: isZcode dropped — folded onto hostBehaviors.skipSharedHooksInstall.
8866
+ const { isOpencode, isCodex, isCursor, isAugment, isTrae, isQwen, isHermes, isCline } = runtimeFlags(runtime);
8309
8867
  const plan = resolveInstallPlan(runtime);
8310
8868
  const dirName = getDirName(runtime);
8311
8869
  const src = path.join(__dirname, '..');
8312
8870
 
8313
- if (isKimi && !isGlobal) {
8871
+ if (_hostBehaviors(runtime).localInstallDeferred && !isGlobal) {
8314
8872
  console.log(` ${yellow}⚠${reset} Kimi local install is deferred for Phase 2.`);
8315
8873
  console.log(` No .kimi-code/skills or .agents/skills project artifacts were written.`);
8316
8874
  console.log(` Project-level Kimi install semantics remain deferred.`);
@@ -8815,7 +9373,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
8815
9373
  // dirs from pre-#1615 installs. #1615 moved Windsurf to .windsurf/workflows/
8816
9374
  // but never cleaned up the old .devin/skills/ layout (#1085). User-owned
8817
9375
  // content is preserved (non-gsd- dirs, gsd-dev-preferences, symlinks).
8818
- if (isWindsurf && !isGlobal) {
9376
+ // Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into
9377
+ // hostBehaviors.legacyDevinSkillsCleanup (windsurf is the only runtime that
9378
+ // declares it, so this is byte-parity).
9379
+ if (_hostBehaviors(runtime).legacyDevinSkillsCleanup && !isGlobal) {
8819
9380
  const removedCount = cleanupWindsurfLegacyDevinSkills(process.cwd());
8820
9381
  if (removedCount > 0) {
8821
9382
  console.log(` ${green}✓${reset} Removed ${removedCount} legacy .devin/skills/gsd-* dir(s) (pre-#1615 Windsurf layout)`);
@@ -8842,7 +9403,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
8842
9403
  } else {
8843
9404
  failures.push('skills/gsd/*');
8844
9405
  }
8845
- } else if (isKimi) {
9406
+ } else if (_hostBehaviors(runtime).verificationStyle === 'kimi') {
8846
9407
  const skillsDir = path.join(targetDir, 'skills');
8847
9408
  const rootAgentPath = path.join(targetDir, 'agents', 'gsd.yaml');
8848
9409
  if (fs.existsSync(skillsDir)) {
@@ -8862,7 +9423,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
8862
9423
  } else {
8863
9424
  failures.push('agents/gsd.yaml');
8864
9425
  }
8865
- } else if (isWindsurf) {
9426
+ // Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into
9427
+ // hostBehaviors.verificationStyle === 'windsurf-workflows' (extends the
9428
+ // same mechanism the 'kimi' verificationStyle branch above uses; windsurf
9429
+ // is the only runtime that declares this value, so this is byte-parity).
9430
+ } else if (_hostBehaviors(runtime).verificationStyle === 'windsurf-workflows') {
8866
9431
  if (isGlobal) {
8867
9432
  console.log(` ${green}✓${reset} Windsurf global install skipped workflow artifacts (workspace-only)`);
8868
9433
  } else {
@@ -8924,22 +9489,6 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
8924
9489
  failures.push('commands/gsd-*');
8925
9490
  }
8926
9491
  }
8927
-
8928
- // CodeBuddy only: also report the commands/ output (#789 — slash commands)
8929
- if (isCodebuddy) {
8930
- const commandsDir = path.join(targetDir, 'commands');
8931
- if (fs.existsSync(commandsDir)) {
8932
- const cmdCount = fs.readdirSync(commandsDir)
8933
- .filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length;
8934
- if (cmdCount > 0) {
8935
- console.log(` ${green}✓${reset} Installed ${cmdCount} slash commands to commands/`);
8936
- } else {
8937
- failures.push('commands/gsd-*');
8938
- }
8939
- } else {
8940
- failures.push('commands/gsd-*');
8941
- }
8942
- }
8943
9492
  }
8944
9493
  } else if (_hostBehaviors(runtime).localCommandsViaRules) {
8945
9494
  // Cline local install: rules-based only — commands are embedded in .clinerules (generated below).
@@ -8948,6 +9497,16 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
8948
9497
  // Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
8949
9498
  // hostBehaviors.localCommandsViaRules.
8950
9499
  console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`);
9500
+ } else if (_hostBehaviors(runtime).pluginOnlyInstall) {
9501
+ // pi (ADR-1239 / #2102 Stage 1): plugin-only install — pi's /gsd command is
9502
+ // registered programmatically by the native extension (pi/gsd.cjs →
9503
+ // extensions/gsd.cjs, staged separately below) and dispatches in-process
9504
+ // through the embedded gsd-core command-routing hub. pi has no host-read
9505
+ // markdown surface (unlike Claude/OpenCode/etc., which scan commands/ or
9506
+ // command/ directories), so writing flat gsd-<cmd>.md files here would be
9507
+ // dead weight the extension never reads. Skip the flat-commands fallback
9508
+ // entirely for pluginOnlyInstall runtimes.
9509
+ console.log(` ${green}✓${reset} pi: /gsd registered via native extension (no declarative command files)`);
8951
9510
  } else {
8952
9511
  // Claude Code local: flat gsd-<cmd>.md layout — Claude Code registers
8953
9512
  // commands from .claude/commands/ using the filename stem as the command
@@ -9018,6 +9577,18 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
9018
9577
  }
9019
9578
  }
9020
9579
 
9580
+ // Native-extension/plugin staging for runtimes OUTSIDE the layout-driven
9581
+ // _isSkillsRuntime branch above (ADR-1239 / #2102 Stage 1: pi). OpenCode/Kilo
9582
+ // already get their nativePlugin file from installOpencodeFamilyArtifacts
9583
+ // (called inside the _isSkillsRuntime branch, since both declare a non-empty
9584
+ // artifactLayout) — guard on `!_isSkillsRuntime` so this standalone call never
9585
+ // double-stages their plugin file. A runtime like pi, whose artifactLayout is
9586
+ // intentionally empty for both scopes (`_isSkillsRuntime` is false), still
9587
+ // needs its declared hostBehaviors.nativePlugin file copied into targetDir.
9588
+ if (!_isSkillsRuntime && _hostBehaviors(runtime).nativePlugin) {
9589
+ _installNativePluginIfDeclared(runtime, targetDir, _hostBehaviors(runtime), src);
9590
+ }
9591
+
9021
9592
  // Copy gsd-core skill with path replacement
9022
9593
  // Preserve user-generated files before the wipe-and-copy so they survive re-install
9023
9594
  const skillSrc = path.join(src, 'gsd-core');
@@ -9069,7 +9640,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
9069
9640
  // this copy, every /gsd-* workflow in Cascade references a missing file and the LLM
9070
9641
  // cannot execute the command body. Surfaced by the #1629 regression test after the
9071
9642
  // original adversarial review of #1622 missed it.
9072
- if (isWindsurf && !isGlobal) {
9643
+ // Descriptor-driven (ADR-1239 / #2100): folded from `isWindsurf` into
9644
+ // hostBehaviors.installsCommandBodiesForWorkflowDelegation (windsurf is the
9645
+ // only runtime that declares it, so this is byte-parity — the #1629 fix
9646
+ // itself is unchanged).
9647
+ if (_hostBehaviors(runtime).installsCommandBodiesForWorkflowDelegation && !isGlobal) {
9073
9648
  const commandsSrc = path.join(src, 'commands', 'gsd');
9074
9649
  const commandsDest = path.join(skillDest, 'commands', 'gsd');
9075
9650
  if (fs.existsSync(commandsSrc)) {
@@ -9125,16 +9700,24 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
9125
9700
  // Trivial group (cursor/windsurf/augment/trae/codebuddy) cut over together.
9126
9701
  // #1575: copilot and antigravity cut over — copilot gets .agent.md filename
9127
9702
  // rename via _copyStaged(runtime); antigravity uses scope-aware converter.
9703
+ // #2092 Phase B Upgrade 1: qwen cut over — native .qwen/agents/*.md subagent
9704
+ // projection via convertClaudeAgentToQwenAgent. Without this exclusion the
9705
+ // legacy inline loop below deletes+re-copies qwen's agents RAW (bypassing the
9706
+ // new converter entirely, since qwen has no dedicated branch in the inline
9707
+ // loop's if/else-if chain — it would silently fall through to the generic
9708
+ // brandingRewrites-only branch).
9128
9709
  // cline remains excluded: rules-only local branch + local/global complication
9129
9710
  // that the descriptor-driven path does not handle correctly.
9130
- const _DESCRIPTOR_AGENTS_RUNTIMES = new Set(['cursor', 'windsurf', 'augment', 'trae', 'codebuddy', 'copilot', 'antigravity']);
9711
+ const _DESCRIPTOR_AGENTS_RUNTIMES = new Set(['cursor', 'windsurf', 'augment', 'trae', 'codebuddy', 'copilot', 'antigravity', 'qwen', 'kimi']);
9131
9712
 
9132
9713
  // Always remove stale gsd-* agents first so re-installing with
9133
9714
  // `--minimal` actually shrinks a previously-full install.
9134
9715
  // For Codex this also covers per-agent `.toml` files alongside the `.md`
9135
9716
  // sources so a full → minimal switch doesn't leave stale registrations.
9136
- // Skipped for descriptor-agent runtimes (installRuntimeArtifacts prunes).
9137
- if (!_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime) && fs.existsSync(agentsDest)) {
9717
+ // Skipped for descriptor-agent runtimes (installRuntimeArtifacts prunes) and
9718
+ // for pluginOnlyInstall runtimes (pi, ADR-1239 / #2102 Stage 1 — no agents/
9719
+ // dir is ever written for them, see the leading branch below).
9720
+ if (!_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime) && !_hostBehaviors(runtime).pluginOnlyInstall && fs.existsSync(agentsDest)) {
9138
9721
  for (const file of fs.readdirSync(agentsDest)) {
9139
9722
  if (
9140
9723
  file.startsWith('gsd-') &&
@@ -9145,8 +9728,11 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
9145
9728
  }
9146
9729
  }
9147
9730
 
9148
- if (isKimi) {
9149
- console.log(` ${dim}↳${reset} Kimi custom agent YAML/prompt artifacts were installed via runtime artifact layout`);
9731
+ if (_hostBehaviors(runtime).pluginOnlyInstall) {
9732
+ // pi (ADR-1239 / #2102 Stage 1): programmatic dispatch has no named-dispatch
9733
+ // subagent toolkit (dispatch.subagentToolkit: "undocumented", no Agent-tool
9734
+ // equivalent) and no host-read markdown surface — skip writing agents/ entirely.
9735
+ console.log(` ${green}✓${reset} pi: no subagent files (programmatic dispatch, no named-dispatch toolkit)`);
9150
9736
  } else if (_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime)) {
9151
9737
  // installRuntimeArtifacts already wrote agents + handles stale-file cleanup
9152
9738
  // via its own prune pass. No further action needed.
@@ -9183,12 +9769,18 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
9183
9769
  const bareDirRegex = /~\/\.claude\b/g;
9184
9770
  const bareHomeDirRegex = /\$HOME\/\.claude\b/g;
9185
9771
  const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
9186
- if (!isCopilot && !isAntigravity) {
9187
- content = content.replace(dirRegex, pathPrefix);
9188
- content = content.replace(homeDirRegex, pathPrefix);
9189
- content = content.replace(bareDirRegex, normalizedPathPrefix);
9190
- content = content.replace(bareHomeDirRegex, normalizedPathPrefix);
9191
- }
9772
+ // #2096: `&& !isAntigravity` dropped — antigravity is in
9773
+ // _DESCRIPTOR_AGENTS_RUNTIMES above, so this whole branch is already
9774
+ // unreachable for it; the path-rewrite skip for antigravity now lives
9775
+ // in the descriptor-driven `applyAgentPathRewrites` (hostBehaviors.noPathRewrite).
9776
+ // #2099: `if (!isCopilot)` guard dropped — copilot is ALSO in
9777
+ // _DESCRIPTOR_AGENTS_RUNTIMES (line ~9564 above), so this whole
9778
+ // `else if (fs.existsSync(agentsSrc))` branch is unreachable for it;
9779
+ // isCopilot was therefore always false here, making the guard a no-op.
9780
+ content = content.replace(dirRegex, pathPrefix);
9781
+ content = content.replace(homeDirRegex, pathPrefix);
9782
+ content = content.replace(bareDirRegex, normalizedPathPrefix);
9783
+ content = content.replace(bareHomeDirRegex, normalizedPathPrefix);
9192
9784
  content = processAttribution(content, getCommitAttribution(runtime));
9193
9785
  // Convert frontmatter for runtime compatibility (agents need different handling)
9194
9786
  if (_hostBehaviors(runtime).frontmatterDialect === 'opencode') {
@@ -9211,33 +9803,52 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
9211
9803
  }
9212
9804
  content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true, modelOverride: _ocModelOverride });
9213
9805
  } else if (_hostBehaviors(runtime).frontmatterDialect === 'kilo') {
9214
- content = convertClaudeToKiloFrontmatter(content, { isAgent: true });
9806
+ // Resolve per-agent model for Kilo agents (#2093 UPGRADE 2; Kilo is an
9807
+ // OpenCode fork with the same static-frontmatter model constraint).
9808
+ // Precedence: model_overrides[agent] > model_profile_overrides.kilo.<tier> > omit.
9809
+ // model_overrides (#2256): explicit per-agent override, highest precedence.
9810
+ // model_profile_overrides (#2794): tier-based runtime resolver, same parity as OpenCode.
9811
+ const _kiloAgentName = entry.name.replace(/\.md$/, '');
9812
+ const _kiloModelOverrides = readGsdEffectiveModelOverrides(targetDir);
9813
+ let _kiloModelOverride = _kiloModelOverrides?.[_kiloAgentName] || null;
9814
+ if (!_kiloModelOverride) {
9815
+ // Fall back to tier-based resolution via model_profile_overrides.kilo.<tier>.
9816
+ const _kiloRuntimeResolver = readGsdRuntimeProfileResolver(targetDir);
9817
+ if (_kiloRuntimeResolver) {
9818
+ const _kiloEntry = _kiloRuntimeResolver.resolve(_kiloAgentName);
9819
+ if (_kiloEntry?.model) {
9820
+ _kiloModelOverride = _kiloEntry.model;
9821
+ }
9822
+ }
9823
+ }
9824
+ content = convertClaudeToKiloFrontmatter(content, { isAgent: true, modelOverride: _kiloModelOverride });
9215
9825
  } else if (_hostBehaviors(runtime).frontmatterDialect === 'codex') {
9216
9826
  content = convertClaudeAgentToCodexAgent(content);
9217
- } else if (isCopilot) {
9218
- content = convertClaudeAgentToCopilotAgent(content, isGlobal);
9219
- } else if (isAntigravity) {
9220
- content = convertClaudeAgentToAntigravityAgent(content, isGlobal);
9221
- } else if (isWindsurf) {
9222
- content = convertClaudeAgentToWindsurfAgent(content);
9223
- } else if (isAugment) {
9224
- content = convertClaudeAgentToAugmentAgent(content);
9225
- } else if (isTrae) {
9226
- content = convertClaudeAgentToTraeAgent(content);
9227
- } else if (isCodebuddy) {
9228
- content = convertClaudeAgentToCodebuddyAgent(content);
9827
+ // #2099: `else if (isCopilot)` arm dropped — copilot is unreachable
9828
+ // here (see the isCopilot-guard-drop comment above); its content
9829
+ // conversion is applied pre-staging via the descriptor's
9830
+ // artifactLayout.converter (runtime-artifact-layout.cts), independent
9831
+ // of this legacy loop.
9832
+ // #2100: `else if (isWindsurf)` arm dropped — windsurf is ALSO in
9833
+ // _DESCRIPTOR_AGENTS_RUNTIMES (line ~9575 above), so this whole
9834
+ // `else if (fs.existsSync(agentsSrc))` branch is unreachable for it;
9835
+ // isWindsurf was therefore always false here, making the arm dead.
9836
+ // Its content conversion is applied pre-staging via the descriptor's
9837
+ // artifactLayout.converter (convertClaudeAgentToWindsurfAgent),
9838
+ // independent of this legacy loop.
9229
9839
  } else if (_hostBehaviors(runtime).frontmatterDialect === 'cline') {
9230
9840
  // Descriptor-driven (ADR-1239 / #2090): folded from `isCline` into
9231
9841
  // hostBehaviors.frontmatterDialect === 'cline'.
9232
9842
  content = convertClaudeAgentToClineAgent(content);
9233
- } else if (isQwen) {
9234
- content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
9235
- content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
9236
- content = content.replace(/\.claude\//g, '.qwen/');
9237
9843
  } else if (_hostBehaviors(runtime).brandingRewrites) {
9238
- content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
9239
- content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
9240
- content = content.replace(/\.claude\//g, '.hermes/');
9844
+ // Descriptor-driven (ADR-1239 / #2092): folded from separate
9845
+ // `isQwen` / hermes-hardcoded branches into a single read of
9846
+ // runtime.hostBehaviors.brandingRewrites (qwen -> QWEN.md/Qwen
9847
+ // Code/.qwen/, hermes -> HERMES.md/Hermes Agent/.hermes/).
9848
+ const _b = _hostBehaviors(runtime).brandingRewrites;
9849
+ content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
9850
+ content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
9851
+ content = content.replace(/\.claude\//g, _b['.claude/']);
9241
9852
  }
9242
9853
  // #443 — Inject `effort:` into the Claude .md frontmatter ONLY.
9243
9854
  // OpenCode/Qwen/Hermes also produce .md files but break on
@@ -9262,7 +9873,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
9262
9873
  // shouldNormalizeHyphenNamespaceInAgentBody above. Mirrors the
9263
9874
  // SKILL.md-body fix shipped via #3629.
9264
9875
  content = normalizeAgentBodyForRuntime(content, runtime, readGsdCommandNames());
9265
- const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name;
9876
+ // #2099: `isCopilot ? ... : entry.name` ternary dropped — copilot is
9877
+ // unreachable here (see the isCopilot-guard-drop comment above), so
9878
+ // the ternary always evaluated to entry.name in practice; its
9879
+ // .agent.md suffix is applied by the descriptor-driven fold in
9880
+ // src/install-engine.cts (hostBehaviors.agentFileExtension).
9881
+ const destName = entry.name;
9266
9882
  fs.writeFileSync(path.join(agentsDest, destName), content);
9267
9883
  }
9268
9884
  }
@@ -9294,29 +9910,41 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
9294
9910
  failures.push('VERSION');
9295
9911
  }
9296
9912
 
9297
- // #1821: Kilo and ZCode declare hooksSurface:'none' AND have no plugin surface,
9298
- // so the staged hook scripts are dead weight for them — exclude both here.
9299
- // OpenCode also declares hooksSurface:'none' but is deliberately NOT excluded:
9300
- // its native plugin adapter (#1914, installed above under plugins/gsd-core.js)
9301
- // spawns the staged hooks/*.js scripts via OpenCode's event bus and needs both
9302
- // them and the CommonJS package.json marker written below.
9303
- // #2089: Cursor's exclusion is now descriptor-driven via
9304
- // hostBehaviors.skipSharedHooksInstall (was hardcoded !isCursor).
9305
- // #2090: Cline's exclusion is likewise descriptor-driven (cline declares
9306
- // skipSharedHooksInstall:true) — the redundant `&& !isCline` was removed.
9307
- if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isTrae && !isKimi && !isKilo && !isZcode) {
9913
+ // Reusable: copy hooks/dist/ + hooks/lib/ into destRootDir, writing the
9914
+ // CommonJS package.json marker alongside them. Used below for the generic
9915
+ // configDir install path (guarded by hostBehaviors.skipSharedHooksInstall),
9916
+ // and — since #2095 — for Kimi's OWN native hook-install root (~/.kimi,
9917
+ // resolved by resolveKimiHooksTomlDir), a directory entirely separate from
9918
+ // Kimi's configDir/agents-root. Kimi's contract forbids hooks/ or
9919
+ // package.json under its generic Agent-Skills root (see
9920
+ // capabilities/kimi/capability.json hostBehaviors.skipSharedHooksInstall
9921
+ // and the kimi-hooks-toml branch further below), so its shared-hooks bundle
9922
+ // is installed into its own root via this same helper instead.
9923
+ // Returns false when hooks/dist/ exists but failed to verify post-copy (a
9924
+ // genuine failure the caller should surface); true otherwise (including
9925
+ // when hooks/dist/ is absent from the package — nothing to verify).
9926
+ function installSharedHooksBundle(destRootDir) {
9927
+ // destRootDir already exists for the generic call site (targetDir — created
9928
+ // earlier in install() by the skills/agents writes above). It does NOT yet
9929
+ // exist for kimi's call site (~/.kimi, resolved by resolveKimiHooksTomlDir):
9930
+ // a fresh install has never created that dir before. mkdirSync recursive is
9931
+ // a safe no-op when the dir is already present.
9932
+ fs.mkdirSync(destRootDir, { recursive: true });
9933
+
9308
9934
  // Write package.json to force CommonJS mode for GSD scripts
9309
9935
  // Prevents "require is not defined" errors when project has "type": "module"
9310
9936
  // Node.js walks up looking for package.json - this stops inheritance from project
9311
- const pkgJsonDest = path.join(targetDir, 'package.json');
9937
+ const pkgJsonDest = path.join(destRootDir, 'package.json');
9312
9938
  fs.writeFileSync(pkgJsonDest, '{"type":"commonjs"}\n');
9313
9939
  console.log(` ${green}✓${reset} Wrote package.json (CommonJS mode)`);
9314
9940
 
9941
+ let hooksOk = true;
9942
+
9315
9943
  // Copy hooks from dist/ (bundled with dependencies)
9316
9944
  // Template paths for the target runtime (replaces '.claude' with correct config dir)
9317
9945
  const hooksSrc = path.join(src, 'hooks', 'dist');
9318
9946
  if (fs.existsSync(hooksSrc)) {
9319
- const hooksDest = path.join(targetDir, 'hooks');
9947
+ const hooksDest = path.join(destRootDir, 'hooks');
9320
9948
  fs.mkdirSync(hooksDest, { recursive: true });
9321
9949
  const hookEntries = fs.readdirSync(hooksSrc);
9322
9950
  const configDirReplacement = getConfigDirFromHome(runtime, isGlobal);
@@ -9329,13 +9957,15 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
9329
9957
  content = content.replace(/'\.claude'/g, configDirReplacement);
9330
9958
  content = content.replace(/\/\.claude\//g, `/${getDirName(runtime)}/`);
9331
9959
  content = content.replace(/\.claude\//g, `${getDirName(runtime)}/`);
9332
- if (isQwen) {
9333
- content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
9334
- content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
9335
- }
9336
- if (_hostBehaviors(runtime).brandingRewrites) {
9337
- content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
9338
- content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
9960
+ // Descriptor-driven (ADR-1239 / #2092): folded from separate
9961
+ // `isQwen` / hermes-hardcoded branches into a single read of
9962
+ // runtime.hostBehaviors.brandingRewrites. This site only
9963
+ // rewrites the two brand-name keys (no `.claude/` here — the
9964
+ // config-dir replace above already handled path fragments).
9965
+ const _b2 = _hostBehaviors(runtime).brandingRewrites;
9966
+ if (_b2) {
9967
+ content = content.replace(/CLAUDE\.md/g, _b2['CLAUDE.md']);
9968
+ content = content.replace(/\bClaude Code\b/g, _b2['Claude Code']);
9339
9969
  }
9340
9970
  // #376: rewrite gsd: → gsd- for hyphen-namespace runtimes
9341
9971
  if (shouldNormalizeHyphenNamespaceInAgentBody(runtime)) {
@@ -9388,27 +10018,63 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
9388
10018
  }
9389
10019
  }
9390
10020
  } else {
9391
- failures.push('hooks');
10021
+ hooksOk = false;
9392
10022
  }
9393
10023
  }
10024
+
10025
+ // Gate hooks/lib/ install on the same set of runtimes that receive hooks/.
10026
+ // Codex/Copilot/Cursor/Windsurf/Trae/Cline/Kilo do not use the shared
10027
+ // hooks/lib/ helpers (Cursor uses standalone .js hook scripts registered
10028
+ // via hooks.json — gated descriptor-driven via
10029
+ // hostBehaviors.skipSharedHooksInstall, #2089; Cline likewise #2090; Kilo
10030
+ // likewise #2093; Trae likewise #2094; Codex uses hooks.json directly;
10031
+ // the others skip hooks entirely); Kilo and ZCode also skip hooks entirely
10032
+ // (hooksSurface:'none' with no plugin surface — #1821). None of the
10033
+ // excluded runtimes must receive the hooks/lib/ helpers — otherwise the
10034
+ // Codex comment downstream ("we deliberately do *not* copy hooks/lib/ for
10035
+ // Codex") is contradicted in practice. (Gating lives at the call sites
10036
+ // below; this helper itself only checks source presence.)
10037
+ const hooksLibSrc = path.join(src, 'hooks', 'lib');
10038
+ if (fs.existsSync(hooksLibSrc)) {
10039
+ const hooksLibDest = path.join(destRootDir, 'hooks', 'lib');
10040
+ fs.mkdirSync(hooksLibDest, { recursive: true });
10041
+ copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);
10042
+ console.log(` ${green}✓${reset} Installed hooks/lib/ helpers (git-cmd, graphify-rebuild, ...)`);
10043
+ }
10044
+
10045
+ return hooksOk;
9394
10046
  }
9395
10047
 
9396
- // Gate hooks/lib/ install on the same runtimes that receive hooks (see line ~8702).
9397
- // Codex/Copilot/Cursor/Windsurf/Trae/Cline do not use the shared hooks/lib/ helpers
9398
- // (Cursor uses standalone .js hook scripts registered via hooks.json — gated
9399
- // descriptor-driven via hostBehaviors.skipSharedHooksInstall, #2089; Cline likewise
9400
- // #2090; Codex uses hooks.json directly; the others skip hooks entirely); Kilo and
9401
- // ZCode also skip hooks entirely (hooksSurface:'none' with no plugin surface — #1821).
9402
- // OpenCode is NOT excluded: its #1914 plugin adapter spawns the staged hooks and
9403
- // requires hooks/lib/ helpers. None of the excluded runtimes must receive the
9404
- // hooks/lib/ helpers — otherwise the Codex comment downstream ("we deliberately do
9405
- // *not* copy hooks/lib/ for Codex") is contradicted in practice.
9406
- const hooksLibSrc = path.join(src, 'hooks', 'lib');
9407
- if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isTrae && !isKimi && !isKilo && !isZcode && fs.existsSync(hooksLibSrc)) {
9408
- const hooksLibDest = path.join(targetDir, 'hooks', 'lib');
9409
- fs.mkdirSync(hooksLibDest, { recursive: true });
9410
- copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);
9411
- console.log(` ${green}✓${reset} Installed hooks/lib/ helpers (git-cmd, graphify-rebuild, ...)`);
10048
+ // #1821: Kilo and ZCode declare hooksSurface:'none' AND have no plugin surface,
10049
+ // so the staged hook scripts are dead weight for them — exclude both here.
10050
+ // OpenCode also declares hooksSurface:'none' but is deliberately NOT excluded:
10051
+ // its native plugin adapter (#1914, installed above under plugins/gsd-core.js)
10052
+ // spawns the staged hooks/*.js scripts via OpenCode's event bus and needs both
10053
+ // them and the CommonJS package.json marker written below.
10054
+ // #2089: Cursor's exclusion is now descriptor-driven via
10055
+ // hostBehaviors.skipSharedHooksInstall (was hardcoded !isCursor).
10056
+ // #2090: Cline's exclusion is likewise descriptor-driven (cline declares
10057
+ // skipSharedHooksInstall:true) — the redundant `&& !isCline` was removed.
10058
+ // #2093: Kilo's exclusion is likewise descriptor-driven (kilo declares
10059
+ // skipSharedHooksInstall:true) — the redundant `&& !isKilo` was removed.
10060
+ // #2094: Trae's exclusion is likewise descriptor-driven (trae declares
10061
+ // skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed.
10062
+ // #2101: ZCode's exclusion is likewise descriptor-driven (zcode declares
10063
+ // skipSharedHooksInstall:true) — the redundant `&& !isZcode` was removed.
10064
+ // #2095: Kimi's exclusion is likewise descriptor-driven (kimi declares
10065
+ // skipSharedHooksInstall:true) — kimi's shared hooks/ + package.json marker
10066
+ // are instead installed into its OWN native hook root (~/.kimi, resolved by
10067
+ // resolveKimiHooksTomlDir) via installSharedHooksBundle, at the
10068
+ // kimi-hooks-toml branch further below — never under the generic
10069
+ // Agent-Skills configDir GSD installs skills/agents into for kimi.
10070
+ // #2099: Copilot's exclusion is likewise descriptor-driven (copilot declares
10071
+ // skipSharedHooksInstall:true) — the redundant `&& !isCopilot` was removed.
10072
+ // #2100: Windsurf's exclusion is likewise descriptor-driven (windsurf declares
10073
+ // skipSharedHooksInstall:true) — the redundant `&& !isWindsurf` was removed.
10074
+ if (!isCodex && _hostBehaviors(runtime).skipSharedHooksInstall !== true) {
10075
+ if (!installSharedHooksBundle(targetDir)) {
10076
+ failures.push('hooks');
10077
+ }
9412
10078
  }
9413
10079
 
9414
10080
  // Install scripts/changeset/ and scripts/lib/ into <configDir>/scripts/
@@ -10030,14 +10696,86 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10030
10696
  } else {
10031
10697
  console.log(` ${green}✓${reset} Cursor lifecycle hooks already up to date`);
10032
10698
  }
10033
- // Re-run the manifest pass so the hook scripts + hooks.json are hash-tracked.
10699
+ // Re-run the manifest pass to capture any files the hooks-json write path
10700
+ // produced. NOTE: hooks.json and the gsd-cursor-*.js scripts are NOT
10701
+ // manifest-tracked (verified) — uninstall removes them explicitly via
10702
+ // removeCursorHooksJson + its script list, and reconcile is idempotent.
10703
+ // The re-run is retained for parity with the settings.json install path.
10034
10704
  writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
10035
10705
  persistActiveProfileMarker();
10036
10706
  return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
10037
10707
  }
10038
10708
 
10039
10709
  if (plan.installSurface === 'profile-marker-only') {
10040
- // Windsurf/Trae/Kimi use artifact-only surfaces — no config.toml or settings.json hooks needed.
10710
+ // Windsurf/Trae use artifact-only surfaces — no config.toml or settings.json
10711
+ // hooks needed. Kimi is also artifact-only for its INSTALL surface (skills +
10712
+ // kimi-agents, no settings.json) but #2095 Upgrade 1 gives it its own
10713
+ // independent hooksSurface: kimi's native config.toml [[hooks]] array, which
10714
+ // lives outside targetDir entirely (resolveKimiHooksTomlDir resolves ~/.kimi,
10715
+ // a sibling of targetDir's ~/.config/agents) — hence writing it here, inside
10716
+ // this early-return, rather than requiring installSurface to change.
10717
+ //
10718
+ // GATED TO GLOBAL ONLY (belt-and-suspenders): kimi local installs already
10719
+ // return early at the top of install() via hostBehaviors.localInstallDeferred,
10720
+ // long before this point is ever reached — so `isGlobal` is always true here
10721
+ // in practice. The explicit check documents that invariant and fails closed
10722
+ // if that early-return is ever refactored away.
10723
+ //
10724
+ // Kimi's contract forbids hooks/ or package.json under its generic
10725
+ // Agent-Skills configDir (targetDir) — capabilities/kimi/capability.json
10726
+ // declares hostBehaviors.skipSharedHooksInstall:true, which excludes it from
10727
+ // the shared installSharedHooksBundle(targetDir) call above. Kimi still needs
10728
+ // those SAME hook scripts + the CommonJS package.json marker, but SELF-
10729
+ // CONTAINED under its own native hook root instead — so install them there,
10730
+ // and point buildHookCommand (via writeKimiHooksToml's second arg) at that
10731
+ // same root so the generated [[hooks]] command paths reference
10732
+ // ~/.kimi/hooks/<script> rather than a script that doesn't exist under
10733
+ // targetDir/hooks (which kimi no longer receives).
10734
+ if (plan.hooksSurface === 'kimi-hooks-toml' && isGlobal) {
10735
+ const kimiHooksRoot = resolveKimiHooksTomlDir();
10736
+ // Note: the `failures` array's hard-fail gate (`if (failures.length > 0)
10737
+ // process.exit(1)`) runs earlier in this function, before this
10738
+ // profile-marker-only branch is ever reached — pushing to it here would
10739
+ // be silently ineffective. Warn instead; a failed hooks copy still
10740
+ // leaves kimi's skills/agents artifacts installed correctly.
10741
+ if (!installSharedHooksBundle(kimiHooksRoot)) {
10742
+ console.warn(` ${yellow}⚠${reset} Kimi hook bundle did not verify at ${path.join(kimiHooksRoot, 'hooks')} — GSD lifecycle hooks may be incomplete`);
10743
+ }
10744
+ const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
10745
+ const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
10746
+ const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
10747
+ if (kimiHooksResult.changed) {
10748
+ console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
10749
+ }
10750
+ }
10751
+
10752
+ // ADR-1239 / #2100 Stage 2: Windsurf's own independent hooksSurface —
10753
+ // Cascade's native hooks.json blocking hook bus (pre_write_code,
10754
+ // pre_run_command), wired via runtime-hooks-surface.cts exactly like
10755
+ // Cursor's writeCursorHooksJson but with Cascade's exit-code-2 blocking
10756
+ // protocol instead of Cursor's stdout-JSON form. Unlike kimi's branch
10757
+ // above, this is NOT gated to `isGlobal` — Windsurf has no
10758
+ // hostBehaviors.localInstallDeferred early-return, so both local
10759
+ // (.windsurf/hooks.json) and global (~/.codeium/windsurf/hooks.json)
10760
+ // installs reach this branch and must get the hook bus wired.
10761
+ if (plan.hooksSurface === 'windsurf-hooks-json') {
10762
+ const windsurfHookResult = writeWindsurfHooksJson(targetDir, src, {
10763
+ platform: process.platform,
10764
+ });
10765
+ if (windsurfHookResult.changed) {
10766
+ console.log(` ${green}✓${reset} Configured Windsurf lifecycle hooks (pre_write_code, pre_run_command)`);
10767
+ } else {
10768
+ console.log(` ${green}✓${reset} Windsurf lifecycle hooks already up to date`);
10769
+ }
10770
+ // Re-run the manifest pass, mirroring the cursor writer's pattern above
10771
+ // for parity. This does NOT hash-track hooks.json or the
10772
+ // gsd-windsurf-*.js scripts (same as cursor): uninstall removes them
10773
+ // explicitly via removeWindsurfHooksJson, and reconcileWindsurfHooksJson
10774
+ // is idempotent on repeated installs, so manifest tracking isn't needed
10775
+ // for correctness here.
10776
+ writeManifest(targetDir, runtime, { mode: _effectiveInstallMode, scope: isGlobal ? 'global' : 'local' });
10777
+ }
10778
+
10041
10779
  persistActiveProfileMarker();
10042
10780
  return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
10043
10781
  }
@@ -10165,7 +10903,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10165
10903
  // Claude Code sets $CLAUDE_PROJECT_DIR; Antigravity does not — and on
10166
10904
  // Windows its own substitution logic doubles the path (#2557). It runs
10167
10905
  // project hooks with the project dir as cwd, so bare relative paths work.
10168
- const localPrefix = projectLocalHookPrefix({ runtime, dirName });
10906
+ // Descriptor-driven (ADR-1239 / #2096): hookPathStyle comes from the
10907
+ // runtime's hostBehaviors instead of a hardcoded `runtime === 'antigravity'`
10908
+ // check inside projectLocalHookPrefix.
10909
+ const localPrefix = projectLocalHookPrefix({ runtime, dirName, hookPathStyle: _hostBehaviors(runtime).hookPathStyle });
10169
10910
  const hookOpts = { portableHooks: hasPortableHooks, runtime };
10170
10911
  // #2979: local-install hook commands also use the absolute node path so
10171
10912
  // GUI/minimal-PATH runtimes can resolve them. Bare `node` fails when the
@@ -10338,7 +11079,16 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
10338
11079
  * Apply statusline config, then print completion message
10339
11080
  */
10340
11081
  function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = DEFAULT_RUNTIME, isGlobal = true, configDir = null, bannerOpts = {}) {
10341
- const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
11082
+ // #2093: isKilo dropped — the Kilo permissions-writer call below is gated
11083
+ // on plan.finishPermissionWriter === 'kilo' (descriptor-driven), not this flag.
11084
+ // #2094: isTrae dropped — unused in this function.
11085
+ // #2095: isKimi dropped — the Kimi "Done!" banner below reads
11086
+ // _hostBehaviors(runtime).doneBannerStyle === 'kimi-agent-file' (descriptor-driven), not this flag.
11087
+ // #2096: isAntigravity dropped — unused in this function.
11088
+ // #2098: isCodebuddy dropped — unused in this function.
11089
+ // #2099: isCopilot dropped — unused in this function.
11090
+ // #2100: isWindsurf dropped — unused in this function.
11091
+ const { isOpencode, isCodex, isCursor, isAugment, isQwen, isHermes, isCline } = runtimeFlags(runtime);
10342
11092
  const plan = resolveInstallPlan(runtime);
10343
11093
 
10344
11094
  if (shouldInstallStatusline && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {
@@ -10400,6 +11150,12 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
10400
11150
  mergeClaudePermissions(settings);
10401
11151
  }
10402
11152
 
11153
+ // #2097 UPGRADE 3 (transport:mcp): companion MCP server for runtimes that host
11154
+ // MCP in settings.json (Augment). settings.json is golden-excluded, so no golden change.
11155
+ if (_hostBehaviors(runtime).mcpCompanion === 'settings-json' && settings && plan.writesSharedSettings) {
11156
+ mergeGsdMcpServerIntoSettings(settings);
11157
+ }
11158
+
10403
11159
  // Write settings when runtime supports settings.json.
10404
11160
  // #3002 CR: defense-in-depth — re-run validateHookFields right before
10405
11161
  // serialization. The push-site guards above already skip null-command
@@ -10421,6 +11177,15 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
10421
11177
  configureKiloPermissions(isGlobal, configDir);
10422
11178
  }
10423
11179
 
11180
+ // Configure Antigravity permissions + MCP companion server (#2096 Phase B
11181
+ // Upgrades 1+2). Not GSD_TEST_MODE-gated — mirrors Kilo's dispatch exactly;
11182
+ // both writers target files (settings.json, mcp_config.json) scoped under
11183
+ // this runtime's own configDir, so they are safe to run unconditionally.
11184
+ if (plan.finishPermissionWriter === 'antigravity') {
11185
+ configureAntigravityPermissions(isGlobal, configDir);
11186
+ configureAntigravityMcpConfig(isGlobal, configDir);
11187
+ }
11188
+
10424
11189
  // For non-Claude runtimes, DEFAULT resolve_model_ids to "omit" in ~/.gsd/defaults.json
10425
11190
  // when it is absent or falsy, so resolveModelInternal() returns '' instead of Claude
10426
11191
  // aliases (opus/sonnet/haiku) the runtime can't resolve. An explicit `true` opt-in
@@ -10480,7 +11245,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
10480
11245
  return;
10481
11246
  }
10482
11247
 
10483
- if (runtime === 'kimi') {
11248
+ if (_hostBehaviors(runtime).doneBannerStyle === 'kimi-agent-file') {
10484
11249
  const agentPath = configDir ? path.join(configDir, 'agents', 'gsd.yaml') : 'agents/gsd.yaml';
10485
11250
  console.log(`
10486
11251
  ${green}Done!${reset} Start ${program} with ${cyan}kimi --agent-file ${agentPath}${reset}, then run ${cyan}${command}${reset}.
@@ -10568,13 +11333,14 @@ const runtimeMap = {
10568
11333
  '10': 'kimi',
10569
11334
  '11': 'kilo',
10570
11335
  '12': 'opencode',
10571
- '13': 'qwen',
10572
- '14': 'trae',
10573
- '15': 'windsurf',
10574
- '16': 'zcode'
11336
+ '13': 'pi',
11337
+ '14': 'qwen',
11338
+ '15': 'trae',
11339
+ '16': 'windsurf',
11340
+ '17': 'zcode'
10575
11341
  };
10576
- const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'qwen', 'trae', 'windsurf', 'zcode'];
10577
- const ALL_RUNTIMES_OPTION = '17';
11342
+ const allRuntimes = ['claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex', 'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'pi', 'qwen', 'trae', 'windsurf', 'zcode'];
11343
+ const ALL_RUNTIMES_OPTION = '18';
10578
11344
 
10579
11345
  /**
10580
11346
  * Build the runtime-selection prompt text shown by the interactive installer.
@@ -10594,11 +11360,12 @@ function buildRuntimePromptText() {
10594
11360
  ${cyan}10${reset}) Kimi ${dim}(~/.config/agents, then ~/.agents if existing)${reset}
10595
11361
  ${cyan}11${reset}) Kilo ${dim}(~/.config/kilo)${reset}
10596
11362
  ${cyan}12${reset}) OpenCode ${dim}(~/.config/opencode)${reset}
10597
- ${cyan}13${reset}) Qwen Code ${dim}(~/.qwen)${reset}
10598
- ${cyan}14${reset}) Trae ${dim}(~/.trae)${reset}
10599
- ${cyan}15${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset}
10600
- ${cyan}16${reset}) ZCode ${dim}(~/.zcode)${reset}
10601
- ${cyan}17${reset}) All
11363
+ ${cyan}13${reset}) pi ${dim}(~/.pi/agent)${reset}
11364
+ ${cyan}14${reset}) Qwen Code ${dim}(~/.qwen)${reset}
11365
+ ${cyan}15${reset}) Trae ${dim}(~/.trae)${reset}
11366
+ ${cyan}16${reset}) Windsurf ${dim}(~/.codeium/windsurf)${reset}
11367
+ ${cyan}17${reset}) ZCode ${dim}(~/.zcode)${reset}
11368
+ ${cyan}18${reset}) All
10602
11369
 
10603
11370
  ${dim}Select multiple: 1,2,6 or 1 2 6${reset}
10604
11371
  `;
@@ -11145,8 +11912,8 @@ const _LEGACY_SCAN_SUBDIR_NAMES = [
11145
11912
  '.agents', // antigravity local form (canonical, #791)
11146
11913
  '.agent', // antigravity local form (legacy, backward-compat)
11147
11914
  '.cursor',
11148
- '.devin', // windsurf local form (canonical, #1085; Devin Desktop preferred dir)
11149
- '.windsurf', // windsurf local form (legacy, backward-compat with pre-#1085 installs)
11915
+ '.devin', // windsurf local form (legacy, pre-#1615; Devin Desktop preferred dir, #1085)
11916
+ '.windsurf', // windsurf local form (canonical since #1615; capability.json localConfigDir)
11150
11917
  '.codeium/windsurf',
11151
11918
  '.augment',
11152
11919
  '.trae',
@@ -11398,6 +12165,13 @@ module.exports = {
11398
12165
  getConfigDirFromHome,
11399
12166
  resolveKiloConfigPath,
11400
12167
  configureKiloPermissions,
12168
+ // #2096 Phase B Upgrades 1+2 — Antigravity permission-writer + MCP companion
12169
+ toTildePosixPath,
12170
+ buildAntigravityAllowRules,
12171
+ configureAntigravityPermissions,
12172
+ configureAntigravityMcpConfig,
12173
+ // #2097 UPGRADE 3 — Augment MCP companion (settings.json-hosted)
12174
+ mergeGsdMcpServerIntoSettings,
11401
12175
  claudeToCopilotTools,
11402
12176
  convertCopilotToolName,
11403
12177
  convertClaudeToCopilotContent,
@@ -11450,6 +12224,11 @@ module.exports = {
11450
12224
  reconcileCursorHooksJson,
11451
12225
  writeCursorHooksJson,
11452
12226
  removeCursorHooksJson,
12227
+ GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT,
12228
+ GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT,
12229
+ GSD_WINDSURF_HOOK_SCRIPTS,
12230
+ writeWindsurfHooksJson,
12231
+ removeWindsurfHooksJson,
11453
12232
  stripGsdFromAgentsMd,
11454
12233
  GSD_AGENTS_MD_MARKER,
11455
12234
  GSD_AGENTS_MD_CLOSE_MARKER,