feat(#295): deepen runtime name policy seam (#296)

This commit is contained in:
Tom Boucher
2026-05-26 11:24:01 -04:00
committed by GitHub
parent 43eafea3d5
commit a361da689b
4 changed files with 71 additions and 10 deletions

View File

@@ -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';
}

View File

@@ -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,
};

View File

@@ -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 {

View File

@@ -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',