Skip to content
Closed
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
2 changes: 2 additions & 0 deletions bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -91,6 +92,7 @@ Wizard.use(basicIntegrationCommand)
.use(replayVisionCommand)
.use(aiObservabilityCommand)
.use(metricsCommand)
.use(featureFlagsCommand)
.use(cliCommand)
.use(auditCommand)
.use(doctorCommand)
Expand Down
14 changes: 14 additions & 0 deletions src/commands/feature-flags.ts
Original file line number Diff line number Diff line change
@@ -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);
14 changes: 13 additions & 1 deletion src/lib/agent/runner/__tests__/switchboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: {} },
Expand Down Expand Up @@ -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'
? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions src/lib/agent/runner/switchboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ export const PROGRAM_BINDINGS: Partial<Record<ProgramId, ProgramBinding>> = {
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 ────────────────────────────────────────────────────
Expand Down
56 changes: 56 additions & 0 deletions src/lib/programs/feature-flags/index.ts
Original file line number Diff line number Diff line change
@@ -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,
},
};
3 changes: 3 additions & 0 deletions src/lib/programs/program-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -85,6 +86,7 @@ export const PROGRAM_REGISTRY = [
aiObservabilityConfig,
metricsConfig,
slackConnectConfig,
featureFlagsConfig,
] as const satisfies readonly ProgramConfig[];

/**
Expand Down Expand Up @@ -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. */
Expand Down