feat: add launchdarkly-flag-drift skill

Detect and reconcile drift between a feature flag's in-code SDK fallback
default and its LaunchDarkly default rule (fallthrough), updating only the
default argument without removing the flag or changing its evaluation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ramon Niebla
2026-07-22 12:24:10 -07:00
parent 22a5175622
commit 81230170da
7 changed files with 468 additions and 0 deletions
+1
View File
@@ -16,6 +16,7 @@ Agent Skills are modular, text-based playbooks that teach an agent how to perfor
| `feature-flags/launchdarkly-flag-create` | Create new feature flags in a way that fits existing codebase patterns |
| `feature-flags/launchdarkly-flag-targeting` | Control targeting, rollouts, rules, and cross-environment config |
| `feature-flags/launchdarkly-flag-cleanup` | Safely remove flags from code using LaunchDarkly as the source of truth |
| `feature-flags/launchdarkly-flag-drift` | Detect and reconcile drift between an in-code SDK fallback default and the LaunchDarkly default rule |
| `feature-flags/launchdarkly-segment-create` | Create reusable audience segments with targeting rules or individual targets, and wire them into flags |
### AI Configs
+19
View File
@@ -100,6 +100,25 @@
"mcp"
]
},
{
"name": "launchdarkly-flag-drift",
"description": "Detect and reconcile drift between a feature flag's in-code SDK fallback default and its LaunchDarkly default rule (fallthrough). Use when a flag's default rule changed, when the user asks to detect flag drift, check whether a hardcoded default still matches LaunchDarkly, sync an in-code default, or open a PR reconciling a fallback value, without removing the flag or its evaluation.",
"path": "skills/feature-flags/launchdarkly-flag-drift",
"version": "0.1.0",
"license": "Apache-2.0",
"compatibility": "Requires the remotely hosted LaunchDarkly MCP server",
"tags": [
"launchdarkly",
"feature-flags",
"feature-management",
"flag-drift",
"default-drift",
"sdk",
"reconciliation",
"devops",
"mcp"
]
},
{
"name": "launchdarkly-flag-targeting",
"description": "Control LaunchDarkly feature flag targeting including toggling flags on/off, percentage rollouts, targeting rules, individual targets, and copying flag configurations between environments. Use when the user wants to change who sees a flag, roll out to a percentage, add targeting rules, or promote config between environments.",
@@ -0,0 +1,66 @@
# LaunchDarkly Flag Drift Skill
An Agent Skill for detecting and reconciling drift between a feature flag's in-code SDK fallback default and its LaunchDarkly default rule, keeping outage behavior consistent with production.
## Overview
This skill teaches agents how to:
- Resolve a flag's current default rule (fallthrough) value from LaunchDarkly
- Locate the fallback default argument in every SDK evaluation and declaration in code
- Compare the two and detect drift
- Reconcile only the in-code default when it has drifted, without removing the flag or changing its evaluation
- Open a well-scoped pull request that documents the change
## Installation (Local)
For now, install by placing this skill directory where your agent client loads skills.
Examples:
- **Generic**: copy `skills/feature-flags/launchdarkly-flag-drift/` into your client's skills path
## Prerequisites
This skill requires the remotely hosted LaunchDarkly MCP server to be configured in your environment. The remote server provides higher-level, agent-optimized tools that orchestrate multiple API calls and return pruned, actionable responses.
Refer to your LaunchDarkly account settings for instructions on connecting to the remotely hosted MCP server.
## Usage
Once installed, the skill activates automatically when you ask about flag default drift:
```
The default rule for `new-checkout-flow` changed in production. Check if the code default drifted
```
```
Does the hardcoded default for `dark-mode` still match LaunchDarkly?
```
```
Open a PR to sync the in-code default for `enable-new-billing` with its fallthrough
```
## Structure
```
launchdarkly-flag-drift/
├── SKILL.md
├── marketplace.json
├── README.md
└── references/
├── sdk-default-patterns.md
└── pr-template.md
```
## Related
- [LaunchDarkly Flag Cleanup](../launchdarkly-flag-cleanup/SKILL.md): Remove a flag from code entirely
- [LaunchDarkly Flag Targeting](../launchdarkly-flag-targeting/SKILL.md): Change the default rule in LaunchDarkly instead of the code
- [LaunchDarkly MCP Server](https://github.com/launchdarkly/mcp-server)
- [LaunchDarkly Docs](https://docs.launchdarkly.com)
- [Agent Skills Specification](https://agentskills.io/specification)
## License
Apache-2.0
@@ -0,0 +1,144 @@
---
name: launchdarkly-flag-drift
description: "Detect and reconcile drift between a feature flag's in-code SDK fallback default and its LaunchDarkly default rule (fallthrough). Use when a flag's default rule changed, when the user asks to detect flag drift, check whether a hardcoded default still matches LaunchDarkly, sync an in-code default, or open a PR reconciling a fallback value, without removing the flag or its evaluation."
license: Apache-2.0
compatibility: Requires the remotely hosted LaunchDarkly MCP server
metadata:
author: launchdarkly
version: "0.1.0"
---
# LaunchDarkly Flag Drift Detection
You're using a skill that will guide you through checking whether a feature flag's **in-code SDK fallback default** has drifted from its **LaunchDarkly default rule (fallthrough)**, and reconciling the code if it has. Your job is to determine the flag's current default rule value from LaunchDarkly, locate the default argument passed to every SDK evaluation in code, compare them, and, only when they differ, update the in-code default so it matches. You never remove the flag or change its evaluation logic.
## Prerequisites
This skill requires the remotely hosted LaunchDarkly MCP server to be configured in your environment.
**Required MCP tools:**
- `get-flag`: fetch the flag's configuration in a specific environment (fallthrough, variations, offVariation)
**Optional MCP tools:**
- `list-flags`: find the flag key if the user only described the flag by name
## Core Concept: The SDK Fallback Default Is Not the Off Variation
Every SDK evaluation call takes a **fallback default**: the value returned when LaunchDarkly is unreachable, the client is uninitialized, or the flag is unavailable. This is a code-side safety value, distinct from the flag's `offVariation` (served when the flag is toggled off) and from its `fallthrough` (the default rule served when no targeting rule matches).
This skill treats one specific invariant as "correct": **the in-code fallback default should match the current default rule (fallthrough) value** for the source environment. When they diverge, that's *drift*. Reconciling it keeps the value returned during an outage consistent with what most users would otherwise receive.
Some teams intentionally keep the fallback as a conservative/off value instead. Treat a mismatch as a finding to surface, not always an automatic edit. See Edge Cases.
## Workflow
### Step 1: Identify the Flag and Source Environment
1. **Get the flag key.** If the user described the flag by name, use `list-flags` to resolve the key. Confirm before proceeding.
2. **Confirm the environment.** The fallthrough is environment-specific; a default rule that changed in `production` may differ in `staging`. Always confirm which environment is the source of truth. If not specified, ask rather than assume.
### Step 2: Determine the Expected Default from LaunchDarkly
Use `get-flag` for the flag key and source environment. Read:
- `fallthrough`: the default rule. If it points to a single `variation` (an index), that's your expected value. If it's a percentage `rollout`, there is **no single default value**, so stop and handle as an edge case.
- `variations`: map `fallthrough.variation` (the index) to `variations[index].value`. This resolved value is the **expected default**.
- `offVariation` and flag type: useful context for the comparison and for spotting type mismatches.
Never guess the fallthrough value. Always resolve it from `get-flag`.
### Step 3: Locate Every In-Code Default
Find every place the flag is evaluated in code and, critically, the **default/fallback argument** passed to the SDK call. Search for the flag key across the codebase, then identify the default in each hit.
- The default is typically the **last positional argument** to `variation(...)` / `*Variation(...)` calls (e.g. `boolVariation("<key>", context, <default>)`).
- Teams often wrap the SDK. Check wrapper/registry/config layers that declare a default once per flag, annotation- or struct-tag-based defaults, and generated default files.
See [SDK Default Patterns](references/sdk-default-patterns.md) for the full set of patterns by language and abstraction, and how to distinguish the default argument from the context argument.
### Step 4: Compare and Decide
Normalize both sides before comparing (see Edge Cases for JSON/number/type notes), then:
| Result | Action |
|--------|--------|
| In-code default **matches** the expected default | **No drift.** Report `drift_detected: false` and stop. Do not open a PR. |
| In-code default **differs** | **Drift detected.** Proceed to Step 5 to reconcile. |
| Multiple evaluations with **different** in-code defaults | Drift. Reconcile all of them to the expected value and note the prior inconsistency. |
### Step 5: Reconcile the In-Code Default
Update **only** the default/fallback argument so it matches the expected value.
- Do **not** change evaluation logic, branching, or off-path behavior.
- Do **not** remove the flag or its evaluation.
- If the default lives in a **generated file**, do not hand-edit it. Update the source of truth (the constructor/registry/annotation) and regenerate using the project's codegen command. Note `requires_generation: true` in your summary.
**Example (before then after), expected default = `true`:**
```typescript
// Before: outage returns false even though the default rule now serves true
const enabled = await ldClient.variation('new-checkout-flow', context, false);
// After: fallback default reconciled to match the fallthrough
const enabled = await ldClient.variation('new-checkout-flow', context, true);
```
The surrounding `if (enabled) { ... } else { ... }` branching is left untouched.
### Step 6: Validate Before Committing
Run the project's configured checks scoped to the changed files. Discover them from `package.json` scripts, a `Makefile`, `AGENTS.md`/`CLAUDE.md`/`CONTRIBUTING.md`, or the CI config. Typically: format, lint, type-check, build, and the relevant tests.
**Hard stop:** if any check fails and you cannot fix it, do not commit or push. Narrow your change instead. Never ship code that fails format/lint/type-check/build/test.
### Step 7: Open the Pull Request
Only after validation passes. Use [references/pr-template.md](references/pr-template.md). The description must state: the flag key, the source environment, the old vs new in-code default, and that **only** the SDK fallback default changed (the flag and its evaluation are preserved).
- Follow the repository's contribution conventions (branch naming, commit style). Check `AGENTS.md`/`CLAUDE.md`/`CONTRIBUTING.md` first.
- A clear default: branch `fix/flag-default-drift-<flag-key>`, commit `fix: sync in-code default for <flag-key> to match fallthrough`.
### Step 8: Report a Structured Summary
Produce a concise summary with these fields:
```
flag_key: <key>
drift_detected: true | false
old_default: <in-code value before, or n/a>
new_default: <expected value / value written, or n/a>
files_modified: [<paths>]
pr_url: <url or null>
requires_generation: true | false
notes: <anything the reviewer should know>
```
## Edge Cases
| Situation | Action |
|-----------|--------|
| Fallthrough is a **percentage rollout** | There is no single default value. Report the split, do not auto-edit, and ask the user which value the fallback should represent. |
| Fallback appears **intentionally conservative** (matches `offVariation` or a safe value) | Surface the mismatch and confirm intent before changing. Some teams keep the fallback as a safe value on purpose. |
| In-code default lives in a **generated file** | Edit the source of truth and regenerate; never hand-edit generated output. Set `requires_generation: true`. |
| **Type mismatch** between code default and the variation's type | Flag as a bug in the PR/summary; the default and variation types should agree. |
| **JSON / object / float** defaults | Compare by normalized value, not string form (`{"a":1}` == `{ "a": 1 }`, `0` == `0.0`). |
| Flag **not found** or wrong environment | Inform the user; check for typos in the key and confirm the environment. |
| **Dynamic flag keys** (`flag-${id}`) | Automated detection may be incomplete; flag for manual review. |
| Environments **disagree** on the fallthrough | The fallback can only match one. Confirm which environment is the source of truth. |
| Flag spans **multiple repositories** | This skill operates on the current repo. Note other repos that also reference the key so they can be reconciled separately. |
## What NOT to Do
- Don't remove the flag or its evaluation; this skill only touches the default argument.
- Don't change evaluation logic, branching, or off-path behavior beyond the fallback default.
- Don't hand-edit generated files; regenerate from the source of truth.
- Don't open a PR when there is no drift.
- Don't guess the fallthrough value; resolve it from `get-flag`.
- Don't ship code that fails format, lint, type-check, build, or tests.
## References
- [SDK Default Patterns](references/sdk-default-patterns.md): Where the fallback default lives by language, wrapper, annotation, and generated-file pattern; how to find it
- [PR Template](references/pr-template.md): Structured PR description for a drift reconciliation
- [Flag Cleanup](../launchdarkly-flag-cleanup/SKILL.md): If the goal is to remove the flag entirely, not reconcile its default
- [Flag Targeting](../launchdarkly-flag-targeting/SKILL.md): If the goal is to change the default rule in LaunchDarkly instead of the code
@@ -0,0 +1,22 @@
{
"name": "launchdarkly-flag-drift",
"description": "Detect and reconcile drift between an in-code SDK fallback default and a LaunchDarkly default rule",
"version": "0.1.0",
"author": "LaunchDarkly",
"repository": "https://github.com/launchdarkly/agent-skills",
"skills": ["./"],
"tags": [
"launchdarkly",
"feature-flags",
"feature-management",
"flag-drift",
"default-drift",
"sdk",
"reconciliation",
"devops",
"mcp"
],
"requirements": {
"mcp-servers": ["@launchdarkly/mcp-server"]
}
}
@@ -0,0 +1,72 @@
# PR Template for Flag Default Drift
Use this template when opening a pull request that reconciles an in-code SDK fallback default with the LaunchDarkly default rule (fallthrough). The change is intentionally narrow: only the default argument moves. The flag and its evaluation stay.
```markdown
## Sync in-code default: `{flag-key}`
### Summary
- **Flag**: `{flag-key}`
- **Source environment**: `{environment}`
- **Old in-code default**: `{old value}`
- **New in-code default**: `{new value}` (matches current default rule / fallthrough)
- **Scope**: Only the SDK fallback default changed. The flag and its evaluation are preserved.
### Why
The flag's default rule (fallthrough) in `{environment}` serves `{new value}`, but the
hardcoded fallback in code returned `{old value}` when LaunchDarkly is unreachable.
This drift meant an outage would serve a different value than normal operation. This PR
reconciles the fallback so the outage value matches the default rule.
### Changes
- Files modified: `{list files}`
- Occurrences updated: `{count}`
- Requires code generation: `{yes/no}` {if yes, note the command run, e.g. `make generate`}
### Not changed
- Flag evaluation and branching logic
- Off-path behavior (`offVariation`)
- The flag itself (not removed, not archived)
### Reviewer checklist
- [ ] New default matches the fallthrough value from `get-flag` for `{environment}`
- [ ] Only the default argument changed (no logic/branching edits)
- [ ] Default type matches the flag's variation type
- [ ] Generated files (if any) were regenerated, not hand-edited
- [ ] Format, lint, type-check, build, and tests pass
```
## Example
```markdown
## Sync in-code default: `new-checkout-flow`
### Summary
- **Flag**: `new-checkout-flow`
- **Source environment**: `production`
- **Old in-code default**: `false`
- **New in-code default**: `true` (matches current default rule / fallthrough)
- **Scope**: Only the SDK fallback default changed. The flag and its evaluation are preserved.
### Why
The default rule in `production` now serves `true`, but the code fallback returned `false`
during outages. This PR reconciles the fallback to `true` so behavior is consistent when
LaunchDarkly is unreachable.
### Changes
- Files modified: `CheckoutService.ts`
- Occurrences updated: 1
- Requires code generation: no
### Not changed
- The `if (enabled) { renderNew() } else { renderOld() }` branching
- `offVariation` behavior
- The flag itself
### Reviewer checklist
- [x] New default matches the fallthrough value from `get-flag` for `production`
- [x] Only the default argument changed (no logic/branching edits)
- [x] Default type matches the flag's variation type
- [x] No generated files involved
- [x] Format, lint, type-check, build, and tests pass
```
@@ -0,0 +1,144 @@
# SDK Default Patterns Reference
How to find the **fallback default**: the value the SDK returns when LaunchDarkly is unreachable, uninitialized, or the flag is unavailable. This is the argument this skill compares against the flag's default rule (fallthrough) and, on drift, the only thing it changes.
## How to Spot the Default Argument
In a direct SDK evaluation, the arguments are usually: **flag key**, **context/user**, **default**. The default is almost always the **last positional argument**. Do not confuse it with the context.
```text
ldClient.<typed>Variation( "<flag-key>", <context>, <DEFAULT> )
^ key ^ context ^ the value this skill checks
```
The default's type should match the flag's variation type (bool flag → boolean default, string flag → string default, etc.). A type mismatch is a bug worth flagging.
## Direct Evaluation by Language
### JavaScript / TypeScript (Node & client)
```typescript
ldClient.variation('flag-key', context, defaultValue);
ldClient.boolVariation('flag-key', context, false); // default = false
ldClient.stringVariation('flag-key', context, 'control'); // default = 'control'
ldClient.numberVariation('flag-key', context, 0);
ldClient.jsonVariation('flag-key', context, {});
ldClient.variationDetail('flag-key', context, defaultValue);
```
React SDK note: `useFlags()` reads already-evaluated values and does not expose a per-call default. The default for those flags is set where `LDProvider` / `asyncWithLDProvider` is configured (`flags` bootstrap / default map). Check the provider setup, not the call site.
### Python
```python
ld_client.variation('flag-key', context, default_value)
ld_client.bool_variation('flag-key', context, False)
ld_client.string_variation('flag-key', context, 'control')
ld_client.int_variation('flag-key', context, 0)
ld_client.variation_detail('flag-key', context, default_value)
```
### Go
```go
ldClient.BoolVariation("flag-key", context, false) // default = false
ldClient.StringVariation("flag-key", context, "control")
ldClient.IntVariation("flag-key", context, 0)
ldClient.JSONVariation("flag-key", context, ldvalue.Null())
ldClient.BoolVariationDetail("flag-key", context, false)
```
### Java / Kotlin
```java
ldClient.boolVariation("flag-key", context, false);
ldClient.stringVariation("flag-key", context, "control");
ldClient.intVariation("flag-key", context, 0);
ldClient.jsonValueVariation("flag-key", context, LDValue.ofNull());
```
### Ruby
```ruby
ld_client.variation('flag-key', context, default_value)
ld_client.bool_variation('flag-key', context, false)
ld_client.string_variation('flag-key', context, 'control')
```
### .NET (C#)
```csharp
ldClient.BoolVariation("flag-key", context, false);
ldClient.StringVariation("flag-key", context, "control");
ldClient.IntVariation("flag-key", context, 0);
ldClient.JsonVariation("flag-key", context, LdValue.Null);
```
## Abstraction Patterns (where the default is declared once)
Many teams don't call the SDK directly at each site. They declare the default in a central place, then read the flag by key elsewhere. When present, the default in these declarations is the value to check and reconcile.
### Wrapper / service method
```typescript
featureFlags.getBool('flag-key', false); // default = false
featureFlags.getValue('flag-key', 'control');
```
Open the wrapper implementation to confirm how the passed value flows into the underlying `variation(...)` call.
### Registry / config map (one default per flag)
```typescript
// A central flag registry
const flags = {
'new-checkout-flow': createFlag('new-checkout-flow', /* default */ false),
};
```
```yaml
# A config file of flag defaults
feature_flags:
new-checkout-flow: false
```
### Annotation / struct-tag defaults
Some typed languages encode the default in metadata rather than an argument.
```go
type Flags struct {
NewCheckoutFlow bool `ld:"new-checkout-flow,false"` // default = false
}
```
```java
@FeatureFlag(key = "new-checkout-flow", defaultValue = "false")
boolean newCheckoutFlow;
```
Reconcile the value inside the annotation/tag, not a downstream copy of it.
### Generated default files
When flag defaults are compiled into a generated file (e.g. an auto-generated defaults map or typed accessor), **do not hand-edit the generated output**. Change the source of truth (the registry, annotation, or codegen input) and re-run the project's generation step. Mark `requires_generation: true` in the summary.
## Search Strategy
Search for the flag key with several forms, since codebases mix conventions:
```bash
# Exact key, both quote styles
rg "'flag-key'" ; rg '"flag-key"'
# camelCase accessor (kebab keys often surface as camelCase in code)
rg "flagKey"
# Wrapper / registry / config usage and constants
rg "flag-key|flagKey|FLAG_KEY"
# Likely declaration sites
rg "flag-key" -g '*flags*' -g '*.constants.*' -g '*config*'
```
For each hit, decide whether it is: a direct evaluation (default = last arg), a declaration (default = the declared value), or a read of an already-declared flag (trace back to the declaration). Reconcile at the declaration; read sites need no change.