Skip to content

Commit 4438709

Browse files
gewenyu99claude
andcommitted
feat(orchestrator): variant gating + shared bootstrap extraction
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7f0ee00 commit 4438709

7 files changed

Lines changed: 218 additions & 22 deletions

File tree

src/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ type RuntimeEnvKey =
4444
| 'POSTHOG_WIZARD_BENCHMARK_FILE'
4545
| 'POSTHOG_WIZARD_LOG_DIR'
4646
| 'POSTHOG_WIZARD_DEBUG'
47+
// Dev-only feature-flag override (JSON map of flagKey -> value). No effect in
48+
// published builds, where IS_PRODUCTION_BUILD is true.
49+
| 'POSTHOG_WIZARD_FORCE_FLAGS'
4750
| 'DEBUG'
4851
// Agent / MCP
4952
| 'MCP_URL'
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {
2+
buildWizardMetadata,
3+
isOrchestratorVariant,
4+
} from '@lib/agent/agent-interface';
5+
6+
describe('isOrchestratorVariant', () => {
7+
it('is true only when the wizard-variant flag is orchestrator', () => {
8+
expect(isOrchestratorVariant({ 'wizard-variant': 'orchestrator' })).toBe(
9+
true,
10+
);
11+
});
12+
13+
it('is false for other variants, a missing flag, and no flags', () => {
14+
expect(isOrchestratorVariant({ 'wizard-variant': 'base' })).toBe(false);
15+
expect(isOrchestratorVariant({ 'wizard-variant': 'subagents' })).toBe(
16+
false,
17+
);
18+
expect(isOrchestratorVariant({ 'some-other-flag': 'orchestrator' })).toBe(
19+
false,
20+
);
21+
expect(isOrchestratorVariant({})).toBe(false);
22+
expect(isOrchestratorVariant()).toBe(false);
23+
});
24+
});
25+
26+
describe('buildWizardMetadata', () => {
27+
it('selects the orchestrator variant header from the flag', () => {
28+
expect(buildWizardMetadata({ 'wizard-variant': 'orchestrator' })).toEqual({
29+
VARIANT: 'orchestrator',
30+
});
31+
});
32+
33+
it('falls back to the base variant for unknown or missing flags', () => {
34+
expect(buildWizardMetadata({ 'wizard-variant': 'nope' })).toEqual({
35+
VARIANT: 'base',
36+
});
37+
expect(buildWizardMetadata({})).toEqual({ VARIANT: 'base' });
38+
});
39+
});

src/lib/agent/agent-interface.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,18 @@ export function buildWizardMetadata(
363363
return { ...variant };
364364
}
365365

366+
/**
367+
* Whether this run uses the experimental task-queue orchestrator. Selected by the
368+
* `wizard-variant` feature flag value `orchestrator`. Gating is the flag: target it
369+
* to your user in PostHog, or in a dev build force it with
370+
* `POSTHOG_WIZARD_FORCE_FLAGS='{"wizard-variant":"orchestrator"}'`.
371+
*/
372+
export function isOrchestratorVariant(
373+
flags: Record<string, string> = {},
374+
): boolean {
375+
return flags[WIZARD_VARIANT_FLAG_KEY] === 'orchestrator';
376+
}
377+
366378
/**
367379
* Build env for the SDK subprocess: process.env plus ANTHROPIC_CUSTOM_HEADERS, which always
368380
* includes `x-posthog-use-bedrock-fallback: true` so the LLM gateway falls back to Bedrock on

src/lib/agent/agent-runner.ts

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
* - What MCP servers and package manager detector to use
1010
* - What happens after the agent completes
1111
*
12-
* The pipeline itself is fixed:
13-
* init → health check → settings → OAuth → [skill install] →
14-
* agent init → prompt → run → errors → [postRun] → outro
12+
* The pipeline runs a shared bootstrap (logging, health check, settings, OAuth,
13+
* flags, MCP url), then forks. The `orchestrator` variant routes to the
14+
* experimental task-queue runner. Every other variant runs the fixed linear
15+
* pipeline:
16+
* [skill install] → agent init → prompt → run → errors → [postRun] → outro
1517
*/
1618

