Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 57 additions & 5 deletions src/lib/agent-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import { analytics } from '../utils/analytics';
import {
WIZARD_INTERACTION_EVENT_NAME,
WIZARD_REMARK_EVENT_NAME,
POSTHOG_PROPERTY_HEADER_PREFIX,
WIZARD_VARIANT_FLAG_KEY,
WIZARD_VARIANTS,
WIZARD_USER_AGENT,
} from './constants';
import { createCustomHeaders } from '../utils/custom-headers';
import { getLlmGatewayUrlFromHost } from '../utils/urls';
import { LINTING_TOOLS } from './safe-tools';
import { createWizardToolsServer, WIZARD_TOOL_NAMES } from './wizard-tools';
Expand Down Expand Up @@ -79,6 +83,9 @@ export type AgentConfig = {
posthogApiHost: string;
additionalMcpServers?: Record<string, { url: string }>;
detectPackageManager: PackageManagerDetector;
/** Feature flag key -> variant (evaluated at start of run). */
wizardFlags?: Record<string, string>;
wizardMetadata?: Record<string, string>;
};

/**
Expand All @@ -88,8 +95,52 @@ type AgentRunConfig = {
workingDirectory: string;
mcpServers: McpServersConfig;
model: string;
wizardFlags?: Record<string, string>;
wizardMetadata?: Record<string, string>;
};

/**
* Select wizard metadata from WIZARD_VARIANTS using the variant feature flag.
* If the flag is missing or the value is not in config, returns the "base" variant (VARIANT: "base").
*/
export function buildWizardMetadata(
flags: Record<string, string> = {},
): Record<string, string> {
const variantKey = flags[WIZARD_VARIANT_FLAG_KEY];
const variant =
(variantKey && WIZARD_VARIANTS[variantKey]) ?? WIZARD_VARIANTS['base'];
return { ...variant };
}

/**
* Build env for the SDK subprocess: process.env plus ANTHROPIC_CUSTOM_HEADERS from wizard metadata/flags.
*/
function buildAgentEnv(
wizardMetadata: Record<string, string>,
wizardFlags: Record<string, string>,
): NodeJS.ProcessEnv {
const headers = createCustomHeaders();
for (const [key, value] of Object.entries(wizardMetadata)) {
headers.add(
key.startsWith(POSTHOG_PROPERTY_HEADER_PREFIX)
? key
: `${POSTHOG_PROPERTY_HEADER_PREFIX}${key}`,
value,
);
}
for (const [flagKey, variant] of Object.entries(wizardFlags)) {
if (!flagKey.toLowerCase().startsWith('wizard')) continue;
headers.addFlag(flagKey, variant);
}
const encoded = headers.encode();
logToFile('ANTHROPIC_CUSTOM_HEADERS', encoded);
return {
...process.env,
ANTHROPIC_API_KEY: undefined,
ANTHROPIC_CUSTOM_HEADERS: encoded,
};
}

/**
* Package managers that can be used to run commands.
*/
Expand Down Expand Up @@ -379,6 +430,8 @@ export async function initializeAgent(
workingDirectory: config.workingDirectory,
mcpServers,
model: 'anthropic/claude-sonnet-4-6',
wizardFlags: config.wizardFlags,
wizardMetadata: config.wizardMetadata,
};

