diff --git a/get-shit-done/bin/lib/profile-output.cjs b/get-shit-done/bin/lib/profile-output.cjs index d6918c5d0..52ca088fe 100644 --- a/get-shit-done/bin/lib/profile-output.cjs +++ b/get-shit-done/bin/lib/profile-output.cjs @@ -16,6 +16,7 @@ const { output, error, loadConfig } = require('./core.cjs'); const { platformReadSync: safeReadFile, platformWriteSync, platformEnsureDir } = require('./shell-command-projection.cjs'); const { getGlobalSkillDir } = require('./runtime-homes.cjs'); const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs'); +const { resolveRuntimeNameFromCandidates } = require('./runtime-name-policy.cjs'); // ─── Constants ──────────────────────────────────────────────────────────────── @@ -811,9 +812,16 @@ function cmdGenerateDevPreferences(cwd, options, raw) { let effectiveRuntime = 'claude'; try { const config = loadConfig(cwd); - effectiveRuntime = process.env.GSD_RUNTIME || config.runtime || 'claude'; + effectiveRuntime = resolveRuntimeNameFromCandidates( + process.env.GSD_RUNTIME, + config.runtime, + 'claude' + ) || 'claude'; } catch { - effectiveRuntime = process.env.GSD_RUNTIME || 'claude'; + effectiveRuntime = resolveRuntimeNameFromCandidates( + process.env.GSD_RUNTIME, + 'claude' + ) || 'claude'; } const skillDir = getGlobalSkillDir(effectiveRuntime, 'gsd-dev-preferences'); if (!skillDir) { @@ -1003,7 +1011,10 @@ function cmdGenerateClaudeMd(cwd, options, raw) { // #3163: When runtime is codex, override the output target to AGENTS.md // regardless of claude_md_path, so Codex projects never write to CLAUDE.md. // GSD_RUNTIME env var takes precedence over config.runtime, mirroring detectRuntime(). - const effectiveRuntime = process.env.GSD_RUNTIME || config.runtime || null; + const effectiveRuntime = resolveRuntimeNameFromCandidates( + process.env.GSD_RUNTIME, + config.runtime + ); if (!options.output && effectiveRuntime === 'codex') { configClaudeMdPath = './AGENTS.md'; } diff --git a/get-shit-done/bin/lib/runtime-name-policy.cjs b/get-shit-done/bin/lib/runtime-name-policy.cjs index a7635219b..0241f3fe8 100644 --- a/get-shit-done/bin/lib/runtime-name-policy.cjs +++ b/get-shit-done/bin/lib/runtime-name-policy.cjs @@ -57,6 +57,27 @@ function canonicalizeRuntimeName(value) { return aliasToCanonical.get(normalizeRuntimeToken(value)) || null; } +/** + * Resolve runtime from a precedence list of candidate values. + * + * - First non-empty string candidate wins. + * - Known aliases are canonicalized (codex-cli -> codex). + * - Unknown values are normalized and returned (future-runtime tolerance). + * + * @param {...string} candidates + * @returns {string|null} + */ +function resolveRuntimeNameFromCandidates(...candidates) { + for (const candidate of candidates) { + if (typeof candidate !== 'string') continue; + const normalized = normalizeRuntimeToken(candidate); + if (!normalized) continue; + return canonicalizeRuntimeName(normalized) || normalized; + } + return null; +} + module.exports = { canonicalizeRuntimeName, + resolveRuntimeNameFromCandidates, }; diff --git a/get-shit-done/bin/lib/runtime-slash.cjs b/get-shit-done/bin/lib/runtime-slash.cjs index 923972730..4da389cd1 100644 --- a/get-shit-done/bin/lib/runtime-slash.cjs +++ b/get-shit-done/bin/lib/runtime-slash.cjs @@ -1,6 +1,6 @@ 'use strict'; -const { canonicalizeRuntimeName } = require('./runtime-name-policy.cjs'); +const { canonicalizeRuntimeName, resolveRuntimeNameFromCandidates } = require('./runtime-name-policy.cjs'); /** * runtime-slash.cjs — single source of truth for emitting GSD slash-command @@ -69,10 +69,8 @@ function formatGsdSlash(commandName, runtime) { * @returns {string} */ function resolveRuntime(projectDir) { - if (process.env.GSD_RUNTIME) { - const rawRuntime = String(process.env.GSD_RUNTIME).toLowerCase(); - return canonicalizeRuntimeName(rawRuntime) || rawRuntime; - } + const envRuntime = resolveRuntimeNameFromCandidates(process.env.GSD_RUNTIME); + if (envRuntime) return envRuntime; if (projectDir) { try { // Read config.json directly (not via loadConfig). loadConfig has a side @@ -87,8 +85,8 @@ function resolveRuntime(projectDir) { const raw = fs.readFileSync(configPath, 'utf-8'); const parsed = JSON.parse(raw); if (parsed && typeof parsed === 'object' && parsed.runtime) { - const rawRuntime = String(parsed.runtime).toLowerCase(); - return canonicalizeRuntimeName(rawRuntime) || rawRuntime; + const configRuntime = resolveRuntimeNameFromCandidates(parsed.runtime); + if (configRuntime) return configRuntime; } } } catch { diff --git a/tests/profile-output.test.cjs b/tests/profile-output.test.cjs index 38238d11d..bbb26da2d 100644 --- a/tests/profile-output.test.cjs +++ b/tests/profile-output.test.cjs @@ -174,6 +174,16 @@ describe('generate-claude-md command', () => { assert.ok(content.includes('.codex/skills/')); assert.ok(!content.includes('get-shit-done/skills')); }); + + test('codex runtime aliases default output to AGENTS.md', () => { + const result = runGsdTools( + ['generate-claude-md', '--auto', '--raw'], + tmpDir, + { GSD_RUNTIME: 'codex-cli' } + ); + assert.ok(result.success, `Failed: ${result.error}`); + assert.ok(fs.existsSync(path.join(tmpDir, 'AGENTS.md')), 'AGENTS.md should be generated for codex aliases'); + }); }); // ─── generate-dev-preferences ───────────────────────────────────────────────── @@ -238,6 +248,27 @@ describe('generate-dev-preferences command', () => { assert.ok(fs.existsSync(out.command_path), 'runtime-aware output should be written'); }); + test('canonicalizes codex runtime aliases for skills output path', () => { + const analysis = { + profile_version: '1.0', + dimensions: { + communication_style: { rating: 'terse-direct', confidence: 'HIGH' }, + }, + }; + const analysisPath = path.join(tmpDir, 'analysis.json'); + const codexHome = path.join(tmpDir, 'codex-home'); + fs.writeFileSync(analysisPath, JSON.stringify(analysis)); + + const result = runGsdTools( + ['generate-dev-preferences', '--analysis', analysisPath, '--raw'], + tmpDir, + { CODEX_HOME: codexHome, GSD_RUNTIME: 'codex-app' } + ); + assert.ok(result.success, `Failed: ${result.error}`); + const out = JSON.parse(result.output); + assert.strictEqual(out.command_path, path.join(codexHome, 'skills', 'gsd-dev-preferences', 'SKILL.md')); + }); + test('errors for cline unless --output is supplied', () => { const analysis = { profile_version: '1.0',