From be0e7aa140caf7aaa06095b6b4c88bf69ef16158 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Wed, 27 May 2026 11:33:41 +0200 Subject: [PATCH 1/2] feat(migrate-eppo): add Eppo to Confidence migration kit Adds a /migrate-eppo slash command and skill that mirror the existing PostHog migration: two-phase flow (flag definitions then code), opt-in gating per flag, progressive plan files, and the same execute sequence with positive- and negative-case resolve verification. Eppo-specific adjustments: - Uses Eppo's REST admin API via curl since no Claude MCP exists yet; requires an EPPO_API_KEY and never persists it to the plan file. - Asks the user to pick a source environment before scanning, because Eppo flag state is per-environment. - Maps Eppo's subjectKey to a single Confidence entity field; rewrites rules that target the special id attribute to use that field. - Emits one Confidence targeting rule per Eppo allocation in waterfall order, with trafficExposure -> rolloutPercentage and variation weights -> variant splits inside a single rule. - Constrains MATCHES to ^prefix.* / .*suffix$ and blocks SemVer comparisons, with execute refusing to proceed on unresolved blocks. - Flags disabled in the source environment are migrated at 0% rollout so they cannot accidentally activate during migration. Co-authored-by: Cursor --- CLAUDE.md | 2 + README.md | 5 +- commands/migrate-eppo.md | 9 + skills/migrate-eppo/SKILL.md | 1292 ++++++++++++++++++++++++++++++++++ 4 files changed, 1307 insertions(+), 1 deletion(-) create mode 100644 commands/migrate-eppo.md create mode 100644 skills/migrate-eppo/SKILL.md diff --git a/CLAUDE.md b/CLAUDE.md index c2e9a1b..ba99ce4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,10 +5,12 @@ This plugin integrates Confidence with Claude Code, providing tools for feature ## Commands - `/confidence:migrate-posthog >` — Migrate feature flags from PostHog to Confidence SDK +- `/confidence:migrate-eppo >` — Migrate feature flags from Eppo to Confidence SDK ## Skills - **migrate-posthog** — Auto-triggers when the user asks to migrate PostHog flags or transform SDK code to Confidence +- **migrate-eppo** — Auto-triggers when the user asks to migrate Eppo flags or transform SDK code to Confidence ## MCP Servers diff --git a/README.md b/README.md index 3736891..d8e0b07 100644 --- a/README.md +++ b/README.md @@ -42,11 +42,12 @@ claude --plugin-dir ./confidence-ai-plugins This plugin provides access to Confidence tools across these categories: - **Feature flags** - Create, list, update, archive, and resolve feature flags -- **Migration** - Migrate feature flags from PostHog to Confidence +- **Migration** - Migrate feature flags from PostHog or Eppo to Confidence ## Slash Commands - `/confidence:migrate-posthog` - Migrate feature flags from PostHog to Confidence SDK +- `/confidence:migrate-eppo` - Migrate feature flags from Eppo to Confidence SDK ## Example Usage @@ -55,6 +56,8 @@ This plugin provides access to Confidence tools across these categories: > Create a flag called new-checkout with a boolean schema > /migrate-posthog plan flag > /migrate-posthog plan code +> /migrate-eppo plan flag +> /migrate-eppo plan code ``` ## MCP Servers diff --git a/commands/migrate-eppo.md b/commands/migrate-eppo.md new file mode 100644 index 0000000..9bdc8dc --- /dev/null +++ b/commands/migrate-eppo.md @@ -0,0 +1,9 @@ +--- +name: migrate-eppo +description: Migrate feature flags from Eppo to Confidence +argument-hint: [plan flag | plan code | execute ] +--- + +All migration instructions are maintained in `skills/migrate-eppo/SKILL.md` to prevent divergence. + +**Before doing anything else**, use the Read tool to read `skills/migrate-eppo/SKILL.md` and follow those instructions to handle this command. diff --git a/skills/migrate-eppo/SKILL.md b/skills/migrate-eppo/SKILL.md new file mode 100644 index 0000000..8cb10b2 --- /dev/null +++ b/skills/migrate-eppo/SKILL.md @@ -0,0 +1,1292 @@ +--- +description: Migrate feature flags from Eppo to Confidence SDK. Use when the user says /migrate-eppo, asks to migrate Eppo flags, or transform SDK code to Confidence. +--- + +# Eppo to Confidence Migration + +REST-driven, self-sufficient migration from Eppo to Confidence. + +## Migration Flow + +The migration happens in two phases: **flags first, then code**. + +``` +Phase 1: Flag Definitions + plan flags → Scan Eppo, choose client & entity, generate plan + execute → Create each flag in Confidence with targeting rules + +Phase 2: Code Transformation + plan code → Scan codebase, fetch SDK guide, generate transform rules + execute → Transform code flag by flag, each flag = one PR +``` + +**Why flags first?** The flags need to exist in Confidence before the +code can resolve them. Once flags are live in Confidence, you migrate +the code that evaluates them — one flag at a time, one PR at a time. + +**Each code PR is scoped to a single flag.** This keeps PRs small, +reviewable, and independently shippable. If one flag's migration has +issues, it doesn't block the others. + +## Commands + +| Command | Description | +|---------|-------------| +| `/migrate-eppo plan flags` | Phase 1: plan flag definitions migration | +| `/migrate-eppo plan code` | Phase 2: plan code transformation | +| `/migrate-eppo execute ` | Execute a plan interactively | + +--- + +## Migration Overview (MUST display at start of `plan flags` or `plan code`) + +**Every time** the user runs `plan flags` or `plan code`, display this +overview FIRST — before doing any work. This orients the user on where +they are in the full migration journey. + +``` +═══════════════════════════════════════════════════════════════ + Eppo → Confidence Migration +═══════════════════════════════════════════════════════════════ + + The migration happens in two phases: flags first, then code. + + ┌─────────────────────────────────────────────────────────┐ + │ PHASE 1 — Flag Definitions │ + │ │ + │ Move all flags from Eppo to Confidence with their │ + │ allocations, targeting rules, and variation splits. │ + │ │ + │ Steps: │ + │ 1. Pick Eppo environment & scan all flags │ + │ 2. Choose a Confidence client (your app) │ + │ 3. Map subjectKey to a Confidence entity field │ + │ 4. Generate migration plan with targeting rules │ + │ 5. Execute: create each flag in Confidence │ + │ │ + │ Result: All flags live in Confidence, ready to resolve│ + ├─────────────────────────────────────────────────────────┤ + │ PHASE 2 — Code Transformation │ + │ │ + │ Once flags exist in Confidence, migrate the code that │ + │ evaluates them. Each flag = one PR. │ + │ │ + │ Steps: │ + │ 1. Detect language & framework │ + │ 2. Fetch Confidence SDK guide │ + │ 3. Scan codebase for Eppo usage │ + │ 4. Generate transform rules (Eppo → Confidence) │ + │ 5. Generate plan grouped by flag │ + │ 6. Execute: transform code flag by flag, one PR each│ + │ │ + │ Result: Code uses Confidence SDK, Eppo removed │ + └─────────────────────────────────────────────────────────┘ + + Why flags first? + Flags must exist in Confidence before code can resolve them. + + Why one PR per flag? + Keeps changes small, reviewable, and independently shippable. + If one flag's migration has issues, it doesn't block the others. + +═══════════════════════════════════════════════════════════════ +``` + +After displaying the overview, indicate which phase the user is about +to enter: + +- For `plan flags`: "Starting **Phase 1** — Flag Definitions" +- For `plan code`: "Starting **Phase 2** — Code Transformation. + Make sure Phase 1 (flag definitions) is complete first — the flags + need to exist in Confidence before the code can resolve them." + +Then proceed with the normal workflow for that phase. + +--- + +## SDK Preference + +**ALWAYS prefer OpenFeature with local resolve.** + +| Priority | Approach | When to use | +|----------|----------|-------------| +| 1st | Local resolve | Default for all new integrations | +| 2nd | Remote resolve | Only if local resolve not supported for platform | +| Avoid | Direct SDK | Being phased out | + +--- + +## Plan Philosophy + +**Plans must be self-sufficient and agent-agnostic.** + +| Principle | Meaning | +|-----------|---------| +| **Self-sufficient** | Plan contains ALL information needed — no "query the API for X" | +| **Agent-agnostic** | Any agent with the prerequisites can execute without prior context | +| **Language-agnostic** | Detect framework, fetch SDK guide from MCP dynamically | + +--- + +## Prerequisites + +Before starting any workflow, check that required prerequisites are +available. If any are missing, install or configure them before +proceeding. + +### Eppo API access + +Eppo does not currently publish a Claude MCP server, so the migration +talks to Eppo's REST API directly using `curl` from the Bash tool. + +**Required:** + +1. An **Eppo API key** (NOT an SDK key). Generated in the Eppo + dashboard under **Admin > API Keys**. The key needs read access to + feature flags. +2. The Eppo API base URL — for most accounts this is + `https://eppo.cloud/api/v1`. Self-hosted or region-specific + deployments may use a different base — ask the user to confirm. + +**Authentication header:** `X-Eppo-Token: ` + +**ASK the user (only if not already provided):** + +> To read your Eppo flags, I need an Eppo API key (Admin > API Keys +> in the Eppo dashboard — make sure it has read access to feature +> flags). +> +> Please paste it here, or set it in your shell as `EPPO_API_KEY` +> before continuing. +> +> What's your Eppo API base URL? Default is `https://eppo.cloud/api/v1`. + +**Storing the key:** Once provided, store the key for the session in +the environment variable `EPPO_API_KEY` (export it in the Bash session +the agent uses) and reference it via `$EPPO_API_KEY` in every `curl` +call — never hardcode the key into the plan file, the conversation +output, or any committed file. If the user pastes a key inline, scrub +it from the plan file and only keep a placeholder like ``. + +**Smoke test before scanning:** + +```bash +curl -sS -H "X-Eppo-Token: $EPPO_API_KEY" \ + "https://eppo.cloud/api/v1/feature-flags?page=1&per_page=1" \ + | head -c 200 +``` + +If this returns a `401`/`403` or HTML, stop and surface the error to +the user — do not start scanning. + +### Confidence MCP + +Test: `mcp__confidence__listClients` + +If not available, install it: +``` +claude mcp add confidence --transport http --url https://mcp.confidence.dev/mcp/flags +``` + +The user will be prompted to authenticate via OAuth in their browser. + +### Confidence Docs MCP (for `plan code` only) + +Test: `mcp__confidence-docs__searchDocumentation` + +If not available, install it: +``` +claude mcp add confidence-docs --transport http --url https://mcp.confidence.dev/mcp/docs +``` + +The user will be prompted to authenticate via OAuth in their browser. + +--- + +## User-Facing Communication Rules + +**NEVER expose internal technical details to the user.** The user should see +human-readable descriptions of what's happening, not internal implementation +details like targeting payload formats, rule types, or operator names. + +- Do NOT say "creating plan based on eqRule / rangeRule / setRule" etc. +- Do NOT show raw targeting payloads or JSON structures in conversation +- Do NOT echo the user's Eppo API key back into the conversation or plan +- DO say things like: "Creating flag with rule: country is US or UK AND appVersion >= 28.5.0" +- DO describe rules in plain English: "age between 18 and 65", "plan is not free" +- The plan FILE may contain MCP command payloads (for machine execution), + but conversation output must be human-friendly + +**Step Tracker:** Display a visual step tracker at every phase transition. +The tracker shows all phases, marks completed ones, highlights the current +one, and shows remaining ones. Update and re-display it each time you move +to a new phase. + +### Plan Flags Step Tracker + +Display this at the START and after EACH step completes (updating status): + +``` +───── Plan Flags ────────────────────────────────────────── + [1] Scan Eppo ○ pending + [2] Choose client ○ pending + [3] Map subject ○ pending + [4] Generate plan ○ pending +──────────────────────────────────────────────────────────── +``` + +Status markers: +- `○ pending` — not started yet +- `◉ in progress` — currently running +- `⏸ awaiting user` — blocked on user input (e.g. picking a client, environment, or entity) +- `✓ done` — completed (add brief user-facing result) +- `⊘ skipped` — skipped by user + +Use `⏸ awaiting user` whenever the workflow has asked a question and is +waiting for an explicit reply. This makes "I'm blocked on you" visible +to both agent and user, and prevents the agent from drifting into +auto-progression while a question is open. + +**IMPORTANT:** Never expose internal/technical details in the tracker. +No pagination info, no API page counts, no internal field names. +Show only what matters to the user. + +Example after Step 1 completes: +``` +───── Plan Flags ────────────────────────────────────────── + [1] Scan Eppo ✓ 12 flags found (environment: Production) + [2] Choose client ◉ in progress + [3] Map subject ○ pending + [4] Generate plan ○ pending +──────────────────────────────────────────────────────────── +``` + +### Execute Step Tracker + +Display this at the START and update after EACH flag: + +``` +───── Execute Migration ─────────────────────────────────── + Client: test | Subject: user_id | Flags: 12 + Progress: [░░░░░░░░░░░░░░░░░░░░] 0/12 +──────────────────────────────────────────────────────────── +``` + +Update the progress bar as flags are processed. Use `█` for completed +and `░` for remaining. The bar should be 20 characters wide. + +Examples at various stages: +``` + Progress: [██████░░░░░░░░░░░░░░] 4/12 (1 skipped) + Current: checkout-redesign +``` + +``` + Progress: [████████████████████] 12/12 done + Result: 11 migrated, 1 skipped +``` + +After each flag completes, show: +``` + ✓ checkout-redesign — MATCH (treatment) +``` + +After a skip: +``` + ⊘ legacy-onboarding — skipped +``` + +### Final Summary (Execute) + +At the end of execution, show a complete summary: + +``` +───── Migration Complete ────────────────────────────────── + Progress: [████████████████████] 12/12 done + Migrated: 11 | Skipped: 1 | Failed: 0 + + ✓ checkout-redesign 50/50 user_id + ✓ pricing-experiment 34/33/33 user_id + ⊘ legacy-onboarding — skipped + ✓ internal-tools-gate 100% user_id + ... +──────────────────────────────────────────────────────────── +``` + +--- + +## Confidence Naming Rules + +- **Flag names:** lowercase letters, digits, and hyphens only (`[a-z0-9-]`). + Eppo flag keys often already follow this convention; if not, normalize + (e.g. `Checkout_Redesign` → `checkout-redesign`) and record the mapping + in the plan so the code phase can find the right replacement. +- **Entity references:** Confidence entity names do NOT support underscores. + The entity reference (e.g. `entities/company`) is separate from the context + field name (e.g. `company_id`). When creating entity fields with + `addContextField`, always provide an explicit `entityReference` with a + clean name (no underscores). If omitted, the tool auto-generates one from + the field name which will fail. + + | Field name | Entity reference | Works? | + |------------|-----------------|--------| + | `user_id` | `entities/user` | Yes | + | `company_id` | `entities/company` | Yes | + | `visitor_id` | `entities/visitor` | Yes | + | `company_id` | *(omitted — auto: `entities/company_id`)* | **No** | + +--- + +## Eppo REST Reference (agent-internal) + +The migration uses these endpoints. All require `-H "X-Eppo-Token: $EPPO_API_KEY"`. +Base URL defaults to `https://eppo.cloud/api/v1`. + +| Purpose | Endpoint | +|---------|----------| +| List environments | `GET /environments` | +| List feature flags | `GET /feature-flags?page=&per_page=` | +| Get a single flag (full definition: variations, allocations, rules) | `GET /feature-flags/{id}` | +| Get environment-specific flag state (enabled + per-env allocations) | `GET /feature-flags/{id}/environments/{environmentId}` | + +The flag object includes: +- `key` — used in code (subject of `get_*_assignment` calls) +- `name`, `description` +- `variationType` — `STRING` / `BOOLEAN` / `NUMERIC` / `INTEGER` / `JSON` +- `variations[]` — each has `key`, `name`, `value` +- `allocations[]` — ordered waterfall (top wins). Each allocation has: + - `name`, `allocationType` (`FEATURE_GATE` / `EXPERIMENT` / `AUDIENCE`) + - `targetingRules[]` — each rule is `{ conditions: [{ attribute, operator, value }] }` + - `variationWeightsByKey` or `variationWeights[]` — split among variations + - `trafficExposure` (0–1) — fraction of matched subjects that enter the allocation +- `environments[]` — per-environment state (enabled flag, env-specific allocations) + +**Always paginate** until the response returns fewer items than `per_page` or +an empty page. Eppo's API uses page-based pagination, not cursors. + +--- + +## Plan Code: Workflow + +### Resume Check (MUST do first) + +Same as Plan Flag: check for existing `.claude/plans/eppo-code-migration-*.md`. +If found with incomplete `Generation Status`, resume from the last +incomplete step. If complete, ask user if they want to start fresh. +If not found, start fresh. + +The plan file uses the same progressive pattern: created at Step 1, +updated after each step, with a `## Generation Status` section. + +### Step 1: Detect Language & Framework + +``` +Grep: pattern="eppo|Eppo|EppoClient|get_.*_assignment|getStringAssignment|getBooleanAssignment" -> Find Eppo usage +Glob: pattern="package.json" or "build.gradle" or "Cargo.toml" or "pyproject.toml" etc +Read: dependency file -> Determine language/framework +``` + +The Eppo package names to look for: +- JS/TS: `@eppo/js-client-sdk`, `@eppo/node-server-sdk`, `@eppo/react-native-sdk` +- Python: `eppo-server-sdk` +- Java/Kotlin: `cloud.eppo:eppo-server-sdk` +- Go: `github.com/Eppo-exp/golang-sdk` +- Ruby: `eppo-server-sdk` +- Rust: `eppo_sdk` +- Swift / iOS: `eppo-ios-sdk` +- Android: `cloud.eppo:eppo-android-sdk` +- DotNet: `Eppo.Sdk` + +### Step 2: Fetch SDK Guide from MCP + +**Query confidence-docs MCP based on detected language:** + +``` +mcp__confidence-docs__getCodeSnippetAndSdkIntegrationTips + sdk: "" +``` + +``` +mcp__confidence-docs__searchDocumentation + query: "OpenFeature local resolve " +``` + +``` +mcp__confidence-docs__getFullSource + source: "https://confidence.spotify.com/docs/sdks/server/" +``` + +**CRITICAL:** Include the ACTUAL response in the plan, not a reference to fetch it. + +### Step 3: Scan Codebase for Eppo Usage + +``` +Grep: pattern="" -> Find all imports +Grep: pattern="get_(string|boolean|numeric|integer|json)_assignment|getStringAssignment|getBooleanAssignment|getNumericAssignment|getIntegerAssignment|getJSONAssignment" -> Find evaluations +``` + +Group files by **flag key** they reference. The flag key is the first +argument to every Eppo `get_*_assignment` call. + +For each evaluation site, record: +- Flag key +- Return type (inferred from which `get_*_assignment` variant is used) +- The `subjectKey` argument (so the transform can map it to `targetingKey`) +- The `subjectAttributes` argument (so the transform can carry them into + the evaluation context) +- The `defaultValue` argument (carried over to the Confidence call) + +### Step 4: Generate Transform Rules + +Based on SDK guide from MCP: +- Extract install commands +- Extract initialization code +- Extract flag evaluation API +- Generate find/replace rules matching Eppo → Confidence patterns + +**Typed assignment mapping (Eppo → OpenFeature / Confidence):** + +| Eppo call | OpenFeature call | +|-----------|------------------| +| `client.get_string_assignment(k, sk, attrs, default)` | `client.getStringValue(k, default, { targetingKey: sk, ...attrs })` | +| `client.get_boolean_assignment(k, sk, attrs, default)` | `client.getBooleanValue(k, default, { targetingKey: sk, ...attrs })` | +| `client.get_numeric_assignment(k, sk, attrs, default)` | `client.getNumberValue(k, default, { targetingKey: sk, ...attrs })` | +| `client.get_integer_assignment(k, sk, attrs, default)` | `client.getNumberValue(k, default, { targetingKey: sk, ...attrs })` | +| `client.get_json_assignment(k, sk, attrs, default)` | `client.getObjectValue(k, default, { targetingKey: sk, ...attrs })` | + +(Adjust method casing per language — `getStringValue` in JS/TS, `get_string_value` +in Python, `getValue` in Kotlin, etc. — based on the MCP-fetched +SDK guide.) + +### Step 5: Generate Plan + +Save to `.claude/plans/eppo-code-migration-.md` + +--- + +## Plan Code: Template + +```markdown +# Eppo to Confidence Code Migration Plan + +**Created:** +**Scope:** Code transformation only +**Language:** +**Framework:** + +--- + +## 1. SDK Setup + +### Install + + + +### API Reference (from MCP: confidence-docs) + + + +### Create Confidence Wrapper + +**File:** + +**Must match Eppo API surface:** + +| Method | Signature | +|--------|-----------| + + +--- + +## 2. Transform Rules + +### Source Files + +| Find | Replace | +|------|---------| +| | | +| `client.get_string_assignment(k, sk, attrs, default)` | `client.getStringValue(k, default, { targetingKey: sk, ...attrs })` | +| | | + +### Test Files + +| Find | Replace | +|------|---------| +| | | + +--- + +## 3. Files to Transform + + + +--- + +## 4. Progress + +| # | Item | Status | +|---|------|--------| +| 0 | SDK Setup | :white_circle: | +``` + +--- + +## Plan Flag: Workflow + +### Resume Check (MUST do first) + +Before starting, check for an existing in-progress plan: + +``` +Glob: .claude/plans/eppo-flag-migration-*.md +``` + +If a plan file exists, read its `## Generation Status` section: +- If status is `complete` → tell user a plan already exists, ask if + they want to start fresh or use the existing one +- If status is NOT `complete` → **resume from the last incomplete step** + Tell the user: "Found an in-progress plan. Resuming from step ." +- If no plan file exists → start fresh + +### Progressive Plan File + +The plan file is created at the START (Step 1) and updated after EACH +step. This means if the session closes, the file has partial progress +that can be resumed. + +**File path:** `.claude/plans/eppo-flag-migration-.md` + +The plan file MUST include a `## Generation Status` section at the top +(right after the title) that tracks which steps are done: + +```markdown +## Generation Status + +| Step | Status | Result | +|------|--------|--------| +| 1. Scan Eppo | ✓ complete | 12 flags | +| 2. Choose client | ✓ complete | test | +| 3. Map subject | ○ not started | | +| 4. Generate rules | ○ not started | | +``` + +Status values: `✓ complete`, `◉ in progress`, `○ not started` + +**After each step completes**, update the status table AND write that +step's data to the plan file. Do NOT wait until the end to write. + +### Step 1: Scan Eppo Flags + +**Step 1a — pick the source environment.** + +Eppo's flag state (enabled, per-environment allocations) is scoped to +an environment. The user MUST choose which environment to migrate from. + +```bash +curl -sS -H "X-Eppo-Token: $EPPO_API_KEY" \ + "https://eppo.cloud/api/v1/environments" +``` + +Show the user the list and ASK: + +> Eppo configures flags per environment (Production, Staging, etc.). +> Which environment should I migrate flag definitions from? +> +> Your environments: +> 1. +> 2. +> ... +> +> Pick a number. (Production is usually the right answer for a real +> rollout migration.) + +Set the step to `⏸ awaiting user` and wait for an explicit pick. + +**Step 1b — list all flags. CRITICAL: paginate until exhausted.** + +``` +page = 1 +LOOP: + response = curl GET /feature-flags?page=&per_page=50 + process response items + if response items < 50 OR response is empty → STOP + page += 1 → continue LOOP +``` + +```bash +curl -sS -H "X-Eppo-Token: $EPPO_API_KEY" \ + "https://eppo.cloud/api/v1/feature-flags?page=1&per_page=50" +``` + +**Step 1c — fetch each flag's full definition (in batches of 5).** + +```bash +curl -sS -H "X-Eppo-Token: $EPPO_API_KEY" \ + "https://eppo.cloud/api/v1/feature-flags/" +``` + +And the environment-specific state for the chosen environment: + +```bash +curl -sS -H "X-Eppo-Token: $EPPO_API_KEY" \ + "https://eppo.cloud/api/v1/feature-flags//environments/" +``` + +**After each batch of 5**, write the flag data to the plan file — +append the flag sections to Section 4. This way if the session closes +mid-scan, the flags fetched so far are saved. + +Skip flags that are **archived** in Eppo unless the user opts in (ask +once up-front: "Include archived flags too? Default: no"). + +Extract from each flag: + +- `key` and `name` +- `description` (if Eppo provides one, include it; otherwise leave blank) +- `variationType` and the list of `variations` (key + value) +- For the chosen environment: + - `enabled` state — flags that are disabled in the chosen environment + still migrate, but with rollout 0% so they don't activate + accidentally; surface this clearly in the plan + - Ordered list of `allocations` with: + - `allocationType` (Feature Gate, Experiment, or Audience) + - `trafficExposure` (0–1) + - `targetingRules[]` (`conditions: [{ attribute, operator, value }]`) + - `variationWeightsByKey` — the split among variations +- The default variation (what subjects see when no allocation matches) + +**Determine the randomization unit:** Eppo always uses `subjectKey`. +Unlike PostHog there's no per-group bucketing concept built into the +flag — group-level experiments are handled by passing a `companyId` as +the `subjectKey`. For the migration, treat every flag as per-subject; +the user will pick which Confidence entity field represents that subject +in Step 3. + +**After scan completes:** Update Generation Status step 1 to `✓ complete`. + +### Step 2: Select Confidence Client + +``` +mcp__confidence__listClients +``` + +**EDUCATE then ASK the user:** + +> **What is a client?** +> A client represents the application that resolves flags — your website, +> backend service, or mobile app. Each client has its own secret for +> authentication and can be scoped to environments (dev, staging, prod). +> Flags are associated with one or more clients, so Confidence knows which +> application should receive which flags. +> +> Think of it like: "Where will these flags be evaluated?" +> +> Your existing clients: +> 1. +> 2. +> ... +> N. Create a new client +> +> Which client should I use as the default for all flags? +> You can always rearrange them later in the Confidence UI. + +**Wait for an explicit pick.** Set the step to `⏸ awaiting user` and +stop. A re-run of `/migrate-eppo`, an empty message, or any reply +that is not a number from the list / `new ` is **not** consent — +NEVER infer the recommendation from silence. If the reply is ambiguous, +re-ask, listing the choices again. + +- If user picks existing -> use it +- If user wants new -> ASK for name -> `mcp__confidence__createClient` + +**After client selected:** Write Section 1 (Default Client) to plan +file and update Generation Status step 2 to `✓ complete`. + +### Step 3: Map Subject Key to a Confidence Entity Field + +``` +mcp__confidence__getContextSchema clientName: "" +``` + +Show the user entity fields (fields marked as entity in the schema). + +This step maps Eppo's `subjectKey` to a Confidence entity field. + +**EDUCATE then ASK:** + +> **What is a randomization unit (entity)?** +> An entity is the "thing" that gets randomly assigned to a variant — +> usually a user. The entity field (like `user_id` or `visitor_id`) is +> the identifier Confidence uses to ensure **consistent assignment**: the +> same user always sees the same variant. +> +> In Confidence, it maps to the `targetingKey` in the evaluation context. +> +> In Eppo, every assignment call passes a `subjectKey`. In your code I +> see calls like `get_string_assignment(flagKey, , ...)` — +> the second argument. Which Confidence entity field is the same thing? +> +> Common choices: +> - **user_id** — if your flags target authenticated users +> - **visitor_id** — if targeting anonymous visitors (auto-generated by +> Confidence client SDKs) +> - **company_id** — if your Eppo subject was a company / org / tenant +> +> Your client's existing entity fields: +> 1. +> 2. +> ... +> N. Create a new field +> +> Which Confidence field represents the same identifier as `subjectKey`? + +**Wait for an explicit pick.** Same rule as Step 2 — set the step to +`⏸ awaiting user` and stop. Silence, a re-run, or any non-listed reply +is **not** consent. Re-ask if the reply is ambiguous. + +- If user picks existing -> use it as `targetingKey` +- If user wants new -> ASK for name + type -> `mcp__confidence__addContextField` + (always provide an explicit `entityReference` — see Confidence Naming Rules) + +**Eppo subject targeting (`id` attribute).** Eppo lets rules target the +subject directly via the special attribute `id`. When a rule references +`id`, map it to the chosen entity field's name in Confidence (the +context key for `targetingKey`). Record this substitution in Section 2 +of the plan. + +**Step 3 only creates the entity field** (the subject's entity). +Attribute fields used in targeting rules (`country`, `plan`, `device`, +`appVersion`, etc.) MUST NOT be created here. Record them in Section 3 +"Need to Create" and let `execute` create them — that way, if the user +later skips a flag, no orphan schema fields are left in Confidence. + +**After entity mapped:** Write Section 2 (Subject Mapping) to plan +file, reconcile and write Section 3 (Context Schema), and update +Generation Status step 3 to `✓ complete`. + +### Step 4: Generate MCP Commands + +**Confirmation gate (MUST pass before generating).** Before writing +Section 4, summarize chosen client + entity + Eppo environment in chat +and ask: + +> Plan will assume client ``, Eppo source environment +> ``, and randomization entity ``. All flags will be +> defaulted to `[ ] Migrate [ ] Skip` (neither pre-checked) — you'll +> opt each one in during review. +> Confirm or change? + +Set the step to `⏸ awaiting user` and stop. Only proceed on an +explicit `yes` / `confirm` / equivalent. A re-run or ambiguous reply +is **not** confirmation. + +For each flag in Section 4, generate the MCP command payloads +(createFlag, addFlagToClient, addTargetingRule, resolveFlag) using the +Operator Mapping Reference (below). Write them into each flag's section. + +**Allocation → targeting-rule order.** Eppo allocations form a +waterfall — the first matching allocation wins. Confidence evaluates +targeting rules in declared order, so emit one `addTargetingRule` +call per Eppo allocation, in the same order. Rules added later sit +below earlier rules. + +**After all commands generated:** Update Generation Status step 4 to +`✓ complete` and set the overall status to `complete`. Write the +Progress table (Section 5). + +**Tell the user:** +> Plan generated! Review it at `.claude/plans/eppo-flag-migration-.md` +> +> Migration is **opt-in**: every flag starts with both checkboxes +> empty. Tick `[x] Migrate` or `[x] Skip` for each flag — `execute` +> will refuse any flag with neither box set. +> When you're ready, run: `/migrate-eppo execute ` + +--- + +## Operator Mapping Reference (agent-internal, do NOT show to user) + +This is how Eppo operators map to Confidence targeting payloads. +Use this when generating `addTargetingRule` payloads in the plan file. + +**CRITICAL: Confidence Targeting Payload Format** + +The payload uses a `criteria` + `expression` pattern. Criteria are named +references (`ref-0`, `ref-1`, ...) that define individual conditions. +The `expression` combines them with boolean logic (`and`, `or`, `not`, `ref`). + +```json +{ + "criteria": { + "ref-0": { + "attribute": { + "attributeName": "", + "": { ... } + } + } + }, + "expression": { "ref": "ref-0" } +} +``` + +**DO NOT use nested rule objects like `{"or": {"operands": [{"eqRule": ...}]}}` +at the top level.** That format is silently parsed as empty targeting +(matching ALL contexts) due to `ignoringUnknownFields()` in the proto parser. + +### Criterion Rules + +| Confidence Rule | Form | +|---|---| +| String eq | `"eqRule": { "value": { "stringValue": "X" } }` | +| Number eq | `"eqRule": { "value": { "numberValue": N } }` | +| Bool eq | `"eqRule": { "value": { "boolValue": true } }` | +| `>=` | `"rangeRule": { "startInclusive": { "numberValue": N } }` | +| `>` | `"rangeRule": { "startExclusive": { "numberValue": N } }` | +| `<` | `"rangeRule": { "endExclusive": { "numberValue": N } }` | +| `<=` | `"rangeRule": { "endInclusive": { "numberValue": N } }` | +| starts with | `"startsWithRule": { "value": "prefix" }` | +| ends with | `"endsWithRule": { "value": "suffix" }` | + +### Expression Combinators + +| Pattern | Expression | +|---------|-----------| +| Single condition | `{ "ref": "ref-0" }` | +| AND | `{ "and": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } }` | +| OR | `{ "or": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } }` | +| NOT | `{ "not": { "ref": "ref-0" } }` | +| NOT IN (list) | `{ "and": { "operands": [{ "not": { "ref": "ref-0" } }, { "not": { "ref": "ref-1" } }] } }` | + +### Eppo Operator Mapping + +Within a single Eppo rule, all `conditions` are ANDed. Across multiple +rules in the same allocation, conditions are ORed (any rule satisfying +means the allocation matches). + +| Eppo operator (typical JSON values: `GT`, `LT`, `GTE`, `LTE`, `MATCHES`, `ONE_OF`, `NOT_ONE_OF`) | Confidence Payload Strategy | +|---|---| +| `GT` / `>` | One criterion with `rangeRule.startExclusive`, expression: `ref` | +| `GTE` / `>=` | One criterion with `rangeRule.startInclusive`, expression: `ref` | +| `LT` / `<` | One criterion with `rangeRule.endExclusive`, expression: `ref` | +| `LTE` / `<=` | One criterion with `rangeRule.endInclusive`, expression: `ref` | +| `ONE_OF ["A"]` (single value) | One criterion with `eqRule`, expression: `ref` | +| `ONE_OF ["A","B",...]` | One criterion per value with `eqRule`, expression: `or` of `ref`s | +| `NOT_ONE_OF ["A"]` (single value) | One criterion with `eqRule`, expression: `not` wrapping `ref` | +| `NOT_ONE_OF ["A","B",...]` | One criterion per value with `eqRule`, expression: `and` of `not`-wrapped `ref`s | +| `MATCHES "^prefix.*"` | One criterion with `startsWithRule { value: "prefix" }` | +| `MATCHES ".*suffix$"` | One criterion with `endsWithRule { value: "suffix" }` | + +**Blocked (manual review):** +- `MATCHES` regex that is not a simple prefix/suffix anchor — Confidence + has no general regex rule. Surface the flag in Section 4 with an + explicit `BLOCKED` marker and a brief explanation; the user must + either rewrite the rule using set membership / starts-with / ends-with + or migrate manually. +- SemVer comparisons — Eppo can compare SemVer strings numerically. + Confidence's `rangeRule` is purely numeric. If the attribute type is + SemVer, mark the rule `BLOCKED` and ask the user whether to convert + the comparison to a numeric `appVersionMajor` / `appVersionMinor` + context field, or migrate manually. +- `id` (subject-key) ONE_OF lists of more than ~50 values — Eppo caps + these at 50 but Confidence is fine with larger sets; not blocked, + just noted. + +### AND / OR Combinations + +**Within a rule:** `conditions[]` are ANDed. Create one criterion per +condition and combine them with an `and` expression. + +**Across rules in one allocation:** any rule matching is enough. Create +criteria for each rule, combine each rule's sub-expression, then OR the +rule expressions together at the top level. + +**Across allocations:** these are NOT ORed inside one Confidence rule. +Each Eppo allocation becomes a **separate Confidence targeting rule**, +in the same waterfall order. + +### Complete Examples + +**Single equality (country = "US"):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "US" } } } } + }, + "expression": { "ref": "ref-0" } +} +``` + +**ONE_OF (country IN [US, UK]):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "US" } } } }, + "ref-1": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "UK" } } } } + }, + "expression": { "or": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } } +} +``` + +**NOT_ONE_OF (country NOT IN [DE, FR]):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "DE" } } } }, + "ref-1": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "FR" } } } } + }, + "expression": { "and": { "operands": [{ "not": { "ref": "ref-0" } }, { "not": { "ref": "ref-1" } }] } } +} +``` + +**AND within a rule (plan = "pro" AND country ONE_OF [US, UK]):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "plan", "eqRule": { "value": { "stringValue": "pro" } } } }, + "ref-1": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "US" } } } }, + "ref-2": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "UK" } } } } + }, + "expression": { "and": { "operands": [{ "ref": "ref-0" }, { "or": { "operands": [{ "ref": "ref-1" }, { "ref": "ref-2" }] } }] } } +} +``` + +**Range (age >= 30):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "age", "rangeRule": { "startInclusive": { "numberValue": 30 } } } } + }, + "expression": { "ref": "ref-0" } +} +``` + +**Subject targeting (`id` ONE_OF ["user-1", "user-2"], mapped to `user_id`):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "user_id", "eqRule": { "value": { "stringValue": "user-1" } } } }, + "ref-1": { "attribute": { "attributeName": "user_id", "eqRule": { "value": { "stringValue": "user-2" } } } } + }, + "expression": { "or": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } } +} +``` + +**Two-allocation waterfall (internal users gate, then 50/50 experiment):** + +This becomes TWO separate `addTargetingRule` calls, in order: +1. Rule 1 — `email endsWith @spotify.com` → assigns `treatment` at 100%. +2. Rule 2 — `country ONE_OF ["US", "CA"]` → assigns `control` 50%, `treatment` 50%. + +### Multivariant A/B Split Handling + +**CRITICAL:** A single Confidence targeting rule CAN assign multiple +variants at different split percentages. Use ONE rule per Eppo +allocation, listing all variants and their shares in that rule. + +**How to map Eppo splits to Confidence rules:** + +For a Feature Gate allocation (all matched subjects get one variation): +- Add ONE rule with one variant assignment at 100%. + +For an Experiment allocation with variation weights (e.g. control 50% / +treatment 50%): +- Add ONE rule with two variant assignments: + control at 50%, treatment at 50%. + +For 3+ variants (e.g. control 34% / A 33% / B 33%): +- Add ONE rule with three variant assignments: + control at 34%, A at 33%, B at 33%. + +**Traffic exposure → rule rollout.** Eppo's `trafficExposure` (0–1) on +an allocation maps to the `rolloutPercentage` of the Confidence rule: +e.g. `trafficExposure: 0.5` → `rolloutPercentage: 50`. Subjects who +match the targeting conditions but fall outside the exposure continue +down the waterfall (next rule) in Confidence too. Variant percentages +within the rule control the split among the subjects who DO enter. + +**Do NOT create separate rules per variant.** One targeting rule = +one set of targeting conditions, with the variant split defined +inside that rule. + +--- + +## Plan Flag: Template + +```markdown +# Eppo to Confidence Flag Migration Plan + +**Created:** +**Scope:** Flag definitions only +**Eppo source environment:** + +--- + +## Generation Status + +| Step | Status | Result | +|------|--------|--------| +| 1. Scan Eppo | ○ not started | | +| 2. Choose client | ○ not started | | +| 3. Map subject | ○ not started | | +| 4. Generate rules | ○ not started | | + +**Overall:** in progress + +--- + +## 1. Default Client + +A client represents the application that resolves flags (e.g. your +website, backend service, or mobile app). Each client authenticates +with its own secret and can be scoped to environments (dev, staging, +prod). Flags are associated with clients so Confidence knows which +application receives which flags. + +**Available Clients:** + +**Selected:** `` + +--- + +## 2. Subject Mapping + +An entity is the "thing" being randomly assigned to a variant — usually +a user. The entity field (like `user_id` or `visitor_id`) is the +identifier Confidence uses for consistent assignment: the same subject +always sees the same variant. + +Eppo's `subjectKey` (the second argument to every `get_*_assignment` +call) is mapped to: **``** + +Any Eppo rules that targeted the special `id` attribute (subject-key +targeting) are rewritten to target ``. + +**Available Entity Fields:** + +--- + +## 3. Context Schema + +The context schema defines what fields Confidence expects in the +evaluation context when resolving flags — things like `country`, +`plan`, or `appVersion` that targeting rules use to decide who gets +what. + +Below is a reconciliation of what Eppo flags need vs what already +exists in the Confidence client's schema. + +### Already in Confidence + +These fields are already defined in the `` client and match +Eppo targeting attributes. No action needed. + +| Field | Type | Entity | Eppo Attribute | +|-------|------|--------|----------------| + + +### Need to Create + +These fields are used in Eppo targeting rules but don't exist yet +in the Confidence client. They will be created during execution using +`addContextField`. + +| Field | Type | Entity | Eppo Attribute | +|-------|------|--------|----------------| + + +### Confidence-only (not in Eppo) + +These fields exist in Confidence but aren't used by any Eppo flag. +Listed for reference — no action needed. + +| Field | Type | Entity | +|-------|------|--------| + + +--- + +## 4. Flags to Migrate + +Below are the flags we're planning to migrate, along with their +allocations described in plain language. + +**Migration is opt-in.** Each flag starts with both checkboxes empty. +Tick `[x] Migrate` for every flag you want to bring across, or +`[x] Skip` to drop it. Flags with neither box ticked will be refused +by `execute` — no implicit defaults. + +During execution, each flag will be created one by one, interactively. + +### Flag: `` + +**Description:** +**Variation type:** +**Variations:** +**Enabled in ``:** +**Allocations (Eppo, in order):** + 1. `` (``) — , exposure %, splits + 2. ... +**Default value (no allocation matches):** +**Confidence entity:** +**Confidence rules:** one targeting rule per allocation (see waterfall in Step 4) +**Action:** [ ] Migrate [ ] Skip + +**MCP Commands:** + + + +--- + +## 5. Progress + +| # | Flag | Status | +|---|------|--------| +| 1 | | :white_circle: | +``` + +--- + +## Execute: How It Works + +**`execute ` walks through the plan interactively, step by step.** + +### For Code Plans + +**Each flag = one PR.** The code migration creates a separate pull +request for each flag, keeping changes small and reviewable. + +``` +1. READ the plan file +2. SDK SETUP (Section 1 of plan) — one-time, before any flag + - Show install command from plan + - ASK: "Install SDK now? [Yes / Skip / I already did]" + - If Yes -> run install command + - Show wrapper file path + API surface from plan + - ASK: "Create the Confidence wrapper now? [Yes / Skip / I already did]" + - If Yes -> create the file using plan's API reference +3. FOR EACH FLAG in the files list: + a. Create a branch: `migrate/-to-confidence` + b. Show flag name + all files using it + c. ASK: "Transform this flag's files? [Yes / Skip / Pause]" + d. If Yes -> apply transform rules from plan to all files for this flag + e. Run lint + typecheck on changed files + f. Commit changes + g. Create PR with title: "feat: migrate from Eppo to Confidence" + h. Show PR link + i. CHECKPOINT: "PR created. [Continue to next flag / Pause]?" + j. Wait for user response +4. COMPLETION + - Show summary: migrated vs skipped + - List all PRs created with links +``` + +### For Flag Plans + +``` +1. READ the plan file + - Client is already in the plan — use it, do NOT re-ask + - Subject entity (randomization unit) is already in the plan + - Eppo source environment is already in the plan + - REFUSE TO PROCEED if any flag has neither `[x] Migrate` nor + `[x] Skip` ticked. List those flags back to the user and ask + them to tick a box for each before re-running execute. Migration + is opt-in — never assume a default. + - REFUSE TO PROCEED if any flag is marked `BLOCKED` and the user + hasn't either resolved the block (rewrote the rule) or ticked + `[x] Skip`. Surface the BLOCKED flags and the reason for each. +2. FOR EACH FLAG marked [x] Migrate: + - Show flag name, description, and allocations in plain English + - If the flag is disabled in the source environment, surface that: + "This flag is OFF in Eppo (). I'll create it in Confidence + but keep the rules at 0% rollout. Continue?" + - ASK: "Create this flag in Confidence? [Yes / Skip / Pause]" + - If Yes -> run the flag setup sequence (see below) + - CHECKPOINT: "Flag done. [Continue / Pause]?" + - Wait for user response +3. COMPLETION + - Show summary: created vs skipped +``` + +**Flag Setup Sequence (MUST complete all steps before resolving):** + +Each flag MUST go through these steps in order. Do NOT call +`resolveFlag` until ALL prior steps succeed. + +``` +STEP 1: createFlag + → Use the variation type from Eppo (STRING/BOOLEAN/NUMERIC/INTEGER/JSON) + as the Confidence schema type. + → Include all variants from Eppo as Confidence variants. + → If flag already exists, check the response for which clients + it's enabled on. + +STEP 2: Ensure flag is active and on the correct client + → If createFlag response does NOT list the target client: + a. Try addFlagToClient + b. If that fails with "Cannot update an archived flag": + → unarchiveFlag first, then retry addFlagToClient + → If createFlag response lists the target client: proceed + +STEP 3: addTargetingRule — ONE per Eppo allocation, in waterfall order + → Add the targeting rules from the plan, in the same order as the + Eppo allocations. Confidence evaluates rules top-down — order is + semantically significant. + → IMPORTANT: targeting rules added while a flag is archived OR + immediately after unarchiving may become inactive. Always complete + steps 1-2 fully (createFlag, unarchive, addFlagToClient) BEFORE + calling addTargetingRule. Do NOT add rules between createFlag and + unarchiveFlag — they will be inactive and you'll have to re-add. + +STEP 4: resolveFlag (verification) + → Only NOW resolve to verify the flag works. + → MUST test BOTH positive AND negative cases: + a. Resolve with a context that SHOULD match the FIRST allocation + → Verify the expected variant is returned + b. Resolve with a context that SHOULD NOT match any allocation + → Verify the default variation / no variant is returned + → If the flag has multiple allocations, also resolve with a context + that misses the first allocation but matches a later one — this + verifies the waterfall order is correct. + → For attribute-based targeting (country, plan, etc.), the resolve + call MUST include those attributes in the evaluation context. + Without them, the targeting conditions cannot be evaluated and + may appear to match when they wouldn't in production. + → If resolve fails with "No active flags found": + something went wrong in steps 1-2 — diagnose, don't skip + → If all rules show "Rule is inactive" / no match: + targeting rules were likely added while flag was archived. + Re-add the targeting rule now that the flag is active. + → Do NOT report a flag as successfully migrated until both + positive and negative resolve tests pass. +``` + +**Why this matters:** Confidence flags can be in states that +`createFlag` won't fix: archived, or enabled for a different client +only. The setup sequence handles all edge cases so resolves never +fail for avoidable reasons. + +### Rules + +- **NEVER auto-continue** -- always wait for user at each checkpoint +- **Flag-by-flag** -- each flag is one unit (its files + tests) +- **Preserve allocation order** -- one Confidence rule per Eppo + allocation, in the same order +- **PR checkpoints** -- offer to create PR after each flag or batch +- **Resumable** -- update Progress table in plan file after each step + +--- + +## Required Prerequisites Summary + +### For `plan flags` + +| Source | What's used | +|--------|-------------| +| Eppo REST API (`X-Eppo-Token`) | `GET /environments`, `GET /feature-flags`, `GET /feature-flags/{id}`, `GET /feature-flags/{id}/environments/{environmentId}` | +| `confidence` MCP | `listClients`, `getContextSchema`, `createFlag`, `addFlagToClient`, `addContextField`, `addTargetingRule`, `resolveFlag` | + +### For `plan code` + +| Source | What's used | +|--------|-------------| +| `confidence-docs` MCP | `getCodeSnippetAndSdkIntegrationTips`, `searchDocumentation`, `getFullSource` | From 7f93b8223dc47a984a9a0be6115f6edfb3a71a81 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Wed, 27 May 2026 11:51:57 +0200 Subject: [PATCH 2/2] refactor(migrations): extract Confidence-side core into shared file Both platform migration skills (migrate-posthog, migrate-eppo) shared roughly 60% of their content: the Confidence targeting payload format, flag setup sequence, naming rules, execute flow, plan-file resume pattern, client-selection step, step-tracker conventions, and the multivariant split handling. Duplicating that content across N skills makes Confidence-side bug fixes like the targeting-payload-format correction in #15 a multi-place chore and gets worse with every new source platform. This commit extracts all platform-agnostic content into skills/_shared/migration-core.md. Each platform skill now starts with a "read the core file first" instruction and only contains: - Its migration overview ASCII art - Source-platform prerequisites (PostHog MCP install vs Eppo REST API key) - The platform-specific scan logic for Step 1 of plan flags - The platform-specific randomization mapping for Step 3 - The Operator Mapping table (source operator -> Confidence payload strategy) plus any blocked-operator guidance - Plan-flag template fields that differ across platforms - The platform-specific code-scan and transform-rule generation for plan code Steps 3 and 4 - Any platform-specific execute notes (e.g. Eppo's disabled-in-env handling) Net effect: -28% total lines, each platform skill ~500 lines instead of ~1100, and adding a third source platform (LaunchDarkly, Statsig, GrowthBook, ConfigCat, ...) is now mechanical - copy a platform skill template and fill in the four platform-specific sections above. Pure refactor; no user-facing behavior change. The slash commands, plan-file paths, MCP tool names, and execute flow are unchanged. Co-authored-by: Cursor --- skills/_shared/migration-core.md | 858 +++++++++++++++++++++++++ skills/migrate-eppo/SKILL.md | 1006 +++++------------------------- skills/migrate-posthog/SKILL.md | 834 +++---------------------- 3 files changed, 1134 insertions(+), 1564 deletions(-) create mode 100644 skills/_shared/migration-core.md diff --git a/skills/_shared/migration-core.md b/skills/_shared/migration-core.md new file mode 100644 index 0000000..7e8918e --- /dev/null +++ b/skills/_shared/migration-core.md @@ -0,0 +1,858 @@ +# Migration Core (Shared) + +> **This is not a standalone skill.** It is shared content read by every +> platform-specific migration skill in this plugin (`migrate-posthog`, +> `migrate-eppo`, ...). It defines the **Confidence-side** conventions +> that all migrations follow. Each platform skill defines what's +> specific to its source system (PostHog, Eppo, etc.). +> +> When the agent is invoked for a migration command, it MUST read both +> this file AND the relevant `skills/migrate-/SKILL.md`, then +> apply them together. The platform skill says what to do; this file +> says how to talk to Confidence. + +--- + +## How These Files Compose + +Every migration has the same structure: + +``` +Phase 1: Flag Definitions + plan flags → Scan , choose client & entity, generate plan + execute → Create each flag in Confidence with targeting rules + +Phase 2: Code Transformation + plan code → Scan codebase, fetch SDK guide, generate transform rules + execute → Transform code flag by flag, each flag = one PR +``` + +**Why flags first?** Flags must exist in Confidence before code can +resolve them. Once flags are live in Confidence, the code that +evaluates them is migrated — one flag at a time, one PR at a time. + +**Each code PR is scoped to a single flag.** Small, reviewable, and +independently shippable. If one flag's migration has issues, it +doesn't block the others. + +Platform skills implement the platform-specific parts of each phase +(scan the source, map randomization concepts, build the operator +table, scan the source SDK in code). This file implements the +platform-agnostic parts (talk to Confidence, manage the plan file, +run execute). + +--- + +## SDK Preference + +**ALWAYS prefer OpenFeature with local resolve.** + +| Priority | Approach | When to use | +|----------|----------|-------------| +| 1st | Local resolve | Default for all new integrations | +| 2nd | Remote resolve | Only if local resolve not supported for platform | +| Avoid | Direct SDK | Being phased out | + +--- + +## Plan Philosophy + +**Plans must be self-sufficient and agent-agnostic.** + +| Principle | Meaning | +|-----------|---------| +| **Source-boxed** | Every external data fetch uses one explicit channel the platform skill defines (an MCP server, a REST API with curl, etc.) — no ad-hoc browsing | +| **Self-sufficient** | Plan contains ALL information needed — no "query the source for X" at execute time | +| **Agent-agnostic** | Any agent with the prerequisites can execute the plan without prior context | +| **Language-agnostic** | Detect framework, fetch SDK guide from `confidence-docs` MCP dynamically | + +--- + +## Prerequisites: Confidence Side + +Every platform skill requires these. The platform skill is responsible +for documenting its own source-system prerequisites (PostHog MCP, Eppo +API key, etc.) in addition to these. + +### Confidence MCP + +Test: `mcp__confidence__listClients` + +If not available, install it: +``` +claude mcp add confidence --transport http --url https://mcp.confidence.dev/mcp/flags +``` + +The user will be prompted to authenticate via OAuth in their browser. + +### Confidence Docs MCP (required for `plan code` only) + +Test: `mcp__confidence-docs__searchDocumentation` + +If not available, install it: +``` +claude mcp add confidence-docs --transport http --url https://mcp.confidence.dev/mcp/docs +``` + +The user will be prompted to authenticate via OAuth in their browser. + +--- + +## User-Facing Communication Rules + +**NEVER expose internal technical details to the user.** The user should +see human-readable descriptions of what's happening, not internal +implementation details like targeting payload formats, rule types, or +operator names. + +- Do NOT say "creating plan based on eqRule / rangeRule / setRule" etc. +- Do NOT show raw targeting payloads or JSON structures in conversation +- Do NOT echo any user-provided secret (API keys, tokens) back into the + conversation or write them to the plan file — store them only as + environment variables for the session +- DO say things like: "Creating flag with rule: plan equals 'pro' AND country is US or UK" +- DO describe rules in plain English: "age between 18 and 65", "plan is not free" +- The plan FILE may contain MCP command payloads (for machine execution), + but conversation output must be human-friendly + +--- + +## Step Tracker Conventions + +Platform skills define the **layout** of their step trackers (which +steps appear, what columns). This file defines the **markers** and the +**rules** for using them. + +### Status markers + +- `○ pending` — not started yet +- `◉ in progress` — currently running +- `⏸ awaiting user` — blocked on user input (e.g. picking a client or entity) +- `✓ done` — completed (add brief user-facing result) +- `⊘ skipped` — skipped by user + +### Rules + +Use `⏸ awaiting user` whenever the workflow has asked a question and is +waiting for an explicit reply. This makes "I'm blocked on you" visible +to both agent and user, and prevents the agent from drifting into +auto-progression while a question is open. + +**Never expose internal/technical details in the tracker.** No +pagination info, no API page counts, no internal field names. Show only +what matters to the user. + +**Update and re-display the tracker** at the start and after each step +completes. + +### Execute progress bar + +The execute step tracker includes a progress bar. Use `█` for completed +and `░` for remaining. The bar should be 20 characters wide. + +Examples at various stages: + +``` + Progress: [██████░░░░░░░░░░░░░░] 5/15 (1 skipped) + Current: complex-deployment-and-version +``` + +``` + Progress: [████████████████████] 15/15 done + Result: 14 migrated, 1 skipped +``` + +After each flag completes, show one of: + +``` + ✓ flag-key — MATCH (variant-name) + ⊘ flag-key — skipped +``` + +### Final summary (Execute) + +At the end of execution, show a complete summary: + +``` +───── Migration Complete ────────────────────────────────── + Progress: [████████████████████] 15/15 done + Migrated: 14 | Skipped: 1 | Failed: 0 + + ✓ flag-key-1 100% user_id + ✓ flag-key-2 50/50 user_id + ⊘ flag-key-3 — skipped + ... +──────────────────────────────────────────────────────────── +``` + +--- + +## Confidence Naming Rules + +- **Flag names:** lowercase letters, digits, and hyphens only (`[a-z0-9-]`). + Source flag keys often already follow this convention; if not, normalize + (e.g. `Checkout_Redesign` → `checkout-redesign`) and record the mapping + in the plan so the code phase can find the right replacement. +- **Entity references:** Confidence entity names do NOT support underscores. + The entity reference (e.g. `entities/company`) is separate from the context + field name (e.g. `company_id`). When creating entity fields with + `addContextField`, always provide an explicit `entityReference` with a + clean name (no underscores). If omitted, the tool auto-generates one from + the field name which will fail. + + | Field name | Entity reference | Works? | + |------------|-----------------|--------| + | `user_id` | `entities/user` | Yes | + | `company_id` | `entities/company` | Yes | + | `visitor_id` | `entities/visitor` | Yes | + | `company_id` | *(omitted — auto: `entities/company_id`)* | **No** | + +--- + +## Plan Files: Resume Check & Progressive Updates + +Both plan flags and plan code use a progressive plan file. Created at +Step 1, updated after each step, so a closed session can resume. + +### Resume check (MUST do first) + +Before starting any plan workflow, check for an existing in-progress +plan. Each platform defines the file glob: + +- `migrate-posthog plan flags` → `.claude/plans/posthog-flag-migration-*.md` +- `migrate-posthog plan code` → `.claude/plans/posthog-code-migration-*.md` +- `migrate-eppo plan flags` → `.claude/plans/eppo-flag-migration-*.md` +- `migrate-eppo plan code` → `.claude/plans/eppo-code-migration-*.md` + +If a plan file exists, read its `## Generation Status` section: + +- If status is `complete` → tell user a plan already exists, ask if + they want to start fresh or use the existing one +- If status is NOT `complete` → **resume from the last incomplete step**. + Tell the user: "Found an in-progress plan. Resuming from step ." +- If no plan file exists → start fresh + +### Generation Status table + +Every plan file MUST include a `## Generation Status` section at the +top (right after the title) that tracks which steps are done: + +```markdown +## Generation Status + +| Step | Status | Result | +|------|--------|--------| +| 1. Scan | ✓ complete | 15 flags | +| 2. Choose client | ✓ complete | test | +| 3. Map randomization | ○ not started | | +| 4. Generate rules | ○ not started | | +``` + +Status values: `✓ complete`, `◉ in progress`, `○ not started` + +**After each step completes**, update the status table AND write that +step's data to the plan file. Do NOT wait until the end to write. + +--- + +## Confidence Targeting Payload Format + +This is how Confidence targeting rules are structured. Use this when +generating `addTargetingRule` payloads. + +**CRITICAL:** The payload uses a `criteria` + `expression` pattern. +Criteria are named references (`ref-0`, `ref-1`, ...) that define +individual conditions. The `expression` combines them with boolean +logic (`and`, `or`, `not`, `ref`). + +```json +{ + "criteria": { + "ref-0": { + "attribute": { + "attributeName": "", + "": { ... } + } + } + }, + "expression": { "ref": "ref-0" } +} +``` + +**DO NOT use nested rule objects like `{"or": {"operands": [{"eqRule": ...}]}}` +at the top level.** That format is silently parsed as empty targeting +(matching ALL contexts) due to `ignoringUnknownFields()` in the proto +parser. + +### Criterion rules + +| Match | Form | +|---|---| +| String eq | `"eqRule": { "value": { "stringValue": "X" } }` | +| Number eq | `"eqRule": { "value": { "numberValue": N } }` | +| Bool eq | `"eqRule": { "value": { "boolValue": true } }` | +| `>=` | `"rangeRule": { "startInclusive": { "numberValue": N } }` | +| `>` | `"rangeRule": { "startExclusive": { "numberValue": N } }` | +| `<` | `"rangeRule": { "endExclusive": { "numberValue": N } }` | +| `<=` | `"rangeRule": { "endInclusive": { "numberValue": N } }` | +| starts with | `"startsWithRule": { "value": "prefix" }` | +| ends with | `"endsWithRule": { "value": "suffix" }` | + +### Expression combinators + +| Pattern | Expression | +|---------|-----------| +| Single condition | `{ "ref": "ref-0" }` | +| AND | `{ "and": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } }` | +| OR | `{ "or": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } }` | +| NOT | `{ "not": { "ref": "ref-0" } }` | +| NOT IN (list) | `{ "and": { "operands": [{ "not": { "ref": "ref-0" } }, { "not": { "ref": "ref-1" } }] } }` | + +### Worked examples + +**Single equality (country = "US"):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "US" } } } } + }, + "expression": { "ref": "ref-0" } +} +``` + +**IN (country IN [US, UK]):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "US" } } } }, + "ref-1": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "UK" } } } } + }, + "expression": { "or": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } } +} +``` + +**NOT IN (country NOT IN [DE, FR]):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "DE" } } } }, + "ref-1": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "FR" } } } } + }, + "expression": { "and": { "operands": [{ "not": { "ref": "ref-0" } }, { "not": { "ref": "ref-1" } }] } } +} +``` + +**AND (plan = "pro" AND country IN [US, UK]):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "plan", "eqRule": { "value": { "stringValue": "pro" } } } }, + "ref-1": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "US" } } } }, + "ref-2": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "UK" } } } } + }, + "expression": { "and": { "operands": [{ "ref": "ref-0" }, { "or": { "operands": [{ "ref": "ref-1" }, { "ref": "ref-2" }] } }] } } +} +``` + +**Range (age >= 30):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "age", "rangeRule": { "startInclusive": { "numberValue": 30 } } } } + }, + "expression": { "ref": "ref-0" } +} +``` + +**Ends with (email ends with @spotify.com OR @gmail.com):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "email", "endsWithRule": { "value": "@spotify.com" } } }, + "ref-1": { "attribute": { "attributeName": "email", "endsWithRule": { "value": "@gmail.com" } } } + }, + "expression": { "or": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } } +} +``` + +**Starts with (utm_source starts with "email-"):** +```json +{ + "criteria": { + "ref-0": { "attribute": { "attributeName": "utm_source", "startsWithRule": { "value": "email-" } } } + }, + "expression": { "ref": "ref-0" } +} +``` + +--- + +## Platform Operator Mapping Contract + +Each platform skill MUST provide an "Operator Mapping" section that +maps the source platform's operators to Confidence payload strategies +expressed in the format above. The table columns are: + +| Source operator | Confidence payload strategy | + +Each row should describe a single source-side operator and how to +realize it using the criterion rules and expression combinators in +this file. When an operator has no clean mapping (e.g. arbitrary +regex, set membership in cohorts, "is set"/"is not set" semantics), +the platform skill MUST list it under a **Blocked** subsection with +guidance on what the user should do (rewrite the rule, migrate the +flag manually, or skip). + +--- + +## Multivariant A/B Split Handling + +**CRITICAL:** A single Confidence targeting rule CAN assign multiple +variants at different split percentages. Use ONE rule per source-side +targeting unit (PostHog filter group, Eppo allocation, etc.), listing +all variants and their shares in that rule. + +**How to map source-side splits to Confidence rules:** + +For a single-variant assignment (e.g. feature gate, kill switch): +- Add ONE rule with one variant assignment at 100%. + +For a 2-variant flag (e.g. control 50% / treatment 50%): +- Add ONE rule with two variant assignments: + control at 50%, treatment at 50%. + +For a 3+ variant flag (e.g. control 34% / A 33% / B 33%): +- Add ONE rule with three variant assignments: + control at 34%, A at 33%, B at 33%. + +**Do NOT create separate rules per variant.** One targeting rule = +one set of targeting conditions, with the variant split defined +inside that rule. The `rolloutPercentage` on the rule controls +what fraction of subjects who match the targeting conditions enter +the rule at all (use 100% unless you want a partial rollout on top of +the targeting). The variant percentages within the rule control the +split among those who enter. + +**Source-side "traffic exposure" / "rollout percentage"** maps to the +rule's `rolloutPercentage`. Subjects who match the targeting conditions +but fall outside that percentage continue down the waterfall to the +next rule. + +--- + +## Plan Flags: Standard Workflow + +Platform skills follow the same 4-step plan flow. This section defines +the steps that are identical across platforms (steps 2 and 4) and +declares the contract for steps that platforms implement themselves +(steps 1 and 3). + +### Step 1: Scan the source platform *(platform-specific)* + +Each platform skill defines how to: +- Discover flags in the source (REST, MCP, etc.) +- Paginate until all flags are fetched +- Extract per-flag: key, name, description, variation type, variations, + targeting rules with operators, rollout percentages, randomization + concept (per-subject, per-group, etc.) +- Skip archived flags by default unless the user opts in +- Write flag data to the plan file in batches so progress survives + session loss + +**After scan completes:** Update Generation Status step 1 to `✓ complete`. + +### Step 2: Select Confidence client *(shared)* + +``` +mcp__confidence__listClients +``` + +**EDUCATE then ASK the user:** + +> **What is a client?** +> A client represents the application that resolves flags — your website, +> backend service, or mobile app. Each client has its own secret for +> authentication and can be scoped to environments (dev, staging, prod). +> Flags are associated with one or more clients, so Confidence knows which +> application should receive which flags. +> +> Think of it like: "Where will these flags be evaluated?" +> +> Your existing clients: +> 1. +> 2. +> ... +> N. Create a new client +> +> Which client should I use as the default for all flags? +> You can always rearrange them later in the Confidence UI. + +**Wait for an explicit pick.** Set the step to `⏸ awaiting user` and +stop. A re-run of the migration command, an empty message, or any reply +that is not a number from the list / `new ` is **not** consent — +NEVER infer the recommendation from silence. If the reply is ambiguous, +re-ask, listing the choices again. + +- If user picks existing → use it +- If user wants new → ASK for name → `mcp__confidence__createClient` + +**After client selected:** Write the "Default Client" section to the +plan file and update Generation Status step 2 to `✓ complete`. + +### Step 3: Map randomization concepts *(platform-specific)* + +Each platform skill defines how its randomization concepts (PostHog +`distinct_id` and `aggregation_group_type_index`, Eppo `subjectKey`, +etc.) map to Confidence entity fields. + +**Standard MCP call** for showing the user available entity fields: + +``` +mcp__confidence__getContextSchema clientName: "" +``` + +**Standard creation call** when a new entity field is needed: + +``` +mcp__confidence__addContextField + clientName: "" + fieldName: "" + type: "" + entityReference: "entities/" ← MUST be set, see Confidence Naming Rules +``` + +**Standard rule for entity-vs-attribute separation.** Step 3 only +creates **entity** fields (the randomization identifiers). Attribute +fields used in targeting rules (`plan`, `country`, `age`, etc.) MUST +NOT be created here. Record them in the "Need to Create" subsection +of the Context Schema section and let `execute` create them — that +way, if the user later skips a flag, no orphan schema fields are left +in Confidence. + +**After randomization mapped:** Write the platform's Subject/Randomization +Mapping section, reconcile and write the Context Schema section, and +update Generation Status step 3 to `✓ complete`. + +### Step 4: Generate MCP commands *(shared scaffolding, platform-specific operators)* + +**Confirmation gate (MUST pass before generating).** Before writing the +Flags to Migrate section, summarize the choices made in earlier steps +in chat and ask: + +> Plan will assume client `` with randomization entity +> `` [and any other platform-specific selections, e.g. source +> environment]. All flags will be defaulted to `[ ] Migrate [ ] Skip` +> (neither pre-checked) — you'll opt each one in during review. +> Confirm or change? + +Set the step to `⏸ awaiting user` and stop. Only proceed on an +explicit `yes` / `confirm` / equivalent. A re-run or ambiguous reply +is **not** confirmation. + +For each flag, generate the MCP command payloads (`createFlag`, +`addFlagToClient`, `addTargetingRule`, `resolveFlag`) using the +platform skill's Operator Mapping table together with this file's +Confidence Targeting Payload Format. Write them into each flag's +section in the plan. + +**After all commands generated:** Update Generation Status step 4 to +`✓ complete` and set the overall status to `complete`. Write the +Progress table. + +**Tell the user:** + +> Plan generated! Review it at `.claude/plans/-flag-migration-.md` +> +> Migration is **opt-in**: every flag starts with both checkboxes +> empty. Tick `[x] Migrate` or `[x] Skip` for each flag — `execute` +> will refuse any flag with neither box set. +> When you're ready, run: `/migrate- execute ` + +--- + +## Plan Code: Standard Workflow + +### Step 1: Detect language & framework *(shared scaffolding, platform-specific imports)* + +``` +Grep: pattern="" → Find source usage +Glob: pattern="package.json" or "build.gradle" or "Cargo.toml" or "pyproject.toml" etc +Read: dependency file → Determine language/framework +``` + +The platform skill provides the exact patterns to grep for (package +names, function names, import lines). + +### Step 2: Fetch SDK guide from `confidence-docs` MCP *(shared)* + +Query the `confidence-docs` MCP based on detected language: + +``` +mcp__confidence-docs__getCodeSnippetAndSdkIntegrationTips + sdk: "" +``` + +``` +mcp__confidence-docs__searchDocumentation + query: "OpenFeature local resolve " +``` + +``` +mcp__confidence-docs__getFullSource + source: "https://confidence.spotify.com/docs/sdks/server/" +``` + +**CRITICAL:** Include the ACTUAL response in the plan, not a reference +to fetch it. Plans are self-sufficient. + +### Step 3: Scan codebase for source-platform usage *(platform-specific)* + +Each platform skill defines: +- The grep patterns to find SDK calls +- Which information to extract from each call site (flag key, + randomization identifier argument, attributes argument, default value) +- How to group files by flag key + +### Step 4: Generate transform rules *(platform-specific)* + +Each platform skill defines the source SDK API surface and how it maps +to OpenFeature / Confidence. The output is a find/replace rule set +written into the plan's "Transform Rules" section. + +(Adjust method casing per language — `getStringValue` in JS/TS, +`get_string_value` in Python, `getValue` in Kotlin, etc. — +based on the MCP-fetched SDK guide.) + +### Step 5: Generate plan *(shared template)* + +Save the plan to `.claude/plans/-code-migration-.md` +using the template below. + +--- + +## Plan Code: Template + +```markdown +# to Confidence Code Migration Plan + +**Created:** +**Scope:** Code transformation only +**Language:** +**Framework:** + +--- + +## Generation Status + +| Step | Status | Result | +|------|--------|--------| +| 1. Detect language | ○ not started | | +| 2. Fetch SDK guide | ○ not started | | +| 3. Scan codebase | ○ not started | | +| 4. Transform rules | ○ not started | | +| 5. Group by flag | ○ not started | | + +**Overall:** in progress + +--- + +## 1. SDK Setup + +### Install + + + +### API Reference (from MCP: confidence-docs) + + + +### Create Confidence Wrapper + +**File:** + +**Must match source API surface:** + +| Method | Signature | +|--------|-----------| + + +--- + +## 2. Transform Rules + +### Source Files + +| Find | Replace | +|------|---------| +| | | +| | | + +### Test Files + +| Find | Replace | +|------|---------| +| | | + +--- + +## 3. Files to Transform + + + +--- + +## 4. Progress + +| # | Item | Status | +|---|------|--------| +| 0 | SDK Setup | :white_circle: | +``` + +--- + +## Execute: How It Works + +`execute ` walks through the plan interactively, step by +step. Both code and flag execute flows follow the same conventions +below; the platform skill may add platform-specific guidance (e.g. a +warning when a flag is disabled in the source environment). + +### For code plans + +**Each flag = one PR.** The code migration creates a separate pull +request for each flag, keeping changes small and reviewable. + +``` +1. READ the plan file +2. SDK SETUP (Section 1 of plan) — one-time, before any flag + - Show install command from plan + - ASK: "Install SDK now? [Yes / Skip / I already did]" + - If Yes → run install command + - Show wrapper file path + API surface from plan + - ASK: "Create the Confidence wrapper now? [Yes / Skip / I already did]" + - If Yes → create the file using plan's API reference +3. FOR EACH FLAG in the files list: + a. Create a branch: `migrate/-to-confidence` + b. Show flag name + all files using it + c. ASK: "Transform this flag's files? [Yes / Skip / Pause]" + d. If Yes → apply transform rules from plan to all files for this flag + e. Run lint + typecheck on changed files + f. Commit changes + g. Create PR with title: "feat: migrate from to Confidence" + h. Show PR link + i. CHECKPOINT: "PR created. [Continue to next flag / Pause]?" + j. Wait for user response +4. COMPLETION + - Show summary: migrated vs skipped + - List all PRs created with links +``` + +### For flag plans + +``` +1. READ the plan file + - Client is already in the plan — use it, do NOT re-ask + - Randomization entity is already in the plan + - Any platform-specific selections (e.g. Eppo source environment) + are already in the plan + - REFUSE TO PROCEED if any flag has neither `[x] Migrate` nor + `[x] Skip` ticked. List those flags back to the user and ask + them to tick a box for each before re-running execute. Migration + is opt-in — never assume a default. + - REFUSE TO PROCEED if any flag is marked `BLOCKED` and the user + hasn't either resolved the block or ticked `[x] Skip`. Surface + the BLOCKED flags and the reason for each. +2. FOR EACH FLAG marked [x] Migrate: + - Show flag name, description, and rules in plain English + - ASK: "Create this flag in Confidence? [Yes / Skip / Pause]" + - If Yes → run the Flag Setup Sequence (below) + - CHECKPOINT: "Flag done. [Continue / Pause]?" + - Wait for user response +3. COMPLETION + - Show summary: created vs skipped +``` + +### Flag Setup Sequence (MUST complete all steps before resolving) + +Each flag MUST go through these steps in order. Do NOT call +`resolveFlag` until ALL prior steps succeed. + +``` +STEP 1: createFlag + → If flag already exists, check the response for which clients + it's enabled on. + +STEP 2: Ensure flag is active and on the correct client + → If createFlag response does NOT list the target client: + a. Try addFlagToClient + b. If that fails with "Cannot update an archived flag": + → unarchiveFlag first, then retry addFlagToClient + → If createFlag response lists the target client: proceed + +STEP 3: addTargetingRule + → Add the targeting rule(s) from the plan. If the source has multiple + ordered targeting units (e.g. an Eppo allocation waterfall, ordered + PostHog filter groups), emit one addTargetingRule call per unit in + the SAME ORDER. Confidence evaluates rules top-down — order is + semantically significant. + → IMPORTANT: targeting rules added while a flag is archived OR + immediately after unarchiving may become inactive. Always complete + steps 1-2 fully (createFlag, unarchive, addFlagToClient) BEFORE + calling addTargetingRule. Do NOT add rules between createFlag and + unarchiveFlag — they will be inactive and you'll have to re-add. + +STEP 4: resolveFlag (verification) + → Only NOW resolve to verify the flag works + → MUST test BOTH positive AND negative cases: + a. Resolve with a context that SHOULD match the targeting rule + → Verify the expected variant is returned + b. Resolve with a context that SHOULD NOT match + → Verify no variant / default is returned + → For multi-rule flags, also resolve with a context that misses the + first rule but matches a later one — this verifies the waterfall + order is correct. + → For attribute-based targeting (country, plan, etc.), the resolve + call MUST include those attributes in the evaluation context. + Without them, the targeting conditions cannot be evaluated and + may appear to match when they wouldn't in production. + → If resolve fails with "No active flags found": + something went wrong in steps 1-2 — diagnose, don't skip + → If all rules show "Rule is inactive" / no match: + targeting rules were likely added while flag was archived. + Re-add the targeting rule now that the flag is active. + → Do NOT report a flag as successfully migrated until both + positive and negative resolve tests pass. +``` + +**Why this matters:** Confidence flags can be in states that +`createFlag` won't fix: archived, or enabled for a different client +only. The setup sequence handles all edge cases so resolves never +fail for avoidable reasons. + +### Rules + +- **NEVER auto-continue** — always wait for user at each checkpoint +- **Flag-by-flag** — each flag is one unit (its files + tests) +- **Preserve source order** — one Confidence rule per source-side + targeting unit, in the same order +- **PR checkpoints** — offer to create PR after each flag or batch +- **Resumable** — update Progress table in plan file after each step + +--- + +## Required MCPs (Confidence side) + +Every migration needs these. Platform skills add their own source-side +prerequisites. + +### For `plan code` + +| MCP | Tools Used | +|-----|------------| +| `confidence-docs` | `getCodeSnippetAndSdkIntegrationTips`, `searchDocumentation`, `getFullSource` | + +### For `plan flag` and `execute` + +| MCP | Tools Used | +|-----|------------| +| `confidence` | `listClients`, `createClient`, `getContextSchema`, `addContextField`, `createFlag`, `addFlagToClient`, `unarchiveFlag`, `addTargetingRule`, `resolveFlag` | diff --git a/skills/migrate-eppo/SKILL.md b/skills/migrate-eppo/SKILL.md index 8cb10b2..0723854 100644 --- a/skills/migrate-eppo/SKILL.md +++ b/skills/migrate-eppo/SKILL.md @@ -4,29 +4,14 @@ description: Migrate feature flags from Eppo to Confidence SDK. Use when the use # Eppo to Confidence Migration -REST-driven, self-sufficient migration from Eppo to Confidence. - -## Migration Flow - -The migration happens in two phases: **flags first, then code**. - -``` -Phase 1: Flag Definitions - plan flags → Scan Eppo, choose client & entity, generate plan - execute → Create each flag in Confidence with targeting rules +> **Read the shared core first.** Before doing anything else, use the +> Read tool to read `skills/_shared/migration-core.md`. That file +> defines all Confidence-side conventions every migration follows — +> payload formats, the flag setup sequence, naming rules, the execute +> flow, etc. THIS file only covers what's specific to Eppo. Apply +> both together. -Phase 2: Code Transformation - plan code → Scan codebase, fetch SDK guide, generate transform rules - execute → Transform code flag by flag, each flag = one PR -``` - -**Why flags first?** The flags need to exist in Confidence before the -code can resolve them. Once flags are live in Confidence, you migrate -the code that evaluates them — one flag at a time, one PR at a time. - -**Each code PR is scoped to a single flag.** This keeps PRs small, -reviewable, and independently shippable. If one flag's migration has -issues, it doesn't block the others. +REST-driven, self-sufficient migration from Eppo to Confidence. ## Commands @@ -41,8 +26,7 @@ issues, it doesn't block the others. ## Migration Overview (MUST display at start of `plan flags` or `plan code`) **Every time** the user runs `plan flags` or `plan code`, display this -overview FIRST — before doing any work. This orients the user on where -they are in the full migration journey. +overview FIRST — before doing any work. ``` ═══════════════════════════════════════════════════════════════ @@ -104,42 +88,15 @@ Then proceed with the normal workflow for that phase. --- -## SDK Preference - -**ALWAYS prefer OpenFeature with local resolve.** - -| Priority | Approach | When to use | -|----------|----------|-------------| -| 1st | Local resolve | Default for all new integrations | -| 2nd | Remote resolve | Only if local resolve not supported for platform | -| Avoid | Direct SDK | Being phased out | - ---- - -## Plan Philosophy - -**Plans must be self-sufficient and agent-agnostic.** - -| Principle | Meaning | -|-----------|---------| -| **Self-sufficient** | Plan contains ALL information needed — no "query the API for X" | -| **Agent-agnostic** | Any agent with the prerequisites can execute without prior context | -| **Language-agnostic** | Detect framework, fetch SDK guide from MCP dynamically | - ---- - -## Prerequisites - -Before starting any workflow, check that required prerequisites are -available. If any are missing, install or configure them before -proceeding. +## Prerequisites: Eppo Side -### Eppo API access +(The core file documents the Confidence-side prerequisites — install +`confidence` and `confidence-docs` MCP servers.) Eppo does not currently publish a Claude MCP server, so the migration talks to Eppo's REST API directly using `curl` from the Bash tool. -**Required:** +### Required 1. An **Eppo API key** (NOT an SDK key). Generated in the Eppo dashboard under **Admin > API Keys**. The key needs read access to @@ -150,7 +107,7 @@ talks to Eppo's REST API directly using `curl` from the Bash tool. **Authentication header:** `X-Eppo-Token: ` -**ASK the user (only if not already provided):** +### ASK the user (only if not already provided) > To read your Eppo flags, I need an Eppo API key (Admin > API Keys > in the Eppo dashboard — make sure it has read access to feature @@ -161,14 +118,18 @@ talks to Eppo's REST API directly using `curl` from the Bash tool. > > What's your Eppo API base URL? Default is `https://eppo.cloud/api/v1`. -**Storing the key:** Once provided, store the key for the session in -the environment variable `EPPO_API_KEY` (export it in the Bash session -the agent uses) and reference it via `$EPPO_API_KEY` in every `curl` -call — never hardcode the key into the plan file, the conversation -output, or any committed file. If the user pastes a key inline, scrub -it from the plan file and only keep a placeholder like ``. +### Storing the key -**Smoke test before scanning:** +Once provided, store the key for the session in the environment +variable `EPPO_API_KEY` (export it in the Bash session the agent uses) +and reference it via `$EPPO_API_KEY` in every `curl` call — never +hardcode the key into the plan file, the conversation output, or any +committed file. If the user pastes a key inline, scrub it from the plan +file and only keep a placeholder like ``. (See also +the "never echo secrets" rule in the core file's user-facing +communication rules.) + +### Smoke test before scanning ```bash curl -sS -H "X-Eppo-Token: $EPPO_API_KEY" \ @@ -179,165 +140,9 @@ curl -sS -H "X-Eppo-Token: $EPPO_API_KEY" \ If this returns a `401`/`403` or HTML, stop and surface the error to the user — do not start scanning. -### Confidence MCP - -Test: `mcp__confidence__listClients` - -If not available, install it: -``` -claude mcp add confidence --transport http --url https://mcp.confidence.dev/mcp/flags -``` - -The user will be prompted to authenticate via OAuth in their browser. - -### Confidence Docs MCP (for `plan code` only) - -Test: `mcp__confidence-docs__searchDocumentation` - -If not available, install it: -``` -claude mcp add confidence-docs --transport http --url https://mcp.confidence.dev/mcp/docs -``` - -The user will be prompted to authenticate via OAuth in their browser. - ---- - -## User-Facing Communication Rules - -**NEVER expose internal technical details to the user.** The user should see -human-readable descriptions of what's happening, not internal implementation -details like targeting payload formats, rule types, or operator names. - -- Do NOT say "creating plan based on eqRule / rangeRule / setRule" etc. -- Do NOT show raw targeting payloads or JSON structures in conversation -- Do NOT echo the user's Eppo API key back into the conversation or plan -- DO say things like: "Creating flag with rule: country is US or UK AND appVersion >= 28.5.0" -- DO describe rules in plain English: "age between 18 and 65", "plan is not free" -- The plan FILE may contain MCP command payloads (for machine execution), - but conversation output must be human-friendly - -**Step Tracker:** Display a visual step tracker at every phase transition. -The tracker shows all phases, marks completed ones, highlights the current -one, and shows remaining ones. Update and re-display it each time you move -to a new phase. - -### Plan Flags Step Tracker - -Display this at the START and after EACH step completes (updating status): - -``` -───── Plan Flags ────────────────────────────────────────── - [1] Scan Eppo ○ pending - [2] Choose client ○ pending - [3] Map subject ○ pending - [4] Generate plan ○ pending -──────────────────────────────────────────────────────────── -``` - -Status markers: -- `○ pending` — not started yet -- `◉ in progress` — currently running -- `⏸ awaiting user` — blocked on user input (e.g. picking a client, environment, or entity) -- `✓ done` — completed (add brief user-facing result) -- `⊘ skipped` — skipped by user - -Use `⏸ awaiting user` whenever the workflow has asked a question and is -waiting for an explicit reply. This makes "I'm blocked on you" visible -to both agent and user, and prevents the agent from drifting into -auto-progression while a question is open. - -**IMPORTANT:** Never expose internal/technical details in the tracker. -No pagination info, no API page counts, no internal field names. -Show only what matters to the user. - -Example after Step 1 completes: -``` -───── Plan Flags ────────────────────────────────────────── - [1] Scan Eppo ✓ 12 flags found (environment: Production) - [2] Choose client ◉ in progress - [3] Map subject ○ pending - [4] Generate plan ○ pending -──────────────────────────────────────────────────────────── -``` - -### Execute Step Tracker - -Display this at the START and update after EACH flag: - -``` -───── Execute Migration ─────────────────────────────────── - Client: test | Subject: user_id | Flags: 12 - Progress: [░░░░░░░░░░░░░░░░░░░░] 0/12 -──────────────────────────────────────────────────────────── -``` - -Update the progress bar as flags are processed. Use `█` for completed -and `░` for remaining. The bar should be 20 characters wide. - -Examples at various stages: -``` - Progress: [██████░░░░░░░░░░░░░░] 4/12 (1 skipped) - Current: checkout-redesign -``` - -``` - Progress: [████████████████████] 12/12 done - Result: 11 migrated, 1 skipped -``` - -After each flag completes, show: -``` - ✓ checkout-redesign — MATCH (treatment) -``` - -After a skip: -``` - ⊘ legacy-onboarding — skipped -``` - -### Final Summary (Execute) - -At the end of execution, show a complete summary: - -``` -───── Migration Complete ────────────────────────────────── - Progress: [████████████████████] 12/12 done - Migrated: 11 | Skipped: 1 | Failed: 0 - - ✓ checkout-redesign 50/50 user_id - ✓ pricing-experiment 34/33/33 user_id - ⊘ legacy-onboarding — skipped - ✓ internal-tools-gate 100% user_id - ... -──────────────────────────────────────────────────────────── -``` - ---- - -## Confidence Naming Rules - -- **Flag names:** lowercase letters, digits, and hyphens only (`[a-z0-9-]`). - Eppo flag keys often already follow this convention; if not, normalize - (e.g. `Checkout_Redesign` → `checkout-redesign`) and record the mapping - in the plan so the code phase can find the right replacement. -- **Entity references:** Confidence entity names do NOT support underscores. - The entity reference (e.g. `entities/company`) is separate from the context - field name (e.g. `company_id`). When creating entity fields with - `addContextField`, always provide an explicit `entityReference` with a - clean name (no underscores). If omitted, the tool auto-generates one from - the field name which will fail. - - | Field name | Entity reference | Works? | - |------------|-----------------|--------| - | `user_id` | `entities/user` | Yes | - | `company_id` | `entities/company` | Yes | - | `visitor_id` | `entities/visitor` | Yes | - | `company_id` | *(omitted — auto: `entities/company_id`)* | **No** | - --- -## Eppo REST Reference (agent-internal) +## Eppo REST Reference The migration uses these endpoints. All require `-H "X-Eppo-Token: $EPPO_API_KEY"`. Base URL defaults to `https://eppo.cloud/api/v1`. @@ -366,216 +171,55 @@ an empty page. Eppo's API uses page-based pagination, not cursors. --- -## Plan Code: Workflow - -### Resume Check (MUST do first) - -Same as Plan Flag: check for existing `.claude/plans/eppo-code-migration-*.md`. -If found with incomplete `Generation Status`, resume from the last -incomplete step. If complete, ask user if they want to start fresh. -If not found, start fresh. +## Step Trackers -The plan file uses the same progressive pattern: created at Step 1, -updated after each step, with a `## Generation Status` section. +(The core file defines the marker legend, progress-bar conventions, and +final-summary format. This section just declares the layouts.) -### Step 1: Detect Language & Framework +### Plan Flags step tracker ``` -Grep: pattern="eppo|Eppo|EppoClient|get_.*_assignment|getStringAssignment|getBooleanAssignment" -> Find Eppo usage -Glob: pattern="package.json" or "build.gradle" or "Cargo.toml" or "pyproject.toml" etc -Read: dependency file -> Determine language/framework -``` - -The Eppo package names to look for: -- JS/TS: `@eppo/js-client-sdk`, `@eppo/node-server-sdk`, `@eppo/react-native-sdk` -- Python: `eppo-server-sdk` -- Java/Kotlin: `cloud.eppo:eppo-server-sdk` -- Go: `github.com/Eppo-exp/golang-sdk` -- Ruby: `eppo-server-sdk` -- Rust: `eppo_sdk` -- Swift / iOS: `eppo-ios-sdk` -- Android: `cloud.eppo:eppo-android-sdk` -- DotNet: `Eppo.Sdk` - -### Step 2: Fetch SDK Guide from MCP - -**Query confidence-docs MCP based on detected language:** - -``` -mcp__confidence-docs__getCodeSnippetAndSdkIntegrationTips - sdk: "" -``` - -``` -mcp__confidence-docs__searchDocumentation - query: "OpenFeature local resolve " +───── Plan Flags ────────────────────────────────────────── + [1] Scan Eppo ○ pending + [2] Choose client ○ pending + [3] Map subject ○ pending + [4] Generate plan ○ pending +──────────────────────────────────────────────────────────── ``` +Example after Step 1 completes: ``` -mcp__confidence-docs__getFullSource - source: "https://confidence.spotify.com/docs/sdks/server/" +───── Plan Flags ────────────────────────────────────────── + [1] Scan Eppo ✓ 12 flags found (environment: Production) + [2] Choose client ◉ in progress + [3] Map subject ○ pending + [4] Generate plan ○ pending +──────────────────────────────────────────────────────────── ``` -**CRITICAL:** Include the ACTUAL response in the plan, not a reference to fetch it. +### Execute step tracker -### Step 3: Scan Codebase for Eppo Usage - -``` -Grep: pattern="" -> Find all imports -Grep: pattern="get_(string|boolean|numeric|integer|json)_assignment|getStringAssignment|getBooleanAssignment|getNumericAssignment|getIntegerAssignment|getJSONAssignment" -> Find evaluations ``` - -Group files by **flag key** they reference. The flag key is the first -argument to every Eppo `get_*_assignment` call. - -For each evaluation site, record: -- Flag key -- Return type (inferred from which `get_*_assignment` variant is used) -- The `subjectKey` argument (so the transform can map it to `targetingKey`) -- The `subjectAttributes` argument (so the transform can carry them into - the evaluation context) -- The `defaultValue` argument (carried over to the Confidence call) - -### Step 4: Generate Transform Rules - -Based on SDK guide from MCP: -- Extract install commands -- Extract initialization code -- Extract flag evaluation API -- Generate find/replace rules matching Eppo → Confidence patterns - -**Typed assignment mapping (Eppo → OpenFeature / Confidence):** - -| Eppo call | OpenFeature call | -|-----------|------------------| -| `client.get_string_assignment(k, sk, attrs, default)` | `client.getStringValue(k, default, { targetingKey: sk, ...attrs })` | -| `client.get_boolean_assignment(k, sk, attrs, default)` | `client.getBooleanValue(k, default, { targetingKey: sk, ...attrs })` | -| `client.get_numeric_assignment(k, sk, attrs, default)` | `client.getNumberValue(k, default, { targetingKey: sk, ...attrs })` | -| `client.get_integer_assignment(k, sk, attrs, default)` | `client.getNumberValue(k, default, { targetingKey: sk, ...attrs })` | -| `client.get_json_assignment(k, sk, attrs, default)` | `client.getObjectValue(k, default, { targetingKey: sk, ...attrs })` | - -(Adjust method casing per language — `getStringValue` in JS/TS, `get_string_value` -in Python, `getValue` in Kotlin, etc. — based on the MCP-fetched -SDK guide.) - -### Step 5: Generate Plan - -Save to `.claude/plans/eppo-code-migration-.md` - ---- - -## Plan Code: Template - -```markdown -# Eppo to Confidence Code Migration Plan - -**Created:** -**Scope:** Code transformation only -**Language:** -**Framework:** - ---- - -## 1. SDK Setup - -### Install - - - -### API Reference (from MCP: confidence-docs) - - - -### Create Confidence Wrapper - -**File:** - -**Must match Eppo API surface:** - -| Method | Signature | -|--------|-----------| - - ---- - -## 2. Transform Rules - -### Source Files - -| Find | Replace | -|------|---------| -| | | -| `client.get_string_assignment(k, sk, attrs, default)` | `client.getStringValue(k, default, { targetingKey: sk, ...attrs })` | -| | | - -### Test Files - -| Find | Replace | -|------|---------| -| | | - ---- - -## 3. Files to Transform - - - ---- - -## 4. Progress - -| # | Item | Status | -|---|------|--------| -| 0 | SDK Setup | :white_circle: | +───── Execute Migration ─────────────────────────────────── + Client: test | Subject: user_id | Flags: 12 + Progress: [░░░░░░░░░░░░░░░░░░░░] 0/12 +──────────────────────────────────────────────────────────── ``` --- -## Plan Flag: Workflow - -### Resume Check (MUST do first) - -Before starting, check for an existing in-progress plan: +## Plan Flag: Eppo-Specific Steps -``` -Glob: .claude/plans/eppo-flag-migration-*.md -``` - -If a plan file exists, read its `## Generation Status` section: -- If status is `complete` → tell user a plan already exists, ask if - they want to start fresh or use the existing one -- If status is NOT `complete` → **resume from the last incomplete step** - Tell the user: "Found an in-progress plan. Resuming from step ." -- If no plan file exists → start fresh - -### Progressive Plan File - -The plan file is created at the START (Step 1) and updated after EACH -step. This means if the session closes, the file has partial progress -that can be resumed. - -**File path:** `.claude/plans/eppo-flag-migration-.md` - -The plan file MUST include a `## Generation Status` section at the top -(right after the title) that tracks which steps are done: - -```markdown -## Generation Status +The core file defines the workflow shape (Step 1 scan, Step 2 client +selection, Step 3 randomization mapping, Step 4 generate). This +section provides the Eppo-specific implementations of steps 1 and 3, +and the operator mapping table needed by step 4. -| Step | Status | Result | -|------|--------|--------| -| 1. Scan Eppo | ✓ complete | 12 flags | -| 2. Choose client | ✓ complete | test | -| 3. Map subject | ○ not started | | -| 4. Generate rules | ○ not started | | -``` +### Plan-file path -Status values: `✓ complete`, `◉ in progress`, `○ not started` +`.claude/plans/eppo-flag-migration-.md` -**After each step completes**, update the status table AND write that -step's data to the plan file. Do NOT wait until the end to write. - -### Step 1: Scan Eppo Flags +### Step 1: Scan Eppo flags **Step 1a — pick the source environment.** @@ -650,65 +294,18 @@ Extract from each flag: accidentally; surface this clearly in the plan - Ordered list of `allocations` with: - `allocationType` (Feature Gate, Experiment, or Audience) - - `trafficExposure` (0–1) + - `trafficExposure` (0–1) → maps to Confidence rule `rolloutPercentage` - `targetingRules[]` (`conditions: [{ attribute, operator, value }]`) - `variationWeightsByKey` — the split among variations - The default variation (what subjects see when no allocation matches) -**Determine the randomization unit:** Eppo always uses `subjectKey`. -Unlike PostHog there's no per-group bucketing concept built into the -flag — group-level experiments are handled by passing a `companyId` as -the `subjectKey`. For the migration, treat every flag as per-subject; -the user will pick which Confidence entity field represents that subject -in Step 3. - -**After scan completes:** Update Generation Status step 1 to `✓ complete`. - -### Step 2: Select Confidence Client +**Randomization unit.** Eppo always uses `subjectKey`. Unlike PostHog +there's no per-group bucketing concept built into the flag — group-level +experiments are handled by passing a `companyId` as the `subjectKey`. +For the migration, treat every flag as per-subject; the user picks which +Confidence entity field represents that subject in Step 3. -``` -mcp__confidence__listClients -``` - -**EDUCATE then ASK the user:** - -> **What is a client?** -> A client represents the application that resolves flags — your website, -> backend service, or mobile app. Each client has its own secret for -> authentication and can be scoped to environments (dev, staging, prod). -> Flags are associated with one or more clients, so Confidence knows which -> application should receive which flags. -> -> Think of it like: "Where will these flags be evaluated?" -> -> Your existing clients: -> 1. -> 2. -> ... -> N. Create a new client -> -> Which client should I use as the default for all flags? -> You can always rearrange them later in the Confidence UI. - -**Wait for an explicit pick.** Set the step to `⏸ awaiting user` and -stop. A re-run of `/migrate-eppo`, an empty message, or any reply -that is not a number from the list / `new ` is **not** consent — -NEVER infer the recommendation from silence. If the reply is ambiguous, -re-ask, listing the choices again. - -- If user picks existing -> use it -- If user wants new -> ASK for name -> `mcp__confidence__createClient` - -**After client selected:** Write Section 1 (Default Client) to plan -file and update Generation Status step 2 to `✓ complete`. - -### Step 3: Map Subject Key to a Confidence Entity Field - -``` -mcp__confidence__getContextSchema clientName: "" -``` - -Show the user entity fields (fields marked as entity in the schema). +### Step 3: Map Subject Key (Eppo-specific) This step maps Eppo's `subjectKey` to a Confidence entity field. @@ -740,13 +337,13 @@ This step maps Eppo's `subjectKey` to a Confidence entity field. > > Which Confidence field represents the same identifier as `subjectKey`? -**Wait for an explicit pick.** Same rule as Step 2 — set the step to -`⏸ awaiting user` and stop. Silence, a re-run, or any non-listed reply -is **not** consent. Re-ask if the reply is ambiguous. +Same wait-for-explicit-pick rule as Step 2 in the core file. Silence is +not consent. -- If user picks existing -> use it as `targetingKey` -- If user wants new -> ASK for name + type -> `mcp__confidence__addContextField` - (always provide an explicit `entityReference` — see Confidence Naming Rules) +- If user picks existing → use it as `targetingKey` +- If user wants new → ASK for name + type → `mcp__confidence__addContextField` + (always provide an explicit `entityReference` — see Confidence Naming + Rules in the core file) **Eppo subject targeting (`id` attribute).** Eppo lets rules target the subject directly via the special attribute `id`. When a rule references @@ -754,116 +351,33 @@ subject directly via the special attribute `id`. When a rule references context key for `targetingKey`). Record this substitution in Section 2 of the plan. -**Step 3 only creates the entity field** (the subject's entity). -Attribute fields used in targeting rules (`country`, `plan`, `device`, -`appVersion`, etc.) MUST NOT be created here. Record them in Section 3 -"Need to Create" and let `execute` create them — that way, if the user -later skips a flag, no orphan schema fields are left in Confidence. - -**After entity mapped:** Write Section 2 (Subject Mapping) to plan -file, reconcile and write Section 3 (Context Schema), and update -Generation Status step 3 to `✓ complete`. - -### Step 4: Generate MCP Commands - -**Confirmation gate (MUST pass before generating).** Before writing -Section 4, summarize chosen client + entity + Eppo environment in chat -and ask: - -> Plan will assume client ``, Eppo source environment -> ``, and randomization entity ``. All flags will be -> defaulted to `[ ] Migrate [ ] Skip` (neither pre-checked) — you'll -> opt each one in during review. -> Confirm or change? - -Set the step to `⏸ awaiting user` and stop. Only proceed on an -explicit `yes` / `confirm` / equivalent. A re-run or ambiguous reply -is **not** confirmation. +### Step 4 confirmation gate (Eppo-specific summary) -For each flag in Section 4, generate the MCP command payloads -(createFlag, addFlagToClient, addTargetingRule, resolveFlag) using the -Operator Mapping Reference (below). Write them into each flag's section. +Summarize chosen client + entity + Eppo source environment and ask the +standard confirm question from the core file. Eppo adds one extra item +to summarize: the source environment chosen in Step 1a. **Allocation → targeting-rule order.** Eppo allocations form a waterfall — the first matching allocation wins. Confidence evaluates targeting rules in declared order, so emit one `addTargetingRule` -call per Eppo allocation, in the same order. Rules added later sit -below earlier rules. - -**After all commands generated:** Update Generation Status step 4 to -`✓ complete` and set the overall status to `complete`. Write the -Progress table (Section 5). - -**Tell the user:** -> Plan generated! Review it at `.claude/plans/eppo-flag-migration-.md` -> -> Migration is **opt-in**: every flag starts with both checkboxes -> empty. Tick `[x] Migrate` or `[x] Skip` for each flag — `execute` -> will refuse any flag with neither box set. -> When you're ready, run: `/migrate-eppo execute ` +call per Eppo allocation, in the same order. --- -## Operator Mapping Reference (agent-internal, do NOT show to user) - -This is how Eppo operators map to Confidence targeting payloads. -Use this when generating `addTargetingRule` payloads in the plan file. - -**CRITICAL: Confidence Targeting Payload Format** - -The payload uses a `criteria` + `expression` pattern. Criteria are named -references (`ref-0`, `ref-1`, ...) that define individual conditions. -The `expression` combines them with boolean logic (`and`, `or`, `not`, `ref`). - -```json -{ - "criteria": { - "ref-0": { - "attribute": { - "attributeName": "", - "": { ... } - } - } - }, - "expression": { "ref": "ref-0" } -} -``` - -**DO NOT use nested rule objects like `{"or": {"operands": [{"eqRule": ...}]}}` -at the top level.** That format is silently parsed as empty targeting -(matching ALL contexts) due to `ignoringUnknownFields()` in the proto parser. +## Operator Mapping (Eppo → Confidence) -### Criterion Rules - -| Confidence Rule | Form | -|---|---| -| String eq | `"eqRule": { "value": { "stringValue": "X" } }` | -| Number eq | `"eqRule": { "value": { "numberValue": N } }` | -| Bool eq | `"eqRule": { "value": { "boolValue": true } }` | -| `>=` | `"rangeRule": { "startInclusive": { "numberValue": N } }` | -| `>` | `"rangeRule": { "startExclusive": { "numberValue": N } }` | -| `<` | `"rangeRule": { "endExclusive": { "numberValue": N } }` | -| `<=` | `"rangeRule": { "endInclusive": { "numberValue": N } }` | -| starts with | `"startsWithRule": { "value": "prefix" }` | -| ends with | `"endsWithRule": { "value": "suffix" }` | - -### Expression Combinators - -| Pattern | Expression | -|---------|-----------| -| Single condition | `{ "ref": "ref-0" }` | -| AND | `{ "and": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } }` | -| OR | `{ "or": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } }` | -| NOT | `{ "not": { "ref": "ref-0" } }` | -| NOT IN (list) | `{ "and": { "operands": [{ "not": { "ref": "ref-0" } }, { "not": { "ref": "ref-1" } }] } }` | - -### Eppo Operator Mapping +This is how Eppo operators map to Confidence targeting payloads. The +core file defines the Confidence payload format (criteria + expression, +criterion rules, combinators, examples). This table is the Eppo-side +half. Within a single Eppo rule, all `conditions` are ANDed. Across multiple rules in the same allocation, conditions are ORed (any rule satisfying -means the allocation matches). +means the allocation matches). Across allocations, each Eppo allocation +becomes a **separate Confidence targeting rule** — see the waterfall +ordering note in Step 4 above. -| Eppo operator (typical JSON values: `GT`, `LT`, `GTE`, `LTE`, `MATCHES`, `ONE_OF`, `NOT_ONE_OF`) | Confidence Payload Strategy | +| Eppo operator (`GT`, `LT`, `GTE`, `LTE`, `MATCHES`, `ONE_OF`, `NOT_ONE_OF`) | Confidence payload strategy | |---|---| | `GT` / `>` | One criterion with `rangeRule.startExclusive`, expression: `ref` | | `GTE` / `>=` | One criterion with `rangeRule.startInclusive`, expression: `ref` | @@ -873,140 +387,37 @@ means the allocation matches). | `ONE_OF ["A","B",...]` | One criterion per value with `eqRule`, expression: `or` of `ref`s | | `NOT_ONE_OF ["A"]` (single value) | One criterion with `eqRule`, expression: `not` wrapping `ref` | | `NOT_ONE_OF ["A","B",...]` | One criterion per value with `eqRule`, expression: `and` of `not`-wrapped `ref`s | -| `MATCHES "^prefix.*"` | One criterion with `startsWithRule { value: "prefix" }` | -| `MATCHES ".*suffix$"` | One criterion with `endsWithRule { value: "suffix" }` | +| `MATCHES "^prefix.*"` | One criterion with `startsWithRule { value: "prefix" }`, expression: `ref` | +| `MATCHES ".*suffix$"` | One criterion with `endsWithRule { value: "suffix" }`, expression: `ref` | **Blocked (manual review):** -- `MATCHES` regex that is not a simple prefix/suffix anchor — Confidence + +- **`MATCHES` regex that is not a simple prefix/suffix anchor.** Confidence has no general regex rule. Surface the flag in Section 4 with an explicit `BLOCKED` marker and a brief explanation; the user must either rewrite the rule using set membership / starts-with / ends-with or migrate manually. -- SemVer comparisons — Eppo can compare SemVer strings numerically. +- **SemVer comparisons.** Eppo can compare SemVer strings numerically. Confidence's `rangeRule` is purely numeric. If the attribute type is SemVer, mark the rule `BLOCKED` and ask the user whether to convert the comparison to a numeric `appVersionMajor` / `appVersionMinor` context field, or migrate manually. -- `id` (subject-key) ONE_OF lists of more than ~50 values — Eppo caps - these at 50 but Confidence is fine with larger sets; not blocked, - just noted. - -### AND / OR Combinations - -**Within a rule:** `conditions[]` are ANDed. Create one criterion per -condition and combine them with an `and` expression. -**Across rules in one allocation:** any rule matching is enough. Create -criteria for each rule, combine each rule's sub-expression, then OR the -rule expressions together at the top level. +**Eppo subject `id` targeting** (`id` ONE_OF [...]): rewrite the +`attributeName` from `id` to the chosen entity field name from Step 3 +(e.g. `user_id`). Lists up to ~50 values are fine; Eppo caps them at 50 +but Confidence handles larger sets. -**Across allocations:** these are NOT ORed inside one Confidence rule. -Each Eppo allocation becomes a **separate Confidence targeting rule**, -in the same waterfall order. +### Worked example (waterfall) -### Complete Examples +A two-allocation Eppo flag — internal users gate at 100% treatment, +then a 50/50 experiment on US/CA users — becomes TWO `addTargetingRule` +calls in order: -**Single equality (country = "US"):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "US" } } } } - }, - "expression": { "ref": "ref-0" } -} -``` - -**ONE_OF (country IN [US, UK]):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "US" } } } }, - "ref-1": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "UK" } } } } - }, - "expression": { "or": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } } -} -``` - -**NOT_ONE_OF (country NOT IN [DE, FR]):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "DE" } } } }, - "ref-1": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "FR" } } } } - }, - "expression": { "and": { "operands": [{ "not": { "ref": "ref-0" } }, { "not": { "ref": "ref-1" } }] } } -} -``` +1. Rule 1: `email endsWith @spotify.com` → `treatment` at 100% +2. Rule 2: `country ONE_OF ["US", "CA"]` → `control` 50%, `treatment` 50% -**AND within a rule (plan = "pro" AND country ONE_OF [US, UK]):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "plan", "eqRule": { "value": { "stringValue": "pro" } } } }, - "ref-1": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "US" } } } }, - "ref-2": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "UK" } } } } - }, - "expression": { "and": { "operands": [{ "ref": "ref-0" }, { "or": { "operands": [{ "ref": "ref-1" }, { "ref": "ref-2" }] } }] } } -} -``` - -**Range (age >= 30):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "age", "rangeRule": { "startInclusive": { "numberValue": 30 } } } } - }, - "expression": { "ref": "ref-0" } -} -``` - -**Subject targeting (`id` ONE_OF ["user-1", "user-2"], mapped to `user_id`):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "user_id", "eqRule": { "value": { "stringValue": "user-1" } } } }, - "ref-1": { "attribute": { "attributeName": "user_id", "eqRule": { "value": { "stringValue": "user-2" } } } } - }, - "expression": { "or": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } } -} -``` - -**Two-allocation waterfall (internal users gate, then 50/50 experiment):** - -This becomes TWO separate `addTargetingRule` calls, in order: -1. Rule 1 — `email endsWith @spotify.com` → assigns `treatment` at 100%. -2. Rule 2 — `country ONE_OF ["US", "CA"]` → assigns `control` 50%, `treatment` 50%. - -### Multivariant A/B Split Handling - -**CRITICAL:** A single Confidence targeting rule CAN assign multiple -variants at different split percentages. Use ONE rule per Eppo -allocation, listing all variants and their shares in that rule. - -**How to map Eppo splits to Confidence rules:** - -For a Feature Gate allocation (all matched subjects get one variation): -- Add ONE rule with one variant assignment at 100%. - -For an Experiment allocation with variation weights (e.g. control 50% / -treatment 50%): -- Add ONE rule with two variant assignments: - control at 50%, treatment at 50%. - -For 3+ variants (e.g. control 34% / A 33% / B 33%): -- Add ONE rule with three variant assignments: - control at 34%, A at 33%, B at 33%. - -**Traffic exposure → rule rollout.** Eppo's `trafficExposure` (0–1) on -an allocation maps to the `rolloutPercentage` of the Confidence rule: -e.g. `trafficExposure: 0.5` → `rolloutPercentage: 50`. Subjects who -match the targeting conditions but fall outside the exposure continue -down the waterfall (next rule) in Confidence too. Variant percentages -within the rule control the split among the subjects who DO enter. - -**Do NOT create separate rules per variant.** One targeting rule = -one set of targeting conditions, with the variant split defined -inside that rule. +(See the core file's worked examples for the exact JSON payload shape.) --- @@ -1072,51 +483,33 @@ evaluation context when resolving flags — things like `country`, `plan`, or `appVersion` that targeting rules use to decide who gets what. -Below is a reconciliation of what Eppo flags need vs what already -exists in the Confidence client's schema. - ### Already in Confidence -These fields are already defined in the `` client and match -Eppo targeting attributes. No action needed. - | Field | Type | Entity | Eppo Attribute | |-------|------|--------|----------------| - + ### Need to Create -These fields are used in Eppo targeting rules but don't exist yet -in the Confidence client. They will be created during execution using -`addContextField`. - | Field | Type | Entity | Eppo Attribute | |-------|------|--------|----------------| - + ### Confidence-only (not in Eppo) -These fields exist in Confidence but aren't used by any Eppo flag. -Listed for reference — no action needed. - | Field | Type | Entity | |-------|------|--------| - + --- ## 4. Flags to Migrate -Below are the flags we're planning to migrate, along with their -allocations described in plain language. - **Migration is opt-in.** Each flag starts with both checkboxes empty. Tick `[x] Migrate` for every flag you want to bring across, or `[x] Skip` to drop it. Flags with neither box ticked will be refused by `execute` — no implicit defaults. -During execution, each flag will be created one by one, interactively. - ### Flag: `` **Description:** @@ -1128,12 +521,11 @@ During execution, each flag will be created one by one, interactively. 2. ... **Default value (no allocation matches):** **Confidence entity:** -**Confidence rules:** one targeting rule per allocation (see waterfall in Step 4) +**Confidence rules:** one targeting rule per allocation, in the same order **Action:** [ ] Migrate [ ] Skip **MCP Commands:** - - + --- @@ -1146,147 +538,95 @@ During execution, each flag will be created one by one, interactively. --- -## Execute: How It Works +## Execute: Eppo-Specific Notes -**`execute ` walks through the plan interactively, step by step.** +(The core file defines the execute flow and the Flag Setup Sequence. +This section adds Eppo-specific guidance.) -### For Code Plans +**Disabled-in-environment handling.** If a flag is off in the source +Eppo environment, surface that during execute: -**Each flag = one PR.** The code migration creates a separate pull -request for each flag, keeping changes small and reviewable. +> This flag is OFF in Eppo (). I'll create it in Confidence but +> keep the rules at 0% rollout so it stays inactive until you turn it +> on intentionally. Continue? -``` -1. READ the plan file -2. SDK SETUP (Section 1 of plan) — one-time, before any flag - - Show install command from plan - - ASK: "Install SDK now? [Yes / Skip / I already did]" - - If Yes -> run install command - - Show wrapper file path + API surface from plan - - ASK: "Create the Confidence wrapper now? [Yes / Skip / I already did]" - - If Yes -> create the file using plan's API reference -3. FOR EACH FLAG in the files list: - a. Create a branch: `migrate/-to-confidence` - b. Show flag name + all files using it - c. ASK: "Transform this flag's files? [Yes / Skip / Pause]" - d. If Yes -> apply transform rules from plan to all files for this flag - e. Run lint + typecheck on changed files - f. Commit changes - g. Create PR with title: "feat: migrate from Eppo to Confidence" - h. Show PR link - i. CHECKPOINT: "PR created. [Continue to next flag / Pause]?" - j. Wait for user response -4. COMPLETION - - Show summary: migrated vs skipped - - List all PRs created with links -``` +**Variation type → Confidence schema.** Use the Eppo `variationType` +(`STRING` / `BOOLEAN` / `NUMERIC` / `INTEGER` / `JSON`) as the +Confidence schema type when calling `createFlag`. Include all Eppo +variations as Confidence variants. -### For Flag Plans +**Waterfall verification.** Because Eppo flags often have multiple +allocations, the core file's Flag Setup Sequence Step 4 requires you to +also resolve with a context that misses the first allocation but +matches a later one — this verifies the waterfall order is preserved. -``` -1. READ the plan file - - Client is already in the plan — use it, do NOT re-ask - - Subject entity (randomization unit) is already in the plan - - Eppo source environment is already in the plan - - REFUSE TO PROCEED if any flag has neither `[x] Migrate` nor - `[x] Skip` ticked. List those flags back to the user and ask - them to tick a box for each before re-running execute. Migration - is opt-in — never assume a default. - - REFUSE TO PROCEED if any flag is marked `BLOCKED` and the user - hasn't either resolved the block (rewrote the rule) or ticked - `[x] Skip`. Surface the BLOCKED flags and the reason for each. -2. FOR EACH FLAG marked [x] Migrate: - - Show flag name, description, and allocations in plain English - - If the flag is disabled in the source environment, surface that: - "This flag is OFF in Eppo (). I'll create it in Confidence - but keep the rules at 0% rollout. Continue?" - - ASK: "Create this flag in Confidence? [Yes / Skip / Pause]" - - If Yes -> run the flag setup sequence (see below) - - CHECKPOINT: "Flag done. [Continue / Pause]?" - - Wait for user response -3. COMPLETION - - Show summary: created vs skipped -``` +--- + +## Plan Code: Eppo-Specific Steps + +(Core file defines Steps 1, 2, and 5. Eppo provides Steps 3 and 4.) + +### Plan-file path -**Flag Setup Sequence (MUST complete all steps before resolving):** +`.claude/plans/eppo-code-migration-.md` -Each flag MUST go through these steps in order. Do NOT call -`resolveFlag` until ALL prior steps succeed. +### Step 3: Scan codebase for Eppo usage ``` -STEP 1: createFlag - → Use the variation type from Eppo (STRING/BOOLEAN/NUMERIC/INTEGER/JSON) - as the Confidence schema type. - → Include all variants from Eppo as Confidence variants. - → If flag already exists, check the response for which clients - it's enabled on. - -STEP 2: Ensure flag is active and on the correct client - → If createFlag response does NOT list the target client: - a. Try addFlagToClient - b. If that fails with "Cannot update an archived flag": - → unarchiveFlag first, then retry addFlagToClient - → If createFlag response lists the target client: proceed - -STEP 3: addTargetingRule — ONE per Eppo allocation, in waterfall order - → Add the targeting rules from the plan, in the same order as the - Eppo allocations. Confidence evaluates rules top-down — order is - semantically significant. - → IMPORTANT: targeting rules added while a flag is archived OR - immediately after unarchiving may become inactive. Always complete - steps 1-2 fully (createFlag, unarchive, addFlagToClient) BEFORE - calling addTargetingRule. Do NOT add rules between createFlag and - unarchiveFlag — they will be inactive and you'll have to re-add. - -STEP 4: resolveFlag (verification) - → Only NOW resolve to verify the flag works. - → MUST test BOTH positive AND negative cases: - a. Resolve with a context that SHOULD match the FIRST allocation - → Verify the expected variant is returned - b. Resolve with a context that SHOULD NOT match any allocation - → Verify the default variation / no variant is returned - → If the flag has multiple allocations, also resolve with a context - that misses the first allocation but matches a later one — this - verifies the waterfall order is correct. - → For attribute-based targeting (country, plan, etc.), the resolve - call MUST include those attributes in the evaluation context. - Without them, the targeting conditions cannot be evaluated and - may appear to match when they wouldn't in production. - → If resolve fails with "No active flags found": - something went wrong in steps 1-2 — diagnose, don't skip - → If all rules show "Rule is inactive" / no match: - targeting rules were likely added while flag was archived. - Re-add the targeting rule now that the flag is active. - → Do NOT report a flag as successfully migrated until both - positive and negative resolve tests pass. +Grep: pattern="eppo|Eppo|EppoClient" → Find Eppo imports +Grep: pattern="get_(string|boolean|numeric|integer|json)_assignment|getStringAssignment|getBooleanAssignment|getNumericAssignment|getIntegerAssignment|getJSONAssignment" → Find evaluations ``` -**Why this matters:** Confidence flags can be in states that -`createFlag` won't fix: archived, or enabled for a different client -only. The setup sequence handles all edge cases so resolves never -fail for avoidable reasons. +Common Eppo package names: +- JS/TS: `@eppo/js-client-sdk`, `@eppo/node-server-sdk`, `@eppo/react-native-sdk` +- Python: `eppo-server-sdk` +- Java/Kotlin: `cloud.eppo:eppo-server-sdk` +- Go: `github.com/Eppo-exp/golang-sdk` +- Ruby: `eppo-server-sdk` +- Rust: `eppo_sdk` +- iOS: `eppo-ios-sdk` +- Android: `cloud.eppo:eppo-android-sdk` +- .NET: `Eppo.Sdk` -### Rules +Group files by **flag key** they reference. The flag key is the first +argument to every Eppo `get_*_assignment` call. -- **NEVER auto-continue** -- always wait for user at each checkpoint -- **Flag-by-flag** -- each flag is one unit (its files + tests) -- **Preserve allocation order** -- one Confidence rule per Eppo - allocation, in the same order -- **PR checkpoints** -- offer to create PR after each flag or batch -- **Resumable** -- update Progress table in plan file after each step +For each evaluation site, record: +- Flag key +- Return type (inferred from which `get_*_assignment` variant is used) +- The `subjectKey` argument (so the transform can map it to `targetingKey`) +- The `subjectAttributes` argument (so the transform can carry them + into the evaluation context) +- The `defaultValue` argument (carried over to the Confidence call) ---- +### Step 4: Generate transform rules -## Required Prerequisites Summary +Based on SDK guide from `confidence-docs` MCP: +- Extract install commands +- Extract initialization code +- Extract flag evaluation API +- Generate find/replace rules -### For `plan flags` +**Typed assignment mapping (Eppo → OpenFeature / Confidence):** -| Source | What's used | -|--------|-------------| -| Eppo REST API (`X-Eppo-Token`) | `GET /environments`, `GET /feature-flags`, `GET /feature-flags/{id}`, `GET /feature-flags/{id}/environments/{environmentId}` | -| `confidence` MCP | `listClients`, `getContextSchema`, `createFlag`, `addFlagToClient`, `addContextField`, `addTargetingRule`, `resolveFlag` | +| Eppo call | OpenFeature call | +|-----------|------------------| +| `client.get_string_assignment(k, sk, attrs, default)` | `client.getStringValue(k, default, { targetingKey: sk, ...attrs })` | +| `client.get_boolean_assignment(k, sk, attrs, default)` | `client.getBooleanValue(k, default, { targetingKey: sk, ...attrs })` | +| `client.get_numeric_assignment(k, sk, attrs, default)` | `client.getNumberValue(k, default, { targetingKey: sk, ...attrs })` | +| `client.get_integer_assignment(k, sk, attrs, default)` | `client.getNumberValue(k, default, { targetingKey: sk, ...attrs })` | +| `client.get_json_assignment(k, sk, attrs, default)` | `client.getObjectValue(k, default, { targetingKey: sk, ...attrs })` | -### For `plan code` +Adjust method casing per language based on the MCP-fetched SDK guide. + +--- + +## Required Prerequisites + +(The core file lists the Confidence-side MCPs. This skill adds the Eppo +REST API as documented in the Prerequisites section above — no MCP, just +`curl` with `X-Eppo-Token: $EPPO_API_KEY`.) | Source | What's used | |--------|-------------| -| `confidence-docs` MCP | `getCodeSnippetAndSdkIntegrationTips`, `searchDocumentation`, `getFullSource` | +| Eppo REST API (`X-Eppo-Token`) | `GET /environments`, `GET /feature-flags`, `GET /feature-flags/{id}`, `GET /feature-flags/{id}/environments/{environmentId}` | diff --git a/skills/migrate-posthog/SKILL.md b/skills/migrate-posthog/SKILL.md index 7534d2a..7d8ee30 100644 --- a/skills/migrate-posthog/SKILL.md +++ b/skills/migrate-posthog/SKILL.md @@ -4,29 +4,14 @@ description: Migrate feature flags from PostHog to Confidence SDK. Use when the # PostHog to Confidence Migration -MCP-driven, self-sufficient migration from PostHog to Confidence. - -## Migration Flow - -The migration happens in two phases: **flags first, then code**. - -``` -Phase 1: Flag Definitions - plan flags → Scan PostHog, choose client & entity, generate plan - execute → Create each flag in Confidence with targeting rules - -Phase 2: Code Transformation - plan code → Scan codebase, fetch SDK guide, generate transform rules - execute → Transform code flag by flag, each flag = one PR -``` +> **Read the shared core first.** Before doing anything else, use the +> Read tool to read `skills/_shared/migration-core.md`. That file +> defines all Confidence-side conventions every migration follows — +> payload formats, the flag setup sequence, naming rules, the execute +> flow, etc. THIS file only covers what's specific to PostHog. Apply +> both together. -**Why flags first?** The flags need to exist in Confidence before the -code can resolve them. Once flags are live in Confidence, you migrate -the code that evaluates them — one flag at a time, one PR at a time. - -**Each code PR is scoped to a single flag.** This keeps PRs small, -reviewable, and independently shippable. If one flag's migration has -issues, it doesn't block the others. +MCP-driven, self-sufficient migration from PostHog to Confidence. ## Commands @@ -104,35 +89,10 @@ Then proceed with the normal workflow for that phase. --- -## SDK Preference - -**ALWAYS prefer OpenFeature with local resolve.** - -| Priority | Approach | When to use | -|----------|----------|-------------| -| 1st | Local resolve | Default for all new integrations | -| 2nd | Remote resolve | Only if local resolve not supported for platform | -| Avoid | Direct SDK | Being phased out | - ---- - -## Plan Philosophy +## Prerequisites: PostHog Side -**Plans must be MCP-boxed, self-sufficient, and agent-agnostic.** - -| Principle | Meaning | -|-----------|---------| -| **MCP-boxed** | Every external data fetch uses explicit MCP tool calls | -| **Self-sufficient** | Plan contains ALL information needed - no "query MCP for X" | -| **Agent-agnostic** | Any agent with MCPs can execute without prior context | -| **Language-agnostic** | Detect framework, fetch SDK guide from MCP dynamically | - ---- - -## Prerequisites - -Before starting any workflow, check that required MCP servers are available. -Try calling a simple tool from each. If it fails, install the missing MCP. +(The core file documents the Confidence-side prerequisites — install +`confidence` and `confidence-docs` MCP servers.) ### PostHog MCP @@ -146,49 +106,17 @@ claude mcp add posthog --transport http --url https://mcp-eu.posthog.com/mcp The user will be prompted to authenticate via OAuth in their browser. For US-based PostHog projects, use `https://mcp.posthog.com/mcp` instead. -### Confidence MCP - -Test: `mcp__confidence__listClients` - -If not available, install it: -``` -claude mcp add confidence --transport http --url https://mcp.confidence.dev/mcp/flags -``` - -The user will be prompted to authenticate via OAuth in their browser. - -### Confidence Docs MCP (for `plan code` only) - -Test: `mcp__confidence-docs__searchDocumentation` - -If not available, install it: -``` -claude mcp add confidence-docs --transport http --url https://mcp.confidence.dev/mcp/docs -``` - --- -## User-Facing Communication Rules - -**NEVER expose internal technical details to the user.** The user should see -human-readable descriptions of what's happening, not internal implementation -details like targeting payload formats, rule types, or operator names. - -- Do NOT say "creating plan based on eqRule / rangeRule / setRule" etc. -- Do NOT show raw targeting payloads or JSON structures in conversation -- DO say things like: "Creating flag with rule: plan equals 'pro' AND country is US or UK" -- DO describe rules in plain English: "age between 18 and 65", "plan is not free" -- The plan FILE may contain MCP command payloads (for machine execution), - but conversation output must be human-friendly +## Step Trackers -**Step Tracker:** Display a visual step tracker at every phase transition. -The tracker shows all phases, marks completed ones, highlights the current -one, and shows remaining ones. Update and re-display it each time you move -to a new phase. +(The core file defines the marker legend, progress-bar conventions, and +final-summary format. This section just declares the layouts specific +to this skill.) -### Plan Flags Step Tracker +### Plan Flags step tracker -Display this at the START and after EACH step completes (updating status): +Display at the START and after EACH step completes: ``` ───── Plan Flags ────────────────────────────────────────── @@ -199,22 +127,6 @@ Display this at the START and after EACH step completes (updating status): ──────────────────────────────────────────────────────────── ``` -Status markers: -- `○ pending` — not started yet -- `◉ in progress` — currently running -- `⏸ awaiting user` — blocked on user input (e.g. picking a client or entity) -- `✓ done` — completed (add brief user-facing result) -- `⊘ skipped` — skipped by user - -Use `⏸ awaiting user` whenever the workflow has asked a question and is -waiting for an explicit reply. This makes "I'm blocked on you" visible -to both agent and user, and prevents the agent from drifting into -auto-progression while a question is open. - -**IMPORTANT:** Never expose internal/technical details in the tracker. -No pagination info, no API page counts, no internal field names. -Show only what matters to the user. - Example after Step 1 completes: ``` ───── Plan Flags ────────────────────────────────────────── @@ -225,9 +137,7 @@ Example after Step 1 completes: ──────────────────────────────────────────────────────────── ``` -### Execute Step Tracker - -Display this at the START and update after EACH flag: +### Execute step tracker ``` ───── Execute Migration ─────────────────────────────────── @@ -236,243 +146,20 @@ Display this at the START and update after EACH flag: ──────────────────────────────────────────────────────────── ``` -Update the progress bar as flags are processed. Use `█` for completed -and `░` for remaining. The bar should be 20 characters wide. - -Examples at various stages: -``` - Progress: [██████░░░░░░░░░░░░░░] 5/15 (1 skipped) - Current: complex-deployment-and-version -``` - -``` - Progress: [████████████████████] 15/15 done - Result: 14 migrated, 1 skipped -``` - -After each flag completes, show: -``` - ✓ simple-usage-limit — MATCH (enabled) -``` - -After a skip: -``` - ⊘ simple-new-onboarding — skipped -``` - -### Final Summary (Execute) - -At the end of execution, show a complete summary: - -``` -───── Migration Complete ────────────────────────────────── - Progress: [████████████████████] 15/15 done - Migrated: 14 | Skipped: 1 | Failed: 0 - - ✓ simple-usage-limit 100% user_id - ✓ simple-ai-features 100% user_id - ⊘ simple-new-onboarding — skipped - ✓ simple-dark-mode 25% user_id - ... -──────────────────────────────────────────────────────────── -``` - ---- - -## Confidence Naming Rules - -- **Flag names:** lowercase letters, digits, and hyphens only (`[a-z0-9-]`) -- **Entity references:** Confidence entity names do NOT support underscores. - The entity reference (e.g. `entities/company`) is separate from the context - field name (e.g. `company_id`). When creating entity fields with - `addContextField`, always provide an explicit `entityReference` with a - clean name (no underscores). If omitted, the tool auto-generates one from - the field name which will fail. - - | Field name | Entity reference | Works? | - |------------|-----------------|--------| - | `user_id` | `entities/user` | Yes | - | `company_id` | `entities/company` | Yes | - | `visitor_id` | `entities/visitor` | Yes | - | `company_id` | *(omitted — auto: `entities/company_id`)* | **No** | - ---- - -## Plan Code: Workflow - -### Resume Check (MUST do first) - -Same as Plan Flag: check for existing `.claude/plans/posthog-code-migration-*.md`. -If found with incomplete `Generation Status`, resume from the last -incomplete step. If complete, ask user if they want to start fresh. -If not found, start fresh. - -The plan file uses the same progressive pattern: created at Step 1, -updated after each step, with a `## Generation Status` section. - -### Step 1: Detect Language & Framework - -``` -Grep: pattern="posthog|PostHog" -> Find PostHog usage -Glob: pattern="package.json" or "build.gradle" or "Cargo.toml" etc -Read: dependency file -> Determine language/framework -``` - -### Step 2: Fetch SDK Guide from MCP - -**Query confidence-docs MCP based on detected language:** - -``` -mcp__confidence-docs__getCodeSnippetAndSdkIntegrationTips - sdk: "" -``` - -``` -mcp__confidence-docs__searchDocumentation - query: "OpenFeature local resolve " -``` - -``` -mcp__confidence-docs__getFullSource - source: "https://confidence.spotify.com/docs/sdks/server/" -``` - -**CRITICAL:** Include the ACTUAL response in the plan, not a reference to fetch it. - -### Step 3: Scan Codebase for PostHog Usage - -``` -Grep: pattern="" -> Find all usages -``` - -Group files by flag constant they reference. - -### Step 4: Generate Transform Rules - -Based on SDK guide from MCP: -- Extract install commands -- Extract initialization code -- Extract flag evaluation API -- Generate find/replace rules matching PostHog -> Confidence patterns - -### Step 5: Generate Plan - -Save to `.claude/plans/posthog-code-migration-.md` - ---- - -## Plan Code: Template - -```markdown -# PostHog to Confidence Code Migration Plan - -**Created:** -**Scope:** Code transformation only -**Language:** -**Framework:** - ---- - -## 1. SDK Setup - -### Install - - - -### API Reference (from MCP: confidence-docs) - - - -### Create Confidence Wrapper - -**File:** - -**Must match PostHog API surface:** - -| Method | Signature | -|--------|-----------| - - ---- - -## 2. Transform Rules - -### Source Files - -| Find | Replace | -|------|---------| -| | | -| | | - -### Test Files - -| Find | Replace | -|------|---------| -| | | - ---- - -## 3. Files to Transform - - - ---- - -## 4. Progress - -| # | Item | Status | -|---|------|--------| -| 0 | SDK Setup | :white_circle: | - -``` - --- -## Plan Flag: Workflow - -### Resume Check (MUST do first) - -Before starting, check for an existing in-progress plan: - -``` -Glob: .claude/plans/posthog-flag-migration-*.md -``` - -If a plan file exists, read its `## Generation Status` section: -- If status is `complete` → tell user a plan already exists, ask if - they want to start fresh or use the existing one -- If status is NOT `complete` → **resume from the last incomplete step** - Tell the user: "Found an in-progress plan. Resuming from step ." -- If no plan file exists → start fresh +## Plan Flag: PostHog-Specific Steps -### Progressive Plan File +The core file defines the workflow shape (Step 1 scan, Step 2 client +selection, Step 3 randomization mapping, Step 4 generate). This +section provides the PostHog-specific implementations of steps 1 and +3, and the operator mapping table needed by step 4. -The plan file is created at the START (Step 1) and updated after EACH -step. This means if the session closes, the file has partial progress -that can be resumed. +### Plan-file path -**File path:** `.claude/plans/posthog-flag-migration-.md` +`.claude/plans/posthog-flag-migration-.md` -The plan file MUST include a `## Generation Status` section at the top -(right after the title) that tracks which steps are done: - -```markdown -## Generation Status - -| Step | Status | Result | -|------|--------|--------| -| 1. Scan PostHog | ✓ complete | 15 flags | -| 2. Choose client | ✓ complete | test | -| 3. Map entities | ○ not started | | -| 4. Generate rules | ○ not started | | -``` - -Status values: `✓ complete`, `◉ in progress`, `○ not started` - -**After each step completes**, update the status table AND write that -step's data to the plan file. Do NOT wait until the end to write. - -### Step 1: Scan PostHog Flags +### Step 1: Scan PostHog flags **CRITICAL: Paginate until ALL flags are fetched.** @@ -519,53 +206,7 @@ Group the flags by bucketing method: - **Per-group flags** (aggregation_group_type_index) — will each use their group identifier directly -**After scan completes:** Update Generation Status step 1 to `✓ complete`. - -### Step 2: Select Confidence Client - -``` -mcp__confidence__listClients -``` - -**EDUCATE then ASK the user:** - -> **What is a client?** -> A client represents the application that resolves flags — your website, -> backend service, or mobile app. Each client has its own secret for -> authentication and can be scoped to environments (dev, staging, prod). -> Flags are associated with one or more clients, so Confidence knows which -> application should receive which flags. -> -> Think of it like: "Where will these flags be evaluated?" -> -> Your existing clients: -> 1. -> 2. -> ... -> N. Create a new client -> -> Which client should I use as the default for all flags? -> You can always rearrange them later in the Confidence UI. - -**Wait for an explicit pick.** Set the step to `⏸ awaiting user` and -stop. A re-run of `/migrate-posthog`, an empty message, or any reply -that is not a number from the list / `new ` is **not** consent — -NEVER infer the recommendation from silence. If the reply is ambiguous, -re-ask, listing the choices again. - -- If user picks existing -> use it -- If user wants new -> ASK for name -> `mcp__confidence__createClient` - -**After client selected:** Write Section 1 (Default Client) to plan -file and update Generation Status step 2 to `✓ complete`. - -### Step 3: Map Randomization Units - -``` -mcp__confidence__getContextSchema clientName: "" -``` - -Show the user entity fields (fields marked as entity in the schema). +### Step 3: Map randomization units (PostHog-specific) This step maps PostHog's bucketing identifiers to Confidence entity fields. @@ -598,12 +239,11 @@ This step maps PostHog's bucketing identifiers to Confidence entity fields. > > Which Confidence field represents the same user as `distinct_id`? -**Wait for an explicit pick.** Same rule as Step 2 — set the step to -`⏸ awaiting user` and stop. Silence, a re-run, or any non-listed reply -is **not** consent. Re-ask if the reply is ambiguous. +Same wait-for-explicit-pick rule as Step 2 in the core file. Silence is +not consent. -- If user picks existing -> use it as `targetingKey` for all per-user flags -- If user wants new -> ASK for name + type -> `mcp__confidence__addContextField` +- If user picks existing → use it as `targetingKey` for all per-user flags +- If user wants new → ASK for name + type → `mcp__confidence__addContextField` **For per-group flags (PostHog `aggregation_group_type_index`):** @@ -615,228 +255,47 @@ If any flags randomize per group, inform the user: > — I'll carry them over as-is. If the group identifier doesn't exist in the Confidence context schema, -create it with `mcp__confidence__addContextField`. See **Confidence -Naming Rules** above — always provide an explicit `entityReference` -(e.g. `entities/company` for a field named `company_id`). - -**Step 3 only creates entity fields** (the per-user entity, plus any -group identifiers from per-group flags). Attribute fields used in -targeting rules (`plan`, `country`, `age`, etc.) MUST NOT be created -here. Record them in Section 3 "Need to Create" and let `execute` -create them — that way, if the user later skips a flag, no orphan -schema fields are left in Confidence. - -**After entity mapped:** Write Section 2 (Randomization Mapping) to -plan file, reconcile and write Section 3 (Context Schema), and update -Generation Status step 3 to `✓ complete`. - -### Step 4: Generate MCP Commands - -**Confirmation gate (MUST pass before generating).** Before writing -Section 4, summarize chosen client + entity in chat and ask: - -> Plan will assume client `` with randomization entity -> ``. All flags will be defaulted to `[ ] Migrate [ ] Skip` -> (neither pre-checked) — you'll opt each one in during review. -> Confirm or change? +create it with `mcp__confidence__addContextField`. Always provide an +explicit `entityReference` (e.g. `entities/company` for a field named +`company_id`) — see the Confidence Naming Rules in the core file. -Set the step to `⏸ awaiting user` and stop. Only proceed on an -explicit `yes` / `confirm` / equivalent. A re-run or ambiguous reply -is **not** confirmation. +### Step 4 confirmation gate -For each flag in Section 4, generate the MCP command payloads -(createFlag, addFlagToClient, addTargetingRule, resolveFlag) using the -Operator Mapping Reference (below). Write them into each flag's section. - -**After all commands generated:** Update Generation Status step 4 to -`✓ complete` and set the overall status to `complete`. Write the -Progress table (Section 5). - -**Tell the user:** -> Plan generated! Review it at `.claude/plans/posthog-flag-migration-.md` -> -> Migration is **opt-in**: every flag starts with both checkboxes -> empty. Tick `[x] Migrate` or `[x] Skip` for each flag — `execute` -> will refuse any flag with neither box set. -> When you're ready, run: `/migrate-posthog execute ` +Summarize chosen client + entity and ask the standard confirm question +from the core file. --- -## Operator Mapping Reference (agent-internal, do NOT show to user) - -This is how PostHog operators map to Confidence targeting payloads. -Use this when generating `addTargetingRule` payloads in the plan file. - -**CRITICAL: Confidence Targeting Payload Format** - -The payload uses a `criteria` + `expression` pattern. Criteria are named -references (`ref-0`, `ref-1`, ...) that define individual conditions. -The `expression` combines them with boolean logic (`and`, `or`, `not`, `ref`). - -```json -{ - "criteria": { - "ref-0": { - "attribute": { - "attributeName": "", - "": { ... } - } - } - }, - "expression": { "ref": "ref-0" } -} -``` +## Operator Mapping (PostHog → Confidence) -**DO NOT use nested rule objects like `{"or": {"operands": [{"eqRule": ...}]}}` -at the top level.** That format is silently parsed as empty targeting -(matching ALL contexts) due to `ignoringUnknownFields()` in the proto parser. - -### Criterion Rules - -| PostHog | Confidence Criterion | -|---------|---------------------| -| `exact: "X"` | `"eqRule": { "value": { "stringValue": "X" } }` | -| `exact: N` (number) | `"eqRule": { "value": { "numberValue": N } }` | -| `exact: true/false` | `"eqRule": { "value": { "boolValue": true } }` | -| `gte: N` | `"rangeRule": { "startInclusive": { "numberValue": N } }` | -| `gt: N` | `"rangeRule": { "startExclusive": { "numberValue": N } }` | -| `lt: N` | `"rangeRule": { "endExclusive": { "numberValue": N } }` | -| `lte: N` | `"rangeRule": { "endInclusive": { "numberValue": N } }` | -| `regex: ^prefix.*` | `"startsWithRule": { "value": "prefix" }` | -| `regex: .*suffix$` | `"endsWithRule": { "value": "suffix" }` | - -### Expression Combinators - -| Pattern | Expression | -|---------|-----------| -| Single condition | `{ "ref": "ref-0" }` | -| AND | `{ "and": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } }` | -| OR | `{ "or": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } }` | -| NOT | `{ "not": { "ref": "ref-0" } }` | -| NOT IN (list) | `{ "and": { "operands": [{ "not": { "ref": "ref-0" } }, { "not": { "ref": "ref-1" } }] } }` | - -### PostHog Operator Mapping - -| PostHog | Confidence Payload Strategy | -|---------|---------------------------| +This is how PostHog operators map to Confidence targeting payloads. The +core file defines the Confidence payload format (criteria + expression, +criterion rules, combinators, examples). This table is the PostHog-side +half. + +| PostHog | Confidence payload strategy | +|---------|----------------------------| | `exact: "X"` | One criterion with `eqRule`, expression: `ref` | | `is_not: "X"` | One criterion with `eqRule`, expression: `not` wrapping `ref` | | `exact: ["A","B"]` | One criterion per value with `eqRule`, expression: `or` of `ref`s | | `is_not: ["A","B"]` | One criterion per value with `eqRule`, expression: `and` of `not`-wrapped `ref`s | -| `gte: N` | One criterion with `rangeRule`, expression: `ref` | -| `regex: ^prefix.*` | One criterion with `startsWithRule`, expression: `ref` | -| `regex: .*suffix$` | One criterion with `endsWithRule`, expression: `ref` | - -**Blocked (manual review):** `icontains`, `is_not_set`, cohort targeting - -### AND / OR Combinations - -**AND conditions:** All properties within one PostHog group are ANDed. -Create one criterion per condition, combine with `and` expression. - -**Multiple groups (OR):** PostHog groups are ORed. Create criteria for -each group, combine group expressions with `or`. - -### Complete Examples - -**Single equality (country = "US"):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "US" } } } } - }, - "expression": { "ref": "ref-0" } -} -``` - -**IN operator (country IN [US, UK]):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "US" } } } }, - "ref-1": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "UK" } } } } - }, - "expression": { "or": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } } -} -``` - -**NOT IN (country NOT IN [DE, FR]):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "DE" } } } }, - "ref-1": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "FR" } } } } - }, - "expression": { "and": { "operands": [{ "not": { "ref": "ref-0" } }, { "not": { "ref": "ref-1" } }] } } -} -``` - -**AND (plan = "pro" AND country IN [US, UK]):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "plan", "eqRule": { "value": { "stringValue": "pro" } } } }, - "ref-1": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "US" } } } }, - "ref-2": { "attribute": { "attributeName": "country", "eqRule": { "value": { "stringValue": "UK" } } } } - }, - "expression": { "and": { "operands": [{ "ref": "ref-0" }, { "or": { "operands": [{ "ref": "ref-1" }, { "ref": "ref-2" }] } }] } } -} -``` - -**Range (age >= 30):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "age", "rangeRule": { "startInclusive": { "numberValue": 30 } } } } - }, - "expression": { "ref": "ref-0" } -} -``` - -**Ends with (email ends with @spotify.com OR @gmail.com):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "email", "endsWithRule": { "value": "@spotify.com" } } }, - "ref-1": { "attribute": { "attributeName": "email", "endsWithRule": { "value": "@gmail.com" } } } - }, - "expression": { "or": { "operands": [{ "ref": "ref-0" }, { "ref": "ref-1" }] } } -} -``` - -**Starts with (utm_source starts with "email-"):** -```json -{ - "criteria": { - "ref-0": { "attribute": { "attributeName": "utm_source", "startsWithRule": { "value": "email-" } } } - }, - "expression": { "ref": "ref-0" } -} -``` - -### Multivariant A/B Split Handling - -**CRITICAL:** A single Confidence targeting rule CAN assign multiple -variants at different split percentages. Use ONE rule per targeting -condition, listing all variants and their shares in that rule. - -**How to map PostHog splits to Confidence rules:** - -For a 2-variant flag (e.g. control 50% / treatment 50%): -- Add ONE rule with two variant assignments: - control at 50%, treatment at 50%. - -For a 3+ variant flag (e.g. control 34% / A 33% / B 33%): -- Add ONE rule with three variant assignments: - control at 34%, A at 33%, B at 33%. - -**Do NOT create separate rules per variant.** One targeting rule = -one set of targeting conditions, with the variant split defined -inside that rule. The `rolloutPercentage` on the rule controls -what fraction of users who match the targeting conditions enter the -rule at all (use 100% unless you want a partial rollout on top of -the targeting). The variant percentages within the rule control the -split among those who enter. +| `gte: N` | One criterion with `rangeRule.startInclusive`, expression: `ref` | +| `gt: N` | One criterion with `rangeRule.startExclusive`, expression: `ref` | +| `lt: N` | One criterion with `rangeRule.endExclusive`, expression: `ref` | +| `lte: N` | One criterion with `rangeRule.endInclusive`, expression: `ref` | +| `regex: ^prefix.*` | One criterion with `startsWithRule { value: "prefix" }`, expression: `ref` | +| `regex: .*suffix$` | One criterion with `endsWithRule { value: "suffix" }`, expression: `ref` | + +**AND / OR combinations:** +- All properties within one PostHog group are ANDed → use `and` expression +- Multiple PostHog groups are ORed → each group becomes a sub-expression, + combined with `or` at the top level + +**Blocked (manual review):** `icontains`, `is_not_set`, cohort targeting, +and any regex that is not a simple prefix anchor (`^prefix.*`) or suffix +anchor (`.*suffix$`). Surface blocked flags in the plan with an explicit +`BLOCKED` marker and a brief explanation; `execute` will refuse to +proceed until the user either resolves the block or ticks `[x] Skip`. --- @@ -909,46 +368,31 @@ exists in the Confidence client's schema. ### Already in Confidence -These fields are already defined in the `` client and match -PostHog targeting properties. No action needed. - | Field | Type | Entity | PostHog Property | |-------|------|--------|------------------| - + ### Need to Create -These fields are used in PostHog targeting rules but don't exist yet -in the Confidence client. They will be created during execution using -`addContextField`. - | Field | Type | Entity | PostHog Property | |-------|------|--------|------------------| - + ### Confidence-only (not in PostHog) -These fields exist in Confidence but aren't used by any PostHog flag. -Listed for reference — no action needed. - | Field | Type | Entity | |-------|------|--------| - + --- ## 4. Flags to Migrate -Below are the flags we're planning to migrate, along with their -targeting rules described in plain language. - **Migration is opt-in.** Each flag starts with both checkboxes empty. Tick `[x] Migrate` for every flag you want to bring across, or `[x] Skip` to drop it. Flags with neither box ticked will be refused by `execute` — no implicit defaults. -During execution, each flag will be created one by one, interactively. - ### Flag: `` **Description:** @@ -957,12 +401,11 @@ During execution, each flag will be created one by one, interactively. **Variants:** **PostHog bucketing:** <"distinct_id (per user)" or "group type (per company/group)"> **Confidence entity:** -**Confidence rollout:** +**Confidence rollout:** **Action:** [ ] Migrate [ ] Skip **MCP Commands:** - - + --- @@ -971,138 +414,67 @@ During execution, each flag will be created one by one, interactively. | # | Flag | Status | |---|------|--------| | 1 | | :white_circle: | - ``` --- -## Execute: How It Works +## Plan Code: PostHog-Specific Steps + +(Core file defines Steps 1, 2, and 5. PostHog provides Steps 3 and 4.) -**`execute ` walks through the plan interactively, step by step.** +### Plan-file path -### For Code Plans +`.claude/plans/posthog-code-migration-.md` -**Each flag = one PR.** The code migration creates a separate pull -request for each flag, keeping changes small and reviewable. +### Step 3: Scan codebase for PostHog usage ``` -1. READ the plan file -2. SDK SETUP (Section 1 of plan) — one-time, before any flag - - Show install command from plan - - ASK: "Install SDK now? [Yes / Skip / I already did]" - - If Yes -> run install command - - Show wrapper file path + API surface from plan - - ASK: "Create the Confidence wrapper now? [Yes / Skip / I already did]" - - If Yes -> create the file using plan's API reference -3. FOR EACH FLAG in the files list: - a. Create a branch: `migrate/-to-confidence` - b. Show flag name + all files using it - c. ASK: "Transform this flag's files? [Yes / Skip / Pause]" - d. If Yes -> apply transform rules from plan to all files for this flag - e. Run lint + typecheck on changed files - f. Commit changes - g. Create PR with title: "feat: migrate from PostHog to Confidence" - h. Show PR link - i. CHECKPOINT: "PR created. [Continue to next flag / Pause]?" - j. Wait for user response -4. COMPLETION - - Show summary: migrated vs skipped - - List all PRs created with links +Grep: pattern="posthog|PostHog" → Find PostHog import lines +Grep: pattern="isFeatureEnabled|getFeatureFlag|getFeatureFlagPayload" → Find evaluations ``` -### For Flag Plans +Common PostHog package names: +- JS/TS: `posthog-js`, `posthog-node`, `posthog-react-native` +- Python: `posthog` +- Java/Kotlin: `com.posthog:posthog-java` +- Go: `github.com/posthog/posthog-go` +- Ruby: `posthog-ruby` +- .NET: `PostHog` -``` -1. READ the plan file - - Client is already in the plan — use it, do NOT re-ask - - Entity (randomization unit) is already in the plan as the default - - For flags where PostHog's bucketing_identifier is NOT distinct_id: - use whatever PostHog uses as the targetingKey for that flag - (e.g. if PostHog uses company_id, use company_id in Confidence too) - - REFUSE TO PROCEED if any flag has neither `[x] Migrate` nor - `[x] Skip` ticked. List those flags back to the user and ask - them to tick a box for each before re-running execute. Migration - is opt-in — never assume a default. -2. FOR EACH FLAG marked [x] Migrate: - - Show flag name, description, and rules in plain English - - ASK: "Create this flag in Confidence? [Yes / Skip / Pause]" - - If Yes -> run the flag setup sequence (see below) - - CHECKPOINT: "Flag done. [Continue / Pause]?" - - Wait for user response -3. COMPLETION - - Show summary: created vs skipped -``` +Group files by **flag key** they reference. The flag key is the first +argument to `posthog.isFeatureEnabled(...)` and similar calls. -**Flag Setup Sequence (MUST complete all steps before resolving):** +For each evaluation site, record: +- Flag key +- The `distinctId` argument (so the transform can map it to `targetingKey`) +- Any `groups` / `personProperties` / `groupProperties` arguments (so + the transform can carry them into the evaluation context) +- The default / fallback value (carried over to the Confidence call) -Each flag MUST go through these steps in order. Do NOT call -`resolveFlag` until ALL prior steps succeed. +### Step 4: Generate transform rules -``` -STEP 1: createFlag - → If flag already exists, check the response for which clients - it's enabled on. - -STEP 2: Ensure flag is active and on the correct client - → If createFlag response does NOT list the target client: - a. Try addFlagToClient - b. If that fails with "Cannot update an archived flag": - → unarchiveFlag first, then retry addFlagToClient - → If createFlag response lists the target client: proceed - -STEP 3: addTargetingRule - → Add the targeting rule from the plan - → IMPORTANT: targeting rules added while a flag is archived OR - immediately after unarchiving may become inactive. Always complete - steps 1-2 fully (createFlag, unarchive, addFlagToClient) BEFORE - calling addTargetingRule. Do NOT add rules between createFlag and - unarchiveFlag — they will be inactive and you'll have to re-add. - -STEP 4: resolveFlag (verification) - → Only NOW resolve to verify the flag works - → MUST test BOTH positive AND negative cases: - a. Resolve with a context that SHOULD match the targeting rule - → Verify the expected variant is returned - b. Resolve with a context that SHOULD NOT match - → Verify no variant / default is returned - → For attribute-based targeting (country, plan, etc.), the resolve - call MUST include those attributes in the evaluation context. - Without them, the targeting conditions cannot be evaluated and - may appear to match when they wouldn't in production. - → If resolve fails with "No active flags found": - something went wrong in steps 1-2 — diagnose, don't skip - → If all rules show "Rule is inactive" / no match: - targeting rules were likely added while flag was archived. - Re-add the targeting rule now that the flag is active. - → Do NOT report a flag as successfully migrated until both - positive and negative resolve tests pass. -``` +Based on SDK guide from `confidence-docs` MCP: +- Extract install commands +- Extract initialization code +- Extract flag evaluation API +- Generate find/replace rules -**Why this matters:** Confidence flags can be in states that -`createFlag` won't fix: archived, or enabled for a different client -only. The setup sequence handles all edge cases so resolves never -fail for avoidable reasons. +**Typical mapping (PostHog → OpenFeature / Confidence):** -### Rules +| PostHog call | OpenFeature call | +|--------------|------------------| +| `posthog.isFeatureEnabled('key', distinctId)` | `client.getBooleanValue('key', false, { targetingKey: distinctId })` | +| `posthog.getFeatureFlag('key', distinctId)` | `client.getStringValue('key', '', { targetingKey: distinctId })` | +| `posthog.getFeatureFlagPayload('key', distinctId)` | `client.getObjectValue('key', {}, { targetingKey: distinctId })` | -- **NEVER auto-continue** -- always wait for user at each checkpoint -- **Flag-by-flag** -- each flag is one unit (its files + tests) -- **PR checkpoints** -- offer to create PR after each flag or batch -- **Resumable** -- update Progress table in plan file after each step +Adjust method casing per language based on the MCP-fetched SDK guide. --- ## Required MCPs -### For `plan code` - -| MCP | Tools Used | -|-----|------------| -| `confidence-docs` | `getCodeSnippetAndSdkIntegrationTips`, `searchDocumentation`, `getFullSource` | - -### For `plan flag` +(The core file lists the Confidence-side MCPs. This skill adds:) | MCP | Tools Used | |-----|------------| | `posthog` | `feature-flag-get-all`, `feature-flag-get-definition` | -| `confidence` | `listClients`, `getContextSchema`, `createFlag`, `addTargetingRule`, `resolveFlag` |