Skip to content

Commit 51706a2

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

4 files changed

Lines changed: 143 additions & 18 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {
2+
buildWizardMetadata,
3+
isOrchestratorEnabled,
4+
} from '@lib/agent/agent-interface';
5+
6+
describe('isOrchestratorEnabled', () => {
7+
it('is true only when the wizard-orchestrator flag is true', () => {
8+
expect(isOrchestratorEnabled({ 'wizard-orchestrator': 'true' })).toBe(true);
9+
});
10+
11+
it('is false when the flag is false, another flag, or absent', () => {
12+
expect(isOrchestratorEnabled({ 'wizard-orchestrator': 'false' })).toBe(
13+
false,
14+
);
15+
expect(isOrchestratorEnabled({ 'wizard-variant': 'orchestrator' })).toBe(
16+
false,
17+
);
18+
expect(isOrchestratorEnabled({})).toBe(false);
19+
expect(isOrchestratorEnabled()).toBe(false);
20+
});
21+
});
22+
23+
describe('buildWizardMetadata', () => {
24+
it('selects a known variant header from the flag', () => {
25+
expect(buildWizardMetadata({ 'wizard-variant': 'subagents' })).toEqual({
26+
VARIANT: 'subagents',
27+
});
28+
});
29+
30+
it('falls back to the base variant for unknown or missing flags', () => {
31+
expect(buildWizardMetadata({ 'wizard-variant': 'nope' })).toEqual({
32+
VARIANT: 'base',
33+
});
34+
expect(buildWizardMetadata({})).toEqual({ VARIANT: 'base' });
35+
});
36+
});