logToFile('Agent config:', {
Expand Down Expand Up @@ -554,11 +607,10 @@ export async function runAgent(
settingSources: ['project'],
// Explicitly enable required tools including Skill
allowedTools,
env: {
...process.env,
// Prevent user's Anthropic API key from overriding the wizard's OAuth token
ANTHROPIC_API_KEY: undefined,
},
env: buildAgentEnv(
agentConfig.wizardMetadata ?? {},
agentConfig.wizardFlags ?? {},
),
canUseTool: (toolName: string, input: unknown) => {
logToFile('canUseTool called:', { toolName, input });
const result = wizardCanUseTool(
Expand Down
7 changes: 7 additions & 0 deletions src/lib/agent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
runAgent,
AgentSignals,
AgentErrorType,
buildWizardMetadata,
} from './agent-interface';
import { getCloudUrlFromRegion } from '../utils/urls';
import chalk from 'chalk';
Expand Down Expand Up @@ -175,6 +176,10 @@ export async function runAgentWizard(
// Initialize and run agent
const spinner = clack.spinner();

// Evaluate all feature flags at the start of the run so they can be sent to the LLM gateway
const wizardFlags = await analytics.getAllFlagsForWizard();
const wizardMetadata = buildWizardMetadata(wizardFlags);

// Determine MCP URL: CLI flag > env var > production default
// Use EU subdomain for EU users to work around Claude Code's OAuth bug
// See: https://github.com/anthropics/claude-code/issues/2267
Expand All @@ -193,6 +198,8 @@ export async function runAgentWizard(
posthogApiHost: host,
additionalMcpServers: config.metadata.additionalMcpServers,
detectPackageManager: config.detection.detectPackageManager,
wizardFlags,
wizardMetadata,
},
options,
);
Expand Down
14 changes: 14 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ export const OAUTH_PORT = 8239;
export const WIZARD_INTERACTION_EVENT_NAME = 'wizard interaction';
export const WIZARD_REMARK_EVENT_NAME = 'wizard remark';

/** Feature flag key whose value selects a variant from WIZARD_VARIANTS. */
export const WIZARD_VARIANT_FLAG_KEY = 'wizard-variant';

/** Variant key -> metadata for wizard run (VARIANT flag selects which entry to use). */
export const WIZARD_VARIANTS: Record<string, Record<string, string>> = {
base: { VARIANT: 'base' },
subagents: { VARIANT: 'subagents' },
};

/** HTTP header prefix for PostHog properties (e.g. X-POSTHOG-PROPERTY-VARIANT). */
export const POSTHOG_PROPERTY_HEADER_PREFIX = 'X-POSTHOG-PROPERTY-';

/** HTTP header prefix for PostHog feature flags (full header name prefix). */
export const POSTHOG_FLAG_HEADER_PREFIX = 'X-POSTHOG-FLAG-';
/**
* User-Agent string for the wizard when making HTTP requests.
* Used for direct PostHog API calls and passed to the MCP server
Expand Down
29 changes: 29 additions & 0 deletions src/utils/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class Analytics {
private distinctId?: string;
private anonymousId: string;
private appName = 'wizard';
private activeFlags: Record<string, string> | null = null;

constructor() {
this.client = new PostHog(ANALYTICS_POSTHOG_PUBLIC_PROJECT_WRITE_KEY, {
Expand Down Expand Up @@ -75,6 +76,34 @@ export class Analytics {
}
}

/**
* Evaluate all feature flags for the current user at the start of a run.
* Result is cached; subsequent calls in the same run return the same map.
* Returns flag key -> string value (booleans become 'true'/'false').
*/
async getAllFlagsForWizard(): Promise<Record<string, string>> {
if (this.activeFlags !== null) {
return this.activeFlags;
}
try {
const distinctId = this.distinctId ?? this.anonymousId;
const result = await this.client.getAllFlagsAndPayloads(distinctId, {
personProperties: { $app_name: this.appName },
});
const flags = result.featureFlags ?? {};
const out: Record<string, string> = {};
for (const [key, value] of Object.entries(flags)) {
if (value === undefined) continue;
out[key] = typeof value === 'boolean' ? String(value) : String(value);
}
this.activeFlags = out;
return out;
} catch (error) {
debug('Failed to get all feature flags:', error);
return {};
}
}

async shutdown(status: 'success' | 'error' | 'cancelled') {
if (Object.keys(this.tags).length === 0) {
return;
Expand Down
30 changes: 30 additions & 0 deletions src/utils/custom-headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { POSTHOG_FLAG_HEADER_PREFIX } from '../lib/constants';

/**
* Builds a list of custom headers for ANTHROPIC_CUSTOM_HEADERS.
*/
export function createCustomHeaders(): {
add(key: string, value: string): void;
/** Add a feature flag for PostHog ($feature/<flagKey>: variant). */
addFlag(flagKey: string, variant: string): void;
encode(): string;
} {
const entries: Array<{ key: string; value: string }> = [];

return {
add(key: string, value: string): void {
const name =
key.startsWith('x-') || key.startsWith('X-') ? key : `X-${key}`;
entries.push({ key: name, value });
},

addFlag(flagKey: string, variant: string): void {
const headerName = POSTHOG_FLAG_HEADER_PREFIX + flagKey.toUpperCase();
entries.push({ key: headerName, value: variant });
},

encode(): string {
return entries.map(({ key, value }) => `${key}: ${value}`).join('\n');
},
};
}
Loading