refactor(config): stabilize synchronous experiment access and add real-time updates

- Revert getExperimentValue to a synchronous interface to maintain compatibility with existing callers.
- Add Config.updateExperimentalSettings() to support immediate application of /experiment set/unset changes.
- Implement CLI argument normalization to handle kebab-case, camelCase, and snake_case consistently.
- Enhance getExperimentFlagIdFromName to be more resilient to different naming conventions.
- Rename getNumericalRoutingEnabled to isNumericalRoutingEnabled for better idiomatic consistency.
- Update documentation in experimentation skill to recommend strongly-typed wrappers in Config.ts.
- Add regression tests for CLI overrides and update all relevant routing and command tests.
- Fix lint errors by removing unnecessary await/Promise.all for synchronous config methods.
This commit is contained in:
mkorwel
2026-02-19 20:42:00 -06:00
committed by Matt Korwel
parent 8d041e2acd
commit 6dd2d219d9
14 changed files with 148 additions and 65 deletions
+32 -22
View File
@@ -889,36 +889,46 @@ export async function loadCliConfig(
const [key, ...valueParts] = entry.split('=');
const value = valueParts.join('=');
if (key && value !== undefined) {
// Normalize key to kebab-case (e.g., enableNumericalRouting -> enable-numerical-routing)
const normalizedKey = key
.replace(/([a-z])([A-Z])/g, '$1-$2')
.toLowerCase()
.replace(/_/g, '-');
// Simple type inference for CLI args
if (value === 'true') experimentalCliArgs[key] = true;
else if (value === 'false') experimentalCliArgs[key] = false;
if (value === 'true') experimentalCliArgs[normalizedKey] = true;
else if (value === 'false') experimentalCliArgs[normalizedKey] = false;
else if (!isNaN(Number(value)))
experimentalCliArgs[key] = Number(value);
else experimentalCliArgs[key] = value;
experimentalCliArgs[normalizedKey] = Number(value);
else experimentalCliArgs[normalizedKey] = value;
}
}
}
let clientName: string | undefined = undefined;
if (isAcpMode) {
const ide = detectIdeFromEnv();
if (
ide &&
(ide.name !== 'vscode' || process.env['TERM_PROGRAM'] === 'vscode')
) {
clientName = `acp-${ide.name}`;
}
let clientName: string | undefined = undefined;
if (isAcpMode) {
const ide = detectIdeFromEnv();
if (
ide &&
(ide.name !== 'vscode' || process.env['TERM_PROGRAM'] === 'vscode')
) {
clientName = `acp-${ide.name}`;
}
}
const useGeneralistProfile =
settings.experimental?.generalistProfile ?? false;
const useContextManagement =
settings.experimental?.contextManagement ?? false;
const contextManagement = {
...(useGeneralistProfile ? generalistProfile : {}),
...(useContextManagement ? settings?.contextManagement : {}),
enabled: useContextManagement || useGeneralistProfile,
};
const useGeneralistProfile =
settings.experimental?.generalistProfile ?? false;
const useContextManagement =
settings.experimental?.contextManagement ?? false;
const contextManagement = {
...(useGeneralistProfile ? generalistProfile : {}),
...(useContextManagement ? settings?.contextManagement : {}),
enabled: useContextManagement || useGeneralistProfile,
};
if (debugMode && Object.keys(experimentalCliArgs).length > 0) {
debugLogger.debug('Experimental CLI args:', experimentalCliArgs);
}
return new Config({
acpMode: isAcpMode,
clientName,
+1 -1
View File
@@ -120,7 +120,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
isTrustedFolder: vi.fn().mockReturnValue(true),
getCompressionThreshold: vi.fn().mockResolvedValue(undefined),
getUserCaching: vi.fn().mockResolvedValue(false),
getNumericalRoutingEnabled: vi.fn().mockResolvedValue(false),
isNumericalRoutingEnabled: vi.fn().mockReturnValue(false),
getClassifierThreshold: vi.fn().mockResolvedValue(undefined),
getBannerTextNoCapacityIssues: vi.fn().mockResolvedValue(''),
getBannerTextCapacityIssues: vi.fn().mockResolvedValue(''),
@@ -18,6 +18,7 @@ describe('experimentCommand', () => {
services: {
config: {
getExperimentValue: vi.fn(),
updateExperimentalSettings: vi.fn(),
},
settings: {
merged: {
@@ -107,13 +107,16 @@ const setExperimentCommand: SlashCommand = {
value = rawValue;
}
const { settings } = context.services;
const { settings, config } = context.services;
if (!config) return;
const currentExperimental = {
...((settings.merged.experimental as Record<string, unknown>) || {}),
};
currentExperimental[name] = value;
settings.setValue(SettingScope.User, 'experimental', currentExperimental);
config.updateExperimentalSettings(currentExperimental);
context.ui.addItem({
type: MessageType.INFO,
@@ -138,7 +141,9 @@ const unsetExperimentCommand: SlashCommand = {
return;
}
const { settings } = context.services;
const { settings, config } = context.services;
if (!config) return;
const currentExperimental = {
...((settings.merged.experimental as Record<string, unknown>) || {}),
};
@@ -153,6 +158,7 @@ const unsetExperimentCommand: SlashCommand = {
delete currentExperimental[name];
settings.setValue(SettingScope.User, 'experimental', currentExperimental);
config.updateExperimentalSettings(currentExperimental);
context.ui.addItem({
type: MessageType.INFO,