src/lib/agent/agent-interface.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
POSTHOG_PROPERTY_HEADER_PREFIX,
1616
WIZARD_VARIANT_FLAG_KEY,
1717
WIZARD_VARIANTS,
18+
WIZARD_ORCHESTRATOR_FLAG_KEY,
1819
WIZARD_USER_AGENT,
1920
} from '@lib/constants';
2021
import {
@@ -245,6 +246,17 @@ export function buildWizardMetadata(
245246
return { ...variant };
246247
}
247248

249+
/**
250+
* Whether this run uses the experimental task-queue orchestrator. Gated by the
251+
* boolean `wizard-orchestrator` feature flag, targeted to the user in the wizard's
252+
* analytics project.
253+
*/
254+
export function isOrchestratorEnabled(
255+
flags: Record<string, string> = {},
256+
): boolean {
257+
return flags[WIZARD_ORCHESTRATOR_FLAG_KEY] === 'true';
258+
}
259+
248260
/**
249261
* Build env for the SDK subprocess: process.env plus ANTHROPIC_CUSTOM_HEADERS, which always
250262
* includes `x-posthog-use-bedrock-fallback: true` so the LLM gateway falls back to Bedrock on

src/lib/agent/agent-runner.ts

Lines changed: 93 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 {
@@ -53,7 +55,7 @@ import { getSkillsBaseUrl } from '@lib/constants';
5355
import { runtimeEnv } from '@env';
5456
import { installSkillById, type InstallSkillResult } from '@lib/wizard-tools';
5557
import { createWizardAskBridge } from '@lib/wizard-ask-bridge';
56-
import type { WizardRunOptions } from '@utils/types';
58+
import type { WizardRunOptions, CloudRegion } from '@utils/types';
5759

5860
import type { ProgramConfig } from '@lib/programs/program-step';
5961
import { assemblePrompt, type PromptContext } from './agent-prompt';
@@ -108,7 +110,7 @@ export interface ProgramRun {
108110
buildOutroData?: (
109111
session: WizardSession,
110112
credentials: Credentials,
111-
cloudRegion: import('@utils/types').CloudRegion | undefined,
113+
cloudRegion: CloudRegion | undefined,
112114
) => WizardSession['outroData'];
113115
/**
114116
* Per-run cap on `wizard_ask` invocations. Defaults to 10. The 4th call
@@ -124,6 +126,23 @@ export interface ProgramRun {
124126
askTimeoutMs?: number;
125127
}
126128

129+
/**
130+
* Result of the shared bootstrap, consumed by both the linear and the
131+
* orchestrator arm. Credentials, role, and user are already applied to the
132+
* session by `bootstrapProgram`; this carries the values both arms still need.
133+
*/
134+
export interface BootstrapResult {
135+
skillsBaseUrl: string;
136+
projectApiKey: Credentials['projectApiKey'];
137+
host: Credentials['host'];
138+
accessToken: Credentials['accessToken'];
139+
projectId: Credentials['projectId'];
140+
cloudRegion: CloudRegion;
141+
mcpUrl: string;
142+
wizardFlags: Record<string, string>;
143+
wizardMetadata: Record<string, string>;
144+
}
145+
127146
// ── Helpers ──────────────────────────────────────────────────────────
128147

129148
/**
@@ -179,16 +198,31 @@ export async function runAgent(
179198
/**
180199
* Run a program's agent pipeline.
181200
*
182-
* This is the single execution path for all programs — both skill-based
183-
* (revenue analytics) and framework-based (core integration). The
184-
* `ProgramRun` controls what varies between them; `programConfig` carries
185-
* the program-level static metadata (tool allow/disallow lists, etc.).
201+
* Runs the shared bootstrap, then forks on the `wizard-variant` flag. The
202+
* `orchestrator` variant routes to the experimental task-queue runner; every
203+
* other variant runs the linear pipeline.
186204
*/
187205
export async function runProgram(
188206
session: WizardSession,
189207
config: ProgramRun,
190208
programConfig: ProgramConfig,
191209
): Promise<void> {
210+
const boot = await bootstrapProgram(session, config, programConfig);
211+
212+
return runLinearProgram(session, config, programConfig, boot);
213+
}
214+
215+
/**
216+
* Shared setup for both arms: logging, health check, settings conflicts, OAuth
217+
* and credentials, then the feature flags, variant metadata, and MCP url. Sets
218+
* `session.credentials`, role, and user as a side effect. Returns the values the
219+
* arms still need.
220+
*/
221+
async function bootstrapProgram(
222+
session: WizardSession,
223+
config: ProgramRun,
224+
programConfig: ProgramConfig,
225+
): Promise<BootstrapResult> {
192226
// 1. Init logging + debug
193227
initLogFile();
194228
session.skillId = config.skillId ?? config.integrationLabel;
@@ -310,10 +344,60 @@ export async function runProgram(
310344
// install and agent start, so no source leaves the machine. The screen
311345
// alone is cosmetic; this await is the actual gate. Resolves
312346
// immediately when the program declared requiresAi: false or in CI.
347+
// In bootstrapProgram so both the linear and orchestrator arms gate.
313348
logToFile('[agent-runner] checking AI opt-in gate');
314349
await getUI().waitForAiOptIn();
315350
logToFile('[agent-runner] AI opt-in gate cleared');
316351

352+
// Feature flags, variant metadata, and MCP url. Both arms need these, and the
353+
// fork decision reads the flags.
354+
const wizardFlags = await analytics.getAllFlagsForWizard();
355+
const wizardMetadata = buildWizardMetadata(wizardFlags);
356+
357+
const mcpUrl = session.localMcp
358+
? 'http://localhost:8787/mcp'
359+
: runtimeEnv('MCP_URL') ||
360+
(cloudRegion === 'eu'
361+
? 'https://mcp-eu.posthog.com/mcp'
362+
: 'https://mcp.posthog.com/mcp');
363+
364+
return {
365+
skillsBaseUrl,
366+
projectApiKey,
367+
host,
368+
accessToken,
369+
projectId,
370+
cloudRegion,
371+
mcpUrl,
372+
wizardFlags,
373+
wizardMetadata,
374+
};
375+
}
376+
377+
/**
378+
* The linear pipeline. Single execution path for all non-orchestrator programs,
379+
* both skill-based (revenue analytics) and framework-based (core integration).
380+
* The `ProgramRun` controls what varies between them; `programConfig` carries the
381+
* program-level static metadata (tool allow/disallow lists, etc.).
382+
*/
383+
async function runLinearProgram(
384+
session: WizardSession,
385+
config: ProgramRun,
386+
programConfig: ProgramConfig,
387+
boot: BootstrapResult,
388+
): Promise<void> {
389+
const {
390+
skillsBaseUrl,
391+
projectApiKey,
392+
host,
393+
accessToken,
394+
projectId,
395+
cloudRegion,
396+
mcpUrl,
397+
wizardFlags,
398+
wizardMetadata,
399+
} = boot;
400+
317401
// 5. Skill install (if skillId provided)
318402
let skillPath: string | undefined;
319403
if (config.skillId) {
@@ -333,15 +417,6 @@ export async function runProgram(
333417

334418
// 6. Initialize agent
335419
const spinner = getUI().spinner();
336-
const wizardFlags = await analytics.getAllFlagsForWizard();
337-
const wizardMetadata = buildWizardMetadata(wizardFlags);
338-
339-
const mcpUrl = session.localMcp
340-
? 'http://localhost:8787/mcp'
341-
: runtimeEnv('MCP_URL') ||
342-
(cloudRegion === 'eu'
343-
? 'https://mcp-eu.posthog.com/mcp'
344-
: 'https://mcp.posthog.com/mcp');
345420

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

src/lib/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ export const WIZARD_INTERACTION_EVENT_NAME = 'wizard interaction';
176176
export const WIZARD_REMARK_EVENT_NAME = 'wizard remark';
177177
/** Feature flag key whose value selects a variant from WIZARD_VARIANTS. */
178178
export const WIZARD_VARIANT_FLAG_KEY = 'wizard-variant';
179+
/** Boolean feature flag that routes a run to the experimental orchestrator runner. */
180+
export const WIZARD_ORCHESTRATOR_FLAG_KEY = 'wizard-orchestrator';
179181
/** Feature flag key that gates the intro-screen "Tools" menu. */
180182
export const WIZARD_TOOLS_MENU_FLAG_KEY = 'wizard-tools-menu';
181183
/** Variant key -> metadata for wizard run (VARIANT flag selects which entry to use). */

0 commit comments

Comments
 (0)