一键导入
一键导入
Build the project for testing
Build an individual Swift package for testing
Build an individual Swift package
Build the project
Format code with swiftlint and swiftformat
Lint code with swiftlint and swiftformat
| name | sync-feature-flags |
| description | Sync feature flags from Statsig gates to code |
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