feat(#1138): make runtime descriptors authoritative (#1157)

This commit is contained in:
Tom Boucher
2026-06-13 01:49:25 -04:00
committed by GitHub
parent bd349aa88e
commit aec3374bc2
40 changed files with 696 additions and 154 deletions

View File

@@ -112,7 +112,9 @@ function parseCallsAgents(content: string): string[] {
* `gsd-*` agent name references. Agent stems are stored under the special
* key `_calls_agents_<stem>` so they don't conflict with skill stems.
*/
function loadSkillsManifest(commandsDir: string): Map<string, string[]> {
const DEFAULT_COMMANDS_DIR = path.resolve(__dirname, '..', '..', '..', 'commands', 'gsd');
function loadSkillsManifest(commandsDir: string = DEFAULT_COMMANDS_DIR): Map<string, string[]> {
const manifest = new Map<string, string[]>();
if (!fs.existsSync(commandsDir)) return manifest;
const entries = fs.readdirSync(commandsDir, { withFileTypes: true });

View File

@@ -4,9 +4,9 @@
* Runtime artifact layout module — resolves the artifact directory shapes
* (commands, agents, skills) for each supported runtime.
*
* grok is intentionally absent: it is in runtime-homes.cjs but not wired
* here. The TypeError on unknown runtime is the loud-fail signal that a
* runtime was added to the homes list without a layout entry.
* grok is intentionally absent: it is in runtime-homes.cjs but has no runtime
* capability descriptor. The TypeError on unknown runtime is the loud-fail
* signal that a runtime was added without an artifact layout descriptor.
*
* ADR-457 build-at-publish: the hand-written bin/lib/runtime-artifact-layout.cjs
* collapsed to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
@@ -166,16 +166,6 @@ function findAgentsSourceRoot(runtimeConfigDir?: string): string {
throw new Error(`findAgentsSourceRoot: could not locate agents/ from ${__dirname}`);
}
// ---------------------------------------------------------------------------
// Allowlisted runtimes
// ---------------------------------------------------------------------------
const ALLOWED_RUNTIMES = new Set([
'claude', 'cursor', 'gemini', 'codex', 'copilot', 'antigravity',
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy',
'cline', 'kimi', 'opencode', 'kilo',
]);
// ---------------------------------------------------------------------------
// Layout table builders
// ---------------------------------------------------------------------------
@@ -382,7 +372,11 @@ interface ArtifactLayoutDescriptor {
}
/** Lazy registry accessor — mirrors pattern from 5b/5c (runtime-homes.cts). */
function getRegistry(): { runtimes: Record<string, { runtime?: { artifactLayout?: ArtifactLayoutDescriptor } }> } {
interface RegistryLike {
runtimes: Record<string, { runtime?: { artifactLayout?: ArtifactLayoutDescriptor } }>;
}
function getRegistry(): RegistryLike {
return _require('./capability-registry.cjs') as {
runtimes: Record<string, { runtime?: { artifactLayout?: ArtifactLayoutDescriptor } }>;
};
@@ -431,19 +425,24 @@ function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, confi
* instead of a hardcoded switch statement.
*/
function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope: 'local' | 'global' = 'global'): Layout {
return resolveRuntimeArtifactLayoutFromRegistry(getRegistry(), runtime, configDir, scope);
}
function resolveRuntimeArtifactLayoutFromRegistry(
registry: RegistryLike,
runtime: string,
configDir: string,
scope: 'local' | 'global' = 'global',
): Layout {
if (typeof configDir !== 'string' || configDir === '') {
throw new TypeError('configDir must be a non-empty string');
}
if (scope !== 'local' && scope !== 'global') {
throw new TypeError('scope must be "local" or "global"');
}
if (!ALLOWED_RUNTIMES.has(runtime)) {
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
}
const desc = getRegistry().runtimes[runtime]?.runtime?.artifactLayout;
const desc = registry.runtimes[runtime]?.runtime?.artifactLayout;
if (!desc) {
// Runtime is in ALLOWED_RUNTIMES but has no descriptor — reproduce old default: throw.
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
}
@@ -453,4 +452,4 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
return { runtime, configDir, scope, kinds };
}
export = { resolveRuntimeArtifactLayout, findInstallSourceRoot, getInstallExports };
export = { resolveRuntimeArtifactLayout, resolveRuntimeArtifactLayoutFromRegistry, findInstallSourceRoot, getInstallExports };

View File

@@ -82,6 +82,8 @@ interface InstallPlan extends RuntimeConfigIntent {
// Exports
// ---------------------------------------------------------------------------
type RuntimeDescriptorMap = Record<string, { runtime: Record<string, unknown> | undefined }>;
/** The complete set of 16 supported runtimes for config-adapter dispatch. */
const ALLOWED_CONFIG_RUNTIMES: ReadonlySet<string> = new Set(
Object.entries(runtimes)
@@ -119,6 +121,29 @@ function resolveRuntimeConfigIntent(runtime: string): RuntimeConfigIntent {
};
}
function resolveInstallPlanFromRuntimes(runtimeDescriptors: RuntimeDescriptorMap, runtime: string): InstallPlan {
const desc = runtimeDescriptors[runtime]?.runtime;
if (!desc) throw new TypeError(`Unknown runtime for install plan: ${runtime}`);
if (desc['hooksSurface'] == null) {
throw new TypeError(`runtime.hooksSurface is required for install plan: ${runtime}`);
}
const sandboxTier = desc['sandboxTier'];
if (typeof sandboxTier !== 'string' || !VALID_SANDBOX_TIERS.has(sandboxTier)) {
throw new TypeError(`Runtime '${runtime}' has a missing or invalid sandboxTier descriptor axis: ${JSON.stringify(sandboxTier)}`);
}
const permissionWriter = desc['permissionWriter'];
return {
runtime,
installSurface: desc['installSurface'] as ConfigInstallSurface,
writesSharedSettings: desc['writesSharedSettings'] as boolean,
finishPermissionWriter: permissionWriter == null ? null : permissionWriter as FinishPermissionWriter,
hookEvents: desc['hookEvents'] as string | undefined,
extendedHookEvents: Array.isArray(desc['extendedHookEvents']) ? [...desc['extendedHookEvents'] as string[]] : [],
hooksSurface: desc['hooksSurface'] as HooksSurface,
sandboxTier,
};
}
/**
* Resolve the complete install plan for a given runtime.
*
@@ -132,25 +157,7 @@ function resolveRuntimeConfigIntent(runtime: string): RuntimeConfigIntent {
* @throws {TypeError} if runtime is not a known supported runtime.
*/
function resolveInstallPlan(runtime: string): InstallPlan {
const desc = runtimes[runtime]?.runtime;
if (!desc) throw new TypeError(`Unknown runtime for install plan: ${runtime}`);
const configIntent = resolveRuntimeConfigIntent(runtime);
const sandboxTier = desc['sandboxTier'];
if (typeof sandboxTier !== 'string' || !VALID_SANDBOX_TIERS.has(sandboxTier)) {
throw new TypeError(`Runtime '${runtime}' has a missing or invalid sandboxTier descriptor axis: ${JSON.stringify(sandboxTier)}`);
}
return {
runtime,
installSurface: configIntent.installSurface,
writesSharedSettings: configIntent.writesSharedSettings,
finishPermissionWriter: configIntent.finishPermissionWriter,
hookEvents: desc['hookEvents'] as string | undefined,
extendedHookEvents: Array.isArray(desc['extendedHookEvents']) ? desc['extendedHookEvents'] as string[] : [],
hooksSurface: desc['hooksSurface'] != null
? desc['hooksSurface'] as HooksSurface
: ((runtime === 'opencode' || runtime === 'kilo') ? 'none' : 'settings-json'),
sandboxTier,
};
return resolveInstallPlanFromRuntimes(runtimes, runtime);
}
export = { resolveRuntimeConfigIntent, resolveInstallPlan, ALLOWED_CONFIG_RUNTIMES, INSTALL_SURFACES };
export = { resolveRuntimeConfigIntent, resolveInstallPlan, resolveInstallPlanFromRuntimes, ALLOWED_CONFIG_RUNTIMES, INSTALL_SURFACES };

View File

@@ -67,6 +67,7 @@ interface DotHomeDescriptor {
kind: 'dot-home';
name: string;
env: string[];
skillsHome?: ConfigHomeDescriptor;
}
interface DotHomeNestedDescriptor {
@@ -75,13 +76,14 @@ interface DotHomeNestedDescriptor {
parent: string;
env: string[];
probe?: string[];
skillsHome?: ConfigHomeDescriptor;
}
interface XdgDescriptor {
kind: 'xdg';
name: string;
env: string[];
skillsHome?: unknown;
skillsHome?: ConfigHomeDescriptor;
}
interface GenericAgentsRootDescriptor {
@@ -90,6 +92,7 @@ interface GenericAgentsRootDescriptor {
env: string[];
probe: string[];
probeExists: string;
skillsHome?: ConfigHomeDescriptor;
}
type ConfigHomeDescriptor =
@@ -98,6 +101,33 @@ type ConfigHomeDescriptor =
| XdgDescriptor
| GenericAgentsRootDescriptor;
interface RuntimeArtifactKindDescriptor {
kind: string;
destSubpath: string;
}
interface RuntimeDescriptor {
configHome: ConfigHomeDescriptor;
artifactLayout?: {
global?: RuntimeArtifactKindDescriptor[];
};
}
function resolveDescriptorWithOptions(configHome: ConfigHomeDescriptor): string {
return resolveConfigHomeFromDescriptor(configHome, {
env: process.env,
home: os.homedir(),
existsSync: fs.existsSync,
});
}
function getRegistry(): { runtimes: Record<string, { runtime?: RuntimeDescriptor }> } {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('./capability-registry.cjs') as {
runtimes: Record<string, { runtime?: RuntimeDescriptor }>;
};
}
/**
* Resolve a configHome descriptor to an absolute directory path.
*
@@ -267,18 +297,11 @@ export function getGlobalConfigDir(runtime: string, explicitDir?: string | null)
}
// ── Descriptor-driven: look up in capability-registry ────────────────────
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { runtimes } = require('./capability-registry.cjs') as {
runtimes: Record<string, { runtime?: { configHome: ConfigHomeDescriptor } }>;
};
const { runtimes } = getRegistry();
const runtimeEntry = runtimes[runtime];
if (runtimeEntry?.runtime?.configHome) {
return resolveConfigHomeFromDescriptor(runtimeEntry.runtime.configHome, {
env: process.env,
home: os.homedir(),
existsSync: fs.existsSync,
});
return resolveDescriptorWithOptions(runtimeEntry.runtime.configHome);
}
// ── Default (unknown runtime → Claude fallback) ───────────────────────────
@@ -288,21 +311,30 @@ export function getGlobalConfigDir(runtime: string, explicitDir?: string | null)
/**
* Return the global skills base directory for the given runtime.
* Most runtimes: <configDir>/skills
* Hermes: <configDir>/skills/gsd (nested category layout — #2841)
* Cline ≥ v3.48.0: <configDir>/skills (SKILL.md-based global skills — #782)
* Descriptor-backed runtimes derive the base home from configHome.skillsHome
* when present, then append the first global skills artifact destSubpath.
*/
export function resolveSkillsBaseFromDescriptor(
configHome: ConfigHomeDescriptor,
opts: ResolveConfigHomeOpts = {},
skillsDestSubpath = 'skills',
): string {
const baseDescriptor = configHome.skillsHome ?? configHome;
const base = resolveConfigHomeFromDescriptor(baseDescriptor, opts);
return path.join(base, skillsDestSubpath);
}
export function getGlobalSkillsBase(runtime: string): string | null {
if (runtime === 'hermes') {
const configDir = getGlobalConfigDir(runtime);
return path.join(configDir, 'skills', 'gsd');
const runtimeEntry = getRegistry().runtimes[runtime];
const descriptor = runtimeEntry?.runtime;
const globalSkillsKind = descriptor?.artifactLayout?.global?.find((entry) => entry.kind === 'skills');
if (descriptor?.configHome && globalSkillsKind?.destSubpath) {
return resolveSkillsBaseFromDescriptor(
descriptor.configHome,
{ env: process.env, home: os.homedir(), existsSync: fs.existsSync },
globalSkillsKind.destSubpath,
);
}
// 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);
return path.join(configDir, 'skills');
}