1719
import {
@@ -29,10 +31,12 @@ import {
2931
AgentErrorType,
3032
AgentSignals,
3133
buildWizardMetadata,
34+
isOrchestratorVariant,
3235
checkAllSettingsConflicts,
3336
backupAndFixClaudeSettings,
3437
restoreClaudeSettings,
3538
} from './agent-interface';
39+
import { runOrchestrator } from '../programs/orchestrator/orchestrator-runner';
3640
import { getCloudUrlFromRegion } from '@utils/urls';
3741
import {
3842
evaluateWizardReadiness,
@@ -51,7 +55,7 @@ import { getSkillsBaseUrl } from '@lib/constants';
5155
import { runtimeEnv } from '@env';
5256
import { installSkillById, type InstallSkillResult } from '@lib/wizard-tools';
5357
import { createWizardAskBridge } from '@lib/wizard-ask-bridge';
54-
import type { WizardRunOptions } from '@utils/types';
58+
import type { WizardRunOptions, CloudRegion } from '@utils/types';
5559

5660
import type { ProgramConfig } from '@lib/programs/program-step';
5761
import { assemblePrompt, type PromptContext } from './agent-prompt';
@@ -106,7 +110,7 @@ export interface ProgramRun {
106110
buildOutroData?: (
107111
session: WizardSession,
108112
credentials: Credentials,
109-
cloudRegion: import('@utils/types').CloudRegion | undefined,
113+
cloudRegion: CloudRegion | undefined,
110114
) => WizardSession['outroData'];
111115
/**
112116
* Per-run cap on `wizard_ask` invocations. Defaults to 10. The 4th call
@@ -115,6 +119,23 @@ export interface ProgramRun {
115119
maxQuestions?: number;
116120
}
117121

122+
/**
123+
* Result of the shared bootstrap, consumed by both the linear and the
124+
* orchestrator arm. Credentials, role, and user are already applied to the
125+
* session by `bootstrapProgram`; this carries the values both arms still need.
126+
*/
127+
export interface BootstrapResult {
128+
skillsBaseUrl: string;
129+
projectApiKey: Credentials['projectApiKey'];
130+
host: Credentials['host'];
131+
accessToken: Credentials['accessToken'];
132+
projectId: Credentials['projectId'];
133+
cloudRegion: CloudRegion;
134+
mcpUrl: string;
135+
wizardFlags: Record<string, string>;
136+
wizardMetadata: Record<string, string>;
137+
}
138+
118139
// ── Helpers ──────────────────────────────────────────────────────────
119140

120141
/**
@@ -170,16 +191,35 @@ export async function runAgent(
170191
/**
171192
* Run a program's agent pipeline.
172193
*
173-
* This is the single execution path for all programs — both skill-based
174-
* (revenue analytics) and framework-based (core integration). The
175-
* `ProgramRun` controls what varies between them; `programConfig` carries
176-
* the program-level static metadata (tool allow/disallow lists, etc.).
194+
* Runs the shared bootstrap, then forks on the `wizard-variant` flag. The
195+
* `orchestrator` variant routes to the experimental task-queue runner; every
196+
* other variant runs the linear pipeline.
177197
*/
178198
export async function runProgram(
179199
session: WizardSession,
180200
config: ProgramRun,
181201
programConfig: ProgramConfig,
182202
): Promise<void> {
203+
const boot = await bootstrapProgram(session, config, programConfig);
204+
205+
if (isOrchestratorVariant(boot.wizardFlags)) {
206+
return runOrchestrator(session, programConfig, boot);
207+
}
208+
209+
return runLinearProgram(session, config, programConfig, boot);
210+
}
211+
212+
/**
213+
* Shared setup for both arms: logging, health check, settings conflicts, OAuth
214+
* and credentials, then the feature flags, variant metadata, and MCP url. Sets
215+
* `session.credentials`, role, and user as a side effect. Returns the values the
216+
* arms still need.
217+
*/
218+
async function bootstrapProgram(
219+
session: WizardSession,
220+
config: ProgramRun,
221+
programConfig: ProgramConfig,
222+
): Promise<BootstrapResult> {
183223
// 1. Init logging + debug
184224
initLogFile();
185225
session.skillId = config.skillId ?? config.integrationLabel;
@@ -283,6 +323,55 @@ export async function runProgram(
283323

284324
analytics.setGroups(groupsFromUser(user, host));
285325

326+
// Feature flags, variant metadata, and MCP url. Both arms need these, and the
327+
// fork decision reads the flags.
328+
const wizardFlags = await analytics.getAllFlagsForWizard();
329+
const wizardMetadata = buildWizardMetadata(wizardFlags);
330+
331+
const mcpUrl = session.localMcp
332+
? 'http://localhost:8787/mcp'
333+
: runtimeEnv('MCP_URL') ||
334+
(cloudRegion === 'eu'
335+
? 'https://mcp-eu.posthog.com/mcp'
336+
: 'https://mcp.posthog.com/mcp');
337+
338+
return {
339+
skillsBaseUrl,
340+
projectApiKey,
341+
host,
342+
accessToken,
343+
projectId,
344+
cloudRegion,
345+
mcpUrl,
346+
wizardFlags,
347+
wizardMetadata,
348+
};
349+
}
350+
351+
/**
352+
* The linear pipeline. Single execution path for all non-orchestrator programs,
353+
* both skill-based (revenue analytics) and framework-based (core integration).
354+
* The `ProgramRun` controls what varies between them; `programConfig` carries the
355+
* program-level static metadata (tool allow/disallow lists, etc.).
356+
*/
357+
async function runLinearProgram(
358+
session: WizardSession,
359+
config: ProgramRun,
360+
programConfig: ProgramConfig,
361+
boot: BootstrapResult,
362+
): Promise<void> {
363+
const {
364+
skillsBaseUrl,
365+
projectApiKey,
366+
host,
367+
accessToken,
368+
projectId,
369+
cloudRegion,
370+
mcpUrl,
371+
wizardFlags,
372+
wizardMetadata,
373+
} = boot;
374+
286375
// 5. Skill install (if skillId provided)
287376
let skillPath: string | undefined;
288377
if (config.skillId) {
@@ -302,15 +391,6 @@ export async function runProgram(
302391

303392
// 6. Initialize agent
304393
const spinner = getUI().spinner();
305-
const wizardFlags = await analytics.getAllFlagsForWizard();
306-
const wizardMetadata = buildWizardMetadata(wizardFlags);
307-
308-
const mcpUrl = session.localMcp
309-
? 'http://localhost:8787/mcp'
310-
: runtimeEnv('MCP_URL') ||
311-
(cloudRegion === 'eu'
312-
? 'https://mcp-eu.posthog.com/mcp'
313-
: 'https://mcp.posthog.com/mcp');
314394

315395
const restoreSettings = () => restoreClaudeSettings(session.installDir);
316396
getUI().onEnterScreen('outro', restoreSettings);

src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const WIZARD_TOOLS_MENU_FLAG_KEY = 'wizard-tools-menu';
147147
export const WIZARD_VARIANTS: Record<string, Record<string, string>> = {
148148
base: { VARIANT: 'base' },
149149
subagents: { VARIANT: 'subagents' },
150+
orchestrator: { VARIANT: 'orchestrator' },
150151
};
151152
/** User-Agent for wizard HTTP requests and MCP server identification. */
152153
export const WIZARD_USER_AGENT = `posthog/wizard; version: ${VERSION}`;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Experimental task-queue orchestrator runner.
3+
*
4+
* Branches from the linear runner when the `wizard-variant` feature flag is
5+
* `orchestrator`. The shape: an orchestrator agent inspects the repo and seeds an
6+
* in-memory task queue, and an executor drains it one fresh agent per task.
7+
*
8+
* This is the stub. It logs, emits a start event, and returns. The queue, the
9+
* executor, and the seeding agent land in the following issues.
10+
*/
11+
import type { WizardSession } from '../../wizard-session';
12+
import type { ProgramConfig } from '../program-step';
13+
import type { BootstrapResult } from '../../agent/agent-runner';
14+
import { getUI } from '../../../ui';
15+
import { logToFile } from '../../../utils/debug';
16+
import { analytics } from '../../../utils/analytics';
17+
18+
export function runOrchestrator(
19+
session: WizardSession,
20+
programConfig: ProgramConfig,
21+
boot: BootstrapResult,
22+
): Promise<void> {
23+
logToFile(
24+
`[orchestrator] START program=${programConfig.id} dir=${session.installDir} variant=${boot.wizardMetadata.VARIANT}`,
25+
);
26+
analytics.wizardCapture('orchestrator started', {
27+
program_id: programConfig.id,
28+
});
29+
getUI().log.info(
30+
'Orchestrator variant is active. This runner is a stub for now; the queue and executor land in the following issues.',
31+
);
32+
return Promise.resolve();
33+
}

src/utils/analytics.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,33 @@ import type { WizardSession } from '@lib/wizard-session';
88
import type { ApiUser } from '@lib/api';
99
import { v4 as uuidv4 } from 'uuid';
1010
import { debug } from './debug';
11+
import { IS_PRODUCTION_BUILD, runtimeEnv } from '@env';
12+
13+
/**
14+
* Dev-only feature-flag override. In non-production builds,
15+
* `POSTHOG_WIZARD_FORCE_FLAGS` (a JSON object of flagKey -> value) is merged over
16+
* the evaluated flags, so a developer can force e.g.
17+
* `{"wizard-variant":"orchestrator"}` without targeting the flag in PostHog. It
18+
* has no effect in published builds, where `IS_PRODUCTION_BUILD` is true.
19+
*/
20+
function applyDevFlagOverrides(
21+
flags: Record<string, string>,
22+
): Record<string, string> {
23+
if (IS_PRODUCTION_BUILD) return flags;
24+
const raw = runtimeEnv('POSTHOG_WIZARD_FORCE_FLAGS');
25+
if (!raw) return flags;
26+
try {
27+
const overrides = JSON.parse(raw) as Record<string, unknown>;
28+
const merged = { ...flags };
29+
for (const [key, value] of Object.entries(overrides)) {
30+
merged[key] = String(value);
31+
}
32+
return merged;
33+
} catch (error) {
34+
debug('Invalid POSTHOG_WIZARD_FORCE_FLAGS:', error);
35+
return flags;
36+
}
37+
}
1138

1239
/**
1340
* Extract a standard property bag from the current session.
@@ -144,23 +171,24 @@ export class Analytics {
144171
if (this.activeFlags !== null) {
145172
return this.activeFlags;
146173
}
174+
let out: Record<string, string> = {};
147175
try {
148176
const distinctId = this.distinctId ?? this.anonymousId;
149177
const result = await this.client.getAllFlagsAndPayloads(distinctId, {
150178
personProperties: { $app_name: this.appName },
151179
});
152180
const flags = result.featureFlags ?? {};
153-
const out: Record<string, string> = {};
154181
for (const [key, value] of Object.entries(flags)) {
155182
if (value === undefined) continue;
156183
out[key] = typeof value === 'boolean' ? String(value) : String(value);
157184
}
158-
this.activeFlags = out;
159-
return out;
160185
} catch (error) {
161186
debug('Failed to get all feature flags:', error);
162-
return {};
187+
out = {};
163188
}
189+
out = applyDevFlagOverrides(out);
190+
this.activeFlags = out;
191+
return out;
164192
}
165193

166194
async shutdown(status: 'success' | 'error' | 'cancelled') {

0 commit comments

Comments
 (0)