From c480e04188ce4be94b1becd957960be5a047e4f5 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 7 Jun 2026 14:59:33 -0400 Subject: [PATCH] fix(#783): resolve Kilo global skills base to ~/.kilo/skills (#806) * fix(#783): resolve Kilo global skills base to ~/.kilo/skills getGlobalSkillsBase('kilo') returned ~/.config/kilo/skills (the XDG config dir), but Kilo Code discovers global skills from ~/.kilo/skills/ (the .kilo dir in HOME), independent of the kilo.jsonc config dir. Add a HOME-relative special case so the resolver matches Kilo's actual discovery path. The config dir (~/.config/kilo) and the installer's command/ path are correct and unchanged. This corrects the path used by doctor/status and agent-skills-block resolution; the installer writes commands (not skills) for Kilo, so no files were being written to the wrong location. Closes #783 Co-Authored-By: Claude Opus 4.8 * chore(#783): set changeset pr to 806 Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .changeset/783-kilo-global-skills-base.md | 7 ++ src/runtime-homes.cts | 11 +- .../bug-783-kilo-global-skills-base.test.cjs | 105 ++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 .changeset/783-kilo-global-skills-base.md create mode 100644 tests/bug-783-kilo-global-skills-base.test.cjs diff --git a/.changeset/783-kilo-global-skills-base.md b/.changeset/783-kilo-global-skills-base.md new file mode 100644 index 000000000..5b439e4f0 --- /dev/null +++ b/.changeset/783-kilo-global-skills-base.md @@ -0,0 +1,7 @@ +--- +type: Fixed +pr: 806 +--- +**`getGlobalSkillsBase('kilo')` now resolves to `~/.kilo/skills`** — where Kilo Code actually discovers global skills — instead of `~/.config/kilo/skills`. Per [Kilo Code docs](https://kilo.ai/docs/customize/skills), global skills live in the `.kilo` directory within HOME (`~/.kilo/skills/`), independent of the XDG-based config dir at `~/.config/kilo`. The kilo.jsonc config dir (`~/.config/kilo`) and the `command/` path used by the installer are correct and unchanged. Blast radius: this corrects the resolved skills-base path used by doctor/status checks and agent-skills-block resolution (`init.cjs`); the installer writes commands (not skills) for Kilo, so no files were previously being written to the wrong location. + + diff --git a/src/runtime-homes.cts b/src/runtime-homes.cts index a27c96887..63212fbc4 100644 --- a/src/runtime-homes.cts +++ b/src/runtime-homes.cts @@ -162,8 +162,17 @@ export function getGlobalConfigDir(runtime: string, explicitDir?: string | null) */ export function getGlobalSkillsBase(runtime: string): string | null { if (runtime === 'cline') return null; + if (runtime === 'hermes') { + const configDir = getGlobalConfigDir(runtime); + return path.join(configDir, 'skills', 'gsd'); + } + // Kilo Code discovers global skills from ~/.kilo/skills/ (HOME-relative), + // independent of the XDG-based config dir (~/.config/kilo) used for commands. + // See: https://kilo.ai/docs/customize/skills + // "Global skills are located in the `.kilo` directory within your Home + // directory: ~/.kilo/skills/" + if (runtime === 'kilo') return path.join(os.homedir(), '.kilo', 'skills'); const configDir = getGlobalConfigDir(runtime); - if (runtime === 'hermes') return path.join(configDir, 'skills', 'gsd'); return path.join(configDir, 'skills'); } diff --git a/tests/bug-783-kilo-global-skills-base.test.cjs b/tests/bug-783-kilo-global-skills-base.test.cjs new file mode 100644 index 000000000..90a29c832 --- /dev/null +++ b/tests/bug-783-kilo-global-skills-base.test.cjs @@ -0,0 +1,105 @@ +'use strict'; +// Regression guard for bug #783. +// +// getGlobalSkillsBase('kilo') was returning ~/.config/kilo/skills (the XDG +// config dir) instead of ~/.kilo/skills — where Kilo Code actually discovers +// global skills per its docs: +// https://kilo.ai/docs/customize/skills +// "Global skills are located in the `.kilo` directory within your Home +// directory: ~/.kilo/skills/" +// +// The fix adds a special case in getGlobalSkillsBase() that resolves kilo's +// skills dir from HOME (not from the XDG config dir). The config dir at +// ~/.config/kilo is still CORRECT for commands (command/) and must stay +// unchanged — this test verifies both roles are separate. + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); +const os = require('node:os'); + +const ROOT = path.join(__dirname, '..'); +const { + getGlobalConfigDir, + getGlobalSkillsBase, +} = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-homes.cjs')); + +// Helper: temporarily override env vars for a test, restoring them afterwards. +function withEnv(overrides, fn) { + const saved = {}; + for (const [key, value] of Object.entries(overrides)) { + saved[key] = process.env[key]; + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + try { + return fn(); + } finally { + for (const [key] of Object.entries(overrides)) { + if (saved[key] === undefined) delete process.env[key]; + else process.env[key] = saved[key]; + } + } +} + +// Clear all kilo-relevant env vars so tests are hermetic. +const kiloEnvClears = { + KILO_CONFIG_DIR: undefined, + XDG_CONFIG_HOME: undefined, +}; + +describe('bug #783: kilo global skills dir is ~/.kilo/skills, not ~/.config/kilo/skills', () => { + test('getGlobalSkillsBase("kilo") resolves to ~/.kilo/skills', () => { + withEnv(kiloEnvClears, () => { + assert.strictEqual( + getGlobalSkillsBase('kilo'), + path.join(os.homedir(), '.kilo', 'skills'), + ); + }); + }); + + test('getGlobalConfigDir("kilo") still resolves to ~/.config/kilo (config dir unchanged)', () => { + withEnv(kiloEnvClears, () => { + assert.strictEqual( + getGlobalConfigDir('kilo'), + path.join(os.homedir(), '.config', 'kilo'), + ); + }); + }); + + test('kilo skills dir and config dir are decoupled (not equal, not nested)', () => { + withEnv(kiloEnvClears, () => { + const skillsBase = getGlobalSkillsBase('kilo'); + const configDir = getGlobalConfigDir('kilo'); + + assert.notStrictEqual(skillsBase, configDir, 'skills dir must differ from config dir'); + assert.ok( + !skillsBase.startsWith(configDir + path.sep), + `skills dir (${skillsBase}) must not be nested under config dir (${configDir})`, + ); + assert.ok( + !configDir.startsWith(skillsBase + path.sep), + `config dir (${configDir}) must not be nested under skills dir (${skillsBase})`, + ); + }); + }); + + test('getGlobalSkillsBase("kilo") is NOT affected by KILO_CONFIG_DIR override', () => { + // Skills always live in ~/.kilo/skills regardless of XDG/config-dir overrides. + withEnv({ KILO_CONFIG_DIR: '/tmp/custom-kilo-config', XDG_CONFIG_HOME: undefined }, () => { + assert.strictEqual( + getGlobalSkillsBase('kilo'), + path.join(os.homedir(), '.kilo', 'skills'), + ); + }); + }); + + test('getGlobalSkillsBase("kilo") is NOT affected by XDG_CONFIG_HOME override', () => { + withEnv({ KILO_CONFIG_DIR: undefined, XDG_CONFIG_HOME: '/tmp/custom-xdg' }, () => { + assert.strictEqual( + getGlobalSkillsBase('kilo'), + path.join(os.homedir(), '.kilo', 'skills'), + ); + }); + }); +});