.claude/skills/sync-feature-flags/SKILL.md
Sync feature flags from Statsig gates to code
npx skillsauth add adamayoung/popcorn sync-feature-flagsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Syncs Statsig feature gates (source of truth) to FeatureFlag.swift in code. Handles adding new flags, updating changed names/descriptions, and removing deleted flags with full cascading cleanup.
This skill syncs Statsig → Code (Statsig is the source of truth). For the reverse direction — creating a new Statsig gate when adding a feature flag in code — see the "Feature Flag Creation Pattern" in plan-feature/references/patterns.md. New gates should be created with mcp__statsig__Create_Gate, enabled for development environment only.
| File | Role |
|------|------|
| Platform/FeatureAccess/Sources/FeatureAccess/Models/FeatureFlag.swift | Static flag definitions + allFlags array |
| Platform/FeatureAccess/Tests/FeatureAccessTests/FeatureFlagTests.swift | Count assertion + ID arguments list |
| Features/*/Sources/*/*.swift | Client, Feature, and View files that consume flags |
| Features/*/Tests/*/*.swift | Feature flag tests (*FeatureFlagsTests.swift, *ClientTests.swift) |
| Adapters/Contexts/*/Sources/*/*.swift | Adapter FeatureFlagProvider implementations |
| Adapters/Contexts/*/Tests/*/*.swift | Adapter provider tests |
| App/Features/AppRoot/AppRootClient.swift | Root client with navigation-level flags |
| App/Features/AppRoot/AppRootFeature.swift | Root reducer reading navigation-level flags |
| PopcornUITests/FeatureFlag.swift | UI test feature flag enum (mirrors flag IDs as String raw values) |
Statsig gate IDs use snake_case. Swift properties use camelCase.
| Statsig Gate ID | Swift Property | Swift Name |
|-----------------|---------------|------------|
| media_search | .mediaSearch | "Media Search" |
| explore_discover_movies | .exploreDiscoverMovies | "Explore Discover Movies" |
| plot_remix_game | .plotRemixGame | "Plot Remix Game" |
| tv_series_intelligence | .tvSeriesIntelligence | "TV Series Intelligence" |
Conversion rules:
_, capitalize each word after the first → camelCase property name_, capitalize each word → Title Case nameexplore → .explore, "Explore")Verify the Statsig MCP server is available by checking for mcp__statsig__Get_List_of_Gates in the tool list. If unavailable, tell the user:
"The Statsig MCP server is not available. Please configure it and try again."
Stop here if unavailable.
Call mcp__statsig__Get_List_of_Gates to retrieve all feature gates. Extract id, name, and description from each gate.
Read Platform/FeatureAccess/Sources/FeatureAccess/Models/FeatureFlag.swift and parse:
static let property: extract id, name, descriptionallFlags array entriesMatch gates to code flags by id. Categorize into three groups:
| Category | Condition |
|----------|-----------|
| New | Gate ID exists in Statsig but not in code |
| Updated | Gate ID exists in both but name or description differ |
| Removed | Flag ID exists in code but not in Statsig |
Present the diff to the user before making any changes:
Feature Flag Sync Report:
New (3):
+ media_search_v2 — "Media Search V2" — "Controls access to Media Search V2"
+ cast_details — "Cast Details" — "Controls access to Cast Details"
+ ...
Updated (1):
~ explore — name: "Explore" → "Explore Tab"
Removed (1):
- backdrop_focal_point — "Backdrop Focal Point"
Proceed with sync?
If all three categories are empty, report "Feature flags are already in sync." and stop.
For new flags, add to FeatureFlag.swift:
static let property at the end of the last extension (before the closing }):/// Controls access to <name>.
static let <camelCaseProperty> = FeatureFlag(
id: "<snake_case_id>",
name: "<Name from Statsig>",
description: "<Description from Statsig>"
)
.<camelCaseProperty> to the allFlags arrayEmpty Statsig descriptions: If a gate has no description, generate one: "Controls access to <name>"
For updated flags, edit the existing static let to match the new name and/or description from Statsig. Also update the doc comment if the description changed.
Update PopcornUITests/FeatureFlag.swift to mirror changes:
case with camelCase name and snake_case raw value, in the matching MARK sectioncaseFeatureFlag.swiftFor each removed flag, perform a full impact search before asking for confirmation.
For a flag named .myFlag with ID my_flag, search for:
# Direct flag references
rg "\.myFlag" -- find .myFlag references in code
rg "isMyFlagEnabled" -- find Client/State/View properties
rg "my_flag" -- find ID string references in tests
Show the user every file and usage that will be affected:
Removing flag: .myFlag ("my_flag")
Affected files:
Client: Features/SomeFeature/Sources/.../SomeClient.swift
- var isMyFlagEnabled: @Sendable () throws -> Bool
- liveValue: isMyFlagEnabled closure
- previewValue: isMyFlagEnabled closure
Reducer: Features/SomeFeature/Sources/.../SomeFeature.swift
- State.isMyFlagEnabled property
- State.init parameter
- .updateFeatureFlags case
View: Features/SomeFeature/Sources/.../SomeView.swift
- if store.isMyFlagEnabled { ... }
Tests: Features/SomeFeature/Tests/.../SomeFeatureFeatureFlagsTests.swift
- All test methods referencing isMyFlagEnabled
Adapter: Adapters/.../FeatureFlagProvider.swift (if applicable)
- isMyFlagEnabled() method
Confirm removal of .myFlag? (y/n)
When the user confirms removal of a flag, remove in this order:
static let property and allFlags entry*Client.swift):
var is<Flag>Enabled property from the structliveValuepreviewValue@Dependency(\.featureFlags) var featureFlags line*Feature.swift):
State.is<Flag>Enabled property and its init parameter.updateFeatureFlags caseguard state.is<Flag>Enabled checks and their associated logic*View.swift):
if store.is<Flag>Enabled { ... } conditionals*FeatureFlagsTests.swift, *ClientTests.swift):
FeatureFlagProviderFeatureFlagProviding protocolAfter all additions, updates, and removals:
#expect(FeatureFlag.allFlags.count == <new_count>)
allFlags array — same order, same IDs.After all changes are applied, present a summary:
Feature Flag Sync Complete:
Added: 3 flags
Updated: 1 flag
Removed: 1 flag
Files modified:
- Platform/FeatureAccess/Sources/FeatureAccess/Models/FeatureFlag.swift
- Platform/FeatureAccess/Tests/FeatureAccessTests/FeatureFlagTests.swift
- ... (list other modified files)
After all changes:
Remember to run the pre-PR checklist:
/format,/lint,/build,/test
Do NOT run these automatically — let the user invoke them.
// In @DependencyClient struct:
var is<Flag>Enabled: @Sendable () throws -> Bool
// In liveValue:
is<Flag>Enabled: {
featureFlags.isEnabled(.<flagProperty>)
}
// In previewValue:
is<Flag>Enabled: { true }
// State:
var is<Flag>Enabled: Bool // default false in init
// Action:
case updateFeatureFlags
// Reduce:
case .updateFeatureFlags:
state.is<Flag>Enabled = (try? client.is<Flag>Enabled()) ?? false
return .none
if store.is<Flag>Enabled {
// Conditional UI
}
Four required test cases per feature:
truefalsefalsetrue// Domain protocol:
public protocol FeatureFlagProviding: Sendable {
func is<Flag>Enabled() throws(FeatureFlagProviderError) -> Bool
}
// Adapter:
func is<Flag>Enabled() throws(FeatureFlagProviderError) -> Bool {
featureFlags.isEnabled(.<flagProperty>)
}
To find all usage of a flag with property name myFlag and ID my_flag:
# Static property references (Client liveValue, Adapter providers)
rg "\.myFlag\b"
# Client/State/View property references
rg "isMyFlagEnabled"
# String ID references (tests, FeatureFlag.swift)
rg '"my_flag"'
# FeatureFlagProviding protocol methods (adapters)
rg "isMyFlagEnabled.*throws"
$ARGUMENTS
data-ai
Add properties to an existing domain model from TMDb
testing
Run all unit tests
testing
Run UI tests
testing
Run snapshot tests