* fix(#142): canonicalize runtime aliases across cjs and sdk * fix(#142): satisfy hand-sync and inventory parity gates * test(#1974): remove record-session lock contention in context monitor spec * test(config): retry transient config-ensure-section failures * docs(context): capture PR #143 CI reliability findings * fix(#142): bump CLI Modules inventory headline to 76 (runtime-name-policy + runtime-slash) docs/INVENTORY.md had the two new .cjs rows listed but the headline count stayed at 75; fs count is 76. inventory-counts.test.cjs caught the drift. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -403,10 +403,59 @@ describe('detectRuntime', () => {
|
||||
expect(detectRuntime()).toBe('codex');
|
||||
});
|
||||
|
||||
it('normalizes Codex env aliases to canonical codex runtime', () => {
|
||||
process.env.GSD_RUNTIME = 'codex-app';
|
||||
expect(detectRuntime()).toBe('codex');
|
||||
process.env.GSD_RUNTIME = 'codex_cli';
|
||||
expect(detectRuntime()).toBe('codex');
|
||||
});
|
||||
|
||||
it('normalizes common runtime env aliases to canonical runtime IDs', () => {
|
||||
const cases: Array<[string, Runtime]> = [
|
||||
['claude-code', 'claude'],
|
||||
['gemini-cli', 'gemini'],
|
||||
['opencode-cli', 'opencode'],
|
||||
['qwen-code', 'qwen'],
|
||||
['hermes-agent', 'hermes'],
|
||||
['github-copilot', 'copilot'],
|
||||
['cursor-nightly', 'cursor'],
|
||||
['windsurf-next', 'windsurf'],
|
||||
['augment-code', 'augment'],
|
||||
['codebuddy-cli', 'codebuddy'],
|
||||
['cline-cli', 'cline'],
|
||||
];
|
||||
|
||||
for (const [alias, runtime] of cases) {
|
||||
process.env.GSD_RUNTIME = alias;
|
||||
expect(detectRuntime()).toBe(runtime);
|
||||
}
|
||||
});
|
||||
|
||||
it('falls back to config.runtime when GSD_RUNTIME unset', () => {
|
||||
expect(detectRuntime({ runtime: 'gemini' })).toBe('gemini');
|
||||
});
|
||||
|
||||
it('normalizes Codex config aliases to canonical codex runtime', () => {
|
||||
expect(detectRuntime({ runtime: 'codex-cli' })).toBe('codex');
|
||||
expect(detectRuntime({ runtime: 'codex app' })).toBe('codex');
|
||||
});
|
||||
|
||||
it('normalizes common runtime config aliases to canonical runtime IDs', () => {
|
||||
const cases: Array<[string, Runtime]> = [
|
||||
['claude-cli', 'claude'],
|
||||
['gemini-code', 'gemini'],
|
||||
['open code', 'opencode'],
|
||||
['hermes_cli', 'hermes'],
|
||||
['antigravity-agent', 'antigravity'],
|
||||
['trae-cli', 'trae'],
|
||||
['kilo-cli', 'kilo'],
|
||||
];
|
||||
|
||||
for (const [alias, runtime] of cases) {
|
||||
expect(detectRuntime({ runtime: alias })).toBe(runtime);
|
||||
}
|
||||
});
|
||||
|
||||
it('GSD_RUNTIME wins over config.runtime', () => {
|
||||
process.env.GSD_RUNTIME = 'codex';
|
||||
expect(detectRuntime({ runtime: 'gemini' })).toBe('codex');
|
||||
|
||||
@@ -24,6 +24,7 @@ import { homedir } from 'node:os';
|
||||
import { GSDError, ErrorClassification } from '../errors.js';
|
||||
export { SUPPORTED_RUNTIMES, type Runtime } from '../model-catalog.js';
|
||||
import { SUPPORTED_RUNTIMES, type Runtime } from '../model-catalog.js';
|
||||
import { canonicalizeRuntimeName } from '../runtime-name-policy.js';
|
||||
import { workspacePlanningPaths, resolveWorkspaceContext, type PlanningPaths } from './workspace.js';
|
||||
export { stateExtractField } from './state-document.js';
|
||||
import { relPlanningPath, validateWorkstreamName } from '../workstream-utils.js';
|
||||
@@ -93,14 +94,12 @@ export function getRuntimeConfigDir(runtime: Runtime): string {
|
||||
* stale env values don't hard-block workflows.
|
||||
*/
|
||||
export function detectRuntime(config?: { runtime?: unknown }): Runtime {
|
||||
const envValue = process.env.GSD_RUNTIME;
|
||||
if (envValue && (SUPPORTED_RUNTIMES as readonly string[]).includes(envValue)) {
|
||||
return envValue as Runtime;
|
||||
}
|
||||
const configValue = config?.runtime;
|
||||
if (typeof configValue === 'string' && (SUPPORTED_RUNTIMES as readonly string[]).includes(configValue)) {
|
||||
return configValue as Runtime;
|
||||
}
|
||||
const envRuntime = canonicalizeRuntimeName(process.env.GSD_RUNTIME);
|
||||
if (envRuntime) return envRuntime;
|
||||
|
||||
const configRuntime = canonicalizeRuntimeName(config?.runtime);
|
||||
if (configRuntime) return configRuntime;
|
||||
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ describe('assertRuntimeSupportsAutoMode', () => {
|
||||
expect(() => assertRuntimeSupportsAutoMode({ runtime: 'claude' })).toThrow(/codex/);
|
||||
});
|
||||
|
||||
it('throws for GSD_RUNTIME codex alias values', () => {
|
||||
process.env.GSD_RUNTIME = 'codex-app';
|
||||
expect(() => assertRuntimeSupportsAutoMode({ runtime: 'claude' })).toThrow(/codex/);
|
||||
process.env.GSD_RUNTIME = 'codex_cli';
|
||||
expect(() => assertRuntimeSupportsAutoMode({ runtime: 'claude' })).toThrow(/codex/);
|
||||
});
|
||||
|
||||
it('error message references issue #2832 and slash-command workaround', () => {
|
||||
let caught: Error | undefined;
|
||||
try {
|
||||
@@ -81,4 +88,16 @@ describe('assertRuntimeSupportsAutoMode', () => {
|
||||
expect(caught!.message).toMatch(/config\.runtime="codex"/);
|
||||
expect(caught!.message).not.toMatch(/GSD_RUNTIME=unsupported-env/);
|
||||
});
|
||||
|
||||
it('attributes source to GSD_RUNTIME when env uses a supported codex alias', () => {
|
||||
process.env.GSD_RUNTIME = 'codex-cli';
|
||||
let caught: Error | undefined;
|
||||
try {
|
||||
assertRuntimeSupportsAutoMode({ runtime: 'claude' });
|
||||
} catch (err) {
|
||||
caught = err as Error;
|
||||
}
|
||||
expect(caught).toBeDefined();
|
||||
expect(caught!.message).toMatch(/GSD_RUNTIME=codex-cli/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* for non-Claude runtimes.
|
||||
*/
|
||||
import { detectRuntime, SUPPORTED_RUNTIMES, type Runtime } from './query/helpers.js';
|
||||
import { canonicalizeRuntimeName } from './runtime-name-policy.js';
|
||||
|
||||
/**
|
||||
* Throw a clear error when the active runtime is not Claude.
|
||||
@@ -33,8 +34,9 @@ export function assertRuntimeSupportsAutoMode(config?: Record<string, unknown> |
|
||||
// a `GSD_RUNTIME` value that isn't in SUPPORTED_RUNTIMES falls through to
|
||||
// the config tier, so reporting it as the source would be misleading.
|
||||
const env = process.env.GSD_RUNTIME;
|
||||
const envCanonical = canonicalizeRuntimeName(env);
|
||||
const envIsSupported =
|
||||
typeof env === 'string' && (SUPPORTED_RUNTIMES as readonly string[]).includes(env);
|
||||
typeof envCanonical === 'string' && (SUPPORTED_RUNTIMES as readonly string[]).includes(envCanonical);
|
||||
const source = envIsSupported
|
||||
? `GSD_RUNTIME=${env}`
|
||||
: `config.runtime="${String(cfg.runtime ?? '')}"`;
|
||||
|
||||
32
sdk/src/runtime-name-policy.ts
Normal file
32
sdk/src/runtime-name-policy.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { SUPPORTED_RUNTIMES, type Runtime } from './model-catalog.js';
|
||||
|
||||
interface RuntimeAliasManifest {
|
||||
[canonicalRuntime: string]: string[];
|
||||
}
|
||||
|
||||
const MANIFEST_PATH = new URL('../shared/runtime-aliases.manifest.json', import.meta.url);
|
||||
const manifest: RuntimeAliasManifest = JSON.parse(readFileSync(fileURLToPath(MANIFEST_PATH), 'utf-8'));
|
||||
|
||||
function normalizeRuntimeToken(value: string): string {
|
||||
return value.trim().toLowerCase().replace(/[_\s]+/g, '-');
|
||||
}
|
||||
|
||||
const aliasToCanonical = new Map<string, Runtime>();
|
||||
for (const runtime of SUPPORTED_RUNTIMES as Runtime[]) {
|
||||
aliasToCanonical.set(normalizeRuntimeToken(runtime), runtime);
|
||||
}
|
||||
for (const [canonical, aliases] of Object.entries(manifest)) {
|
||||
if (!(SUPPORTED_RUNTIMES as readonly string[]).includes(canonical)) continue;
|
||||
for (const alias of aliases) {
|
||||
if (typeof alias !== 'string') continue;
|
||||
aliasToCanonical.set(normalizeRuntimeToken(alias), canonical as Runtime);
|
||||
}
|
||||
}
|
||||
|
||||
export function canonicalizeRuntimeName(value: unknown): Runtime | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
return aliasToCanonical.get(normalizeRuntimeToken(value)) ?? null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user