From d05e3d7fa79d44661663eef656a3ae34bc54133e Mon Sep 17 00:00:00 2001 From: Tal Gluck Date: Thu, 3 Sep 2026 16:29:56 +0200 Subject: [PATCH] feat(feature-flags): add `wizard feature-flags` command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds PostHog feature flags to a project. Mirrors ai-observability's shape: context-mill's `feature-flags` skill group ships one variant per language/framework with no example apps to detect against, so this program does no framework matching itself — the agent loads the skill menu, matches the project's manifest against it, and installs the right variant, per the custom prompt in src/lib/programs/feature-flags/index.ts. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01HBZpxnSyEjzrmLLUpgPVR3 --- bin.ts | 2 + src/commands/feature-flags.ts | 14 +++++ .../runner/__tests__/switchboard.test.ts | 14 ++++- .../switchboard/flags/__tests__/flags.test.ts | 5 +- src/lib/agent/runner/switchboard/index.ts | 7 +++ src/lib/programs/feature-flags/index.ts | 56 +++++++++++++++++++ src/lib/programs/program-registry.ts | 3 + 7 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/commands/feature-flags.ts create mode 100644 src/lib/programs/feature-flags/index.ts diff --git a/bin.ts b/bin.ts index f4bb1bd58..ac26fe15b 100644 --- a/bin.ts +++ b/bin.ts @@ -58,6 +58,7 @@ import { mcpAnalyticsCommand } from './src/commands/mcp-analytics'; import { replayVisionCommand } from './src/commands/replay-vision'; import { aiObservabilityCommand } from './src/commands/ai-observability'; import { metricsCommand } from './src/commands/metrics'; +import { featureFlagsCommand } from './src/commands/feature-flags'; import { auditCommand } from './src/commands/audit'; import { doctorCommand } from './src/commands/doctor'; import { migrateCommand } from './src/commands/migrate'; @@ -91,6 +92,7 @@ Wizard.use(basicIntegrationCommand) .use(replayVisionCommand) .use(aiObservabilityCommand) .use(metricsCommand) + .use(featureFlagsCommand) .use(cliCommand) .use(auditCommand) .use(doctorCommand) diff --git a/src/commands/feature-flags.ts b/src/commands/feature-flags.ts new file mode 100644 index 000000000..0914c91f2 --- /dev/null +++ b/src/commands/feature-flags.ts @@ -0,0 +1,14 @@ +import { featureFlagsConfig } from '@lib/programs/feature-flags/index'; + +import type { Command } from './command'; +import { nativeCommandFactory } from './factories/native-command-factory'; + +/** + * `wizard feature-flags` — add PostHog feature flags to the project. + * + * The agent picks the language/framework variant itself (see the program's + * `customPrompt`) since context-mill's `feature-flags` skill has no example + * apps to detect against. + */ +export const featureFlagsCommand: Command = + nativeCommandFactory(featureFlagsConfig); diff --git a/src/lib/agent/runner/__tests__/switchboard.test.ts b/src/lib/agent/runner/__tests__/switchboard.test.ts index aa925c6dc..c2a90808d 100644 --- a/src/lib/agent/runner/__tests__/switchboard.test.ts +++ b/src/lib/agent/runner/__tests__/switchboard.test.ts @@ -61,6 +61,7 @@ describe('switchboard PROGRAM_BINDINGS', () => { it('resolves every program, unflagged, to the same default binding', () => { for (const program of PROGRAM_IDS) { if (program === 'ai-observability') continue; // pinned below + if (program === 'feature-flags') continue; // pinned below if (program === 'error-tracking-upload-source-maps') continue; // pinned below if (program === 'metrics') continue; // pinned below if (program === 'replay-vision') continue; // pinned below @@ -80,6 +81,17 @@ describe('switchboard PROGRAM_BINDINGS', () => { }, trace: { harness: 'binding', model: 'binding', sequence: 'binding' }, }, + { + name: 'binds feature-flags to anthropic + sonnet 5', + ctx: { program: 'feature-flags', flags: {} }, + binding: { + sequence: Sequence.linear, + harness: Harness.anthropic, + model: SONNET_5_MODEL, + thinkingLevel: undefined, + }, + trace: { harness: 'binding', model: 'binding', sequence: 'binding' }, + }, { name: 'binds source-map uploads to pi + sol medium', ctx: { program: 'error-tracking-upload-source-maps', flags: {} }, @@ -223,7 +235,7 @@ describe('switchboard composed clamp', () => { expect(resolveBinding(ctx)).toEqual( program === 'posthog-integration' ? { ...DEFAULT_RESOLVED, harness: Harness.pi } - : program === 'ai-observability' + : program === 'ai-observability' || program === 'feature-flags' ? { ...DEFAULT_RESOLVED, model: SONNET_5_MODEL } : program === 'error-tracking-upload-source-maps' ? { diff --git a/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts b/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts index 16383d72a..df9f1e03d 100644 --- a/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts +++ b/src/lib/agent/runner/switchboard/flags/__tests__/flags.test.ts @@ -273,7 +273,10 @@ describe('isolation — everything on at once', () => { model: GPT5_6_TERRA_MODEL, thinkingLevel: 'high', }); - } else if (program === 'ai-observability') { + } else if ( + program === 'ai-observability' || + program === 'feature-flags' + ) { expect(resolved).toEqual({ ...LINEAR_ANTHROPIC_DEFAULT, model: SONNET_5_MODEL, diff --git a/src/lib/agent/runner/switchboard/index.ts b/src/lib/agent/runner/switchboard/index.ts index fb4232036..366a72b3b 100644 --- a/src/lib/agent/runner/switchboard/index.ts +++ b/src/lib/agent/runner/switchboard/index.ts @@ -160,6 +160,13 @@ export const PROGRAM_BINDINGS: Partial> = { model: SONNET_5_MODEL, }, slack: DEFAULT_BINDING, + // Same shape as ai-observability: no pre-installed skill, the agent + // self-selects the language/framework variant via load_skill_menu. + 'feature-flags': { + sequence: Sequence.linear, + harness: Harness.anthropic, + model: SONNET_5_MODEL, + }, }; // ── Unified resolver ──────────────────────────────────────────────────── diff --git a/src/lib/programs/feature-flags/index.ts b/src/lib/programs/feature-flags/index.ts new file mode 100644 index 000000000..5007dec25 --- /dev/null +++ b/src/lib/programs/feature-flags/index.ts @@ -0,0 +1,56 @@ +import type { ProgramConfig } from '@lib/programs/program-step'; +import { AGENT_SKILL_STEPS } from '@lib/programs/agent-skill/index'; +import { getContentBlocks } from '@lib/programs/agent-skill/content/index'; + +const FEATURE_FLAGS_REPORT_FILE = 'posthog-feature-flags-report.md'; + +/** + * `wizard feature-flags` — add PostHog feature flag code to the project. + * + * No `run.skillId`: the context-mill `feature-flags` group ships one variant + * per language/framework (React, Next.js, Django, Rails, Go, ...) with no + * example apps to detect against, so the wizard does no framework matching — + * the agent loads the menu, matches the manifest, and installs the right + * variant itself (see `customPrompt`). Mirrors `ai-observability`. + */ +export const featureFlagsConfig: ProgramConfig = { + command: 'feature-flags', + description: 'Add PostHog feature flags to your project', + id: 'feature-flags', + steps: AGENT_SKILL_STEPS, + reportFile: FEATURE_FLAGS_REPORT_FILE, + getContentBlocks, + run: { + integrationLabel: 'feature-flags', + // No `skillId`: linear.ts skips its pre-install step (see the gate on + // `linear.ts:47`), so the agent must load the menu and install the right + // variant itself. The prompt below tells it how. + customPrompt: () => `Add PostHog feature flags to this project. + +This flow has no pre-installed skill — you install the right one yourself: + +1. Call \`load_skill_menu\` with \`category: "feature-flags"\`. The menu is + the source of truth: one variant per language/framework. + +2. Scan the project manifest (\`package.json\`, \`pyproject.toml\`, + \`requirements.txt\`, \`Gemfile\`, \`go.mod\`, ...) for the project's + language/framework and pick the variant that matches it. Genuinely + ambiguous → \`wizard_ask\` with a multi-choice picker. + +3. Call \`install_skill\` with the picked variant id. Then follow that + skill's \`SKILL.md\` and references end-to-end. Wire up a real feature + flag around a small, meaningful piece of existing functionality so the + user can see it working — don't just install the SDK. If PostHog is not + integrated yet, install and initialize it first as the skill instructs — + do not abort. + +Make only additive changes — do not touch existing PostHog init, identify +calls, event capture, or dashboards. Those belong to other skills. The +final report is written to ./${FEATURE_FLAGS_REPORT_FILE}.`, + successMessage: `Feature flags configured! View the report at ./${FEATURE_FLAGS_REPORT_FILE}`, + reportFile: FEATURE_FLAGS_REPORT_FILE, + docsUrl: 'https://posthog.com/docs/feature-flags', + spinnerMessage: 'Setting up feature flags...', + estimatedDurationMinutes: 5, + }, +}; diff --git a/src/lib/programs/program-registry.ts b/src/lib/programs/program-registry.ts index 2633c295e..db5541820 100644 --- a/src/lib/programs/program-registry.ts +++ b/src/lib/programs/program-registry.ts @@ -34,6 +34,7 @@ import { replayVisionConfig } from './replay-vision/index.js'; import { aiObservabilityConfig } from './ai-observability/index.js'; import { metricsConfig } from './metrics/index.js'; import { slackConnectConfig } from './slack/index.js'; +import { featureFlagsConfig } from './feature-flags/index.js'; // Generic skill program — runs an arbitrary context-mill skill chosen at // dispatch time (session.skillId) rather than a registered named program. @@ -85,6 +86,7 @@ export const PROGRAM_REGISTRY = [ aiObservabilityConfig, metricsConfig, slackConnectConfig, + featureFlagsConfig, ] as const satisfies readonly ProgramConfig[]; /** @@ -112,6 +114,7 @@ export const Program = { AiObservability: aiObservabilityConfig.id, Metrics: metricsConfig.id, SlackConnect: slackConnectConfig.id, + FeatureFlags: featureFlagsConfig.id, } as const; /** Compile-time union of every registered program id. */