Install
openclaw skills install cm-feature-toggle-managerAudit, manage, and clean up feature flags across codebases to reduce toggle debt
openclaw skills install cm-feature-toggle-managerManage the full lifecycle of feature flags and toggles across a codebase. This skill teaches an AI agent to discover all toggle usage, detect stale or orphaned flags that should be removed, plan safe cleanup operations, and evaluate overall toggle debt. Supports LaunchDarkly, Unleash, environment-variable-based, and custom in-code implementations.
Use when: "audit feature flags", "find stale toggles", "clean up feature flags", "toggle debt", "feature flag lifecycle", "orphaned flags", "flag rollout plan"
audit -- Find all toggles in code and check stalenessDiscover every feature flag reference in the codebase, classify each by type and status, and produce a staleness report.
Determine which toggle system is in use by scanning for known SDK imports and common patterns.
# Check for LaunchDarkly SDK
rg -l "launchdarkly|ldclient|LDClient|useFlags|withLDConsumer" --type-add 'code:*.{js,ts,tsx,py,go,java,rb,cs}' -t code .
# Check for Unleash SDK
rg -l "unleash-client|Unleash|isEnabled|unleash_client" --type-add 'code:*.{js,ts,tsx,py,go,java,rb}' -t code .
# Check for environment-variable-based flags
rg "FEATURE_|ENABLE_|FF_|TOGGLE_|FLAG_" --type-add 'code:*.{js,ts,py,go,java,env,yaml,yml,toml}' -t code .
# Check for custom toggle files (JSON/YAML config)
rg -l "featureFlags|feature_flags|toggles|feature.toggles" . -g '*.{json,yaml,yml,toml}'
Build a structured list of every distinct flag name and where it appears.
# Extract flag names from LaunchDarkly variation calls
rg -o "variation\(['\"]([^'\"]+)['\"]" -r '$1' --no-filename . | sort -u
# Extract flag names from Unleash isEnabled calls
rg -o "isEnabled\(['\"]([^'\"]+)['\"]" -r '$1' --no-filename . | sort -u
# Extract environment-variable flags
rg -o "(FEATURE_[A-Z_]+|ENABLE_[A-Z_]+|FF_[A-Z_]+)" --no-filename . | sort -u
# For each flag, count references to gauge how embedded it is
for flag in $(rg -o "variation\(['\"]([^'\"]+)['\"]" -r '$1' --no-filename . | sort -u); do
count=$(rg -c "$flag" -l . 2>/dev/null | wc -l)
echo "$count files -- $flag"
done | sort -rn
A flag is potentially stale if its last meaningful change was long ago and it has been consistently returning one value.
# For each flag, find the date it was last modified in any file
flag="my-feature-flag"
git log -1 --format="%ai" -S "$flag" -- '*.ts' '*.js' '*.py' '*.go'
# Bulk staleness check: flags not touched in 90+ days
threshold=$(date -d '90 days ago' +%s)
for flag in $(rg -o "variation\(['\"]([^'\"]+)['\"]" -r '$1' --no-filename . | sort -u); do
last_date=$(git log -1 --format="%at" -S "$flag" 2>/dev/null)
if [ -n "$last_date" ] && [ "$last_date" -lt "$threshold" ]; then
human_date=$(date -d "@$last_date" +%Y-%m-%d)
echo "STALE ($human_date) -- $flag"
fi
done
# LaunchDarkly API: list flags and their current status
curl -s -H "Authorization: $LD_API_KEY" \
"https://app.launchdarkly.com/api/v2/flags/$LD_PROJECT_KEY" | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
for f in data.get('items', []):
status = 'ON' if f['on'] else 'OFF'
age_days = (json.loads('{\"now\":0}')['now']) # placeholder
print(f\"{status:>3} {f['key']} (envs: {len(f.get('environments', {}))})\" )
"
# Unleash API: list all toggles
curl -s -H "Authorization: $UNLEASH_API_TOKEN" \
"$UNLEASH_URL/api/admin/features" | python3 -c "
import sys, json
data = json.load(sys.stdin)
for f in data.get('features', []):
stale = 'STALE' if f.get('stale') else 'active'
print(f\"{stale:>6} {f['name']} type={f.get('type','unknown')}\")
"
## Feature Toggle Audit Report
**Date:** YYYY-MM-DD
**Repository:** repo-name
**Toggle system:** LaunchDarkly / Unleash / env-vars / custom
### Summary
- Total unique flags found: N
- Active (touched in last 90 days): N
- Stale (untouched 90+ days): N
- Orphaned (in code but not in service): N
- Phantom (in service but not in code): N
### Flag Inventory
| Flag Name | Files | Last Modified | Service Status | Recommendation |
|-----------|-------|---------------|----------------|----------------|
| flag-name | 12 | 2025-11-03 | ON (100%) | REMOVE (fully rolled out) |
### Toggle Debt Score
- Debt score: N/10 (stale_count * 2 + orphan_count * 3, capped at 10)
- Recommended action: [cleanup sprint / urgent removal / acceptable]
cleanup -- Identify removable flags and generate removal planFor each stale or fully-rolled-out flag, trace every code path it touches and produce a safe removal plan.
flag="old-feature-flag"
# Find every file referencing this flag
rg -l "$flag" .
# Show the actual usage context (5 lines around each match)
rg -C 5 "$flag" .
# Identify conditional branches that would collapse
rg -n "if.*$flag|$flag.*\?" . --type-add 'code:*.{js,ts,tsx,py,go,java}' -t code
Reason about which branch to keep. If the flag is ON everywhere (fully rolled out), keep the "enabled" branch. If OFF everywhere, keep the "disabled" branch (or delete the feature entirely).
# Analysis helper: parse toggle usage and determine kept branch
import ast, sys
flag_name = sys.argv[1] if len(sys.argv) > 1 else "old-feature-flag"
# For Python codebases, find if/else blocks guarded by the flag
# and output which branch (if-body or else-body) to keep
print(f"Flag: {flag_name}")
print(f"Action: Keep the ENABLED branch (flag was fully rolled out)")
print(f"Files to modify: [list from rg output]")
print(f"Tests to update: [list test files referencing flag]")
# Check if the flag is referenced in infrastructure configs
rg "$flag" . -g '*.{yaml,yml,json,tf,hcl,toml}'
# Check CI/CD pipelines
rg "$flag" . -g '{Jenkinsfile,*.groovy,.github/workflows/*,*.gitlab-ci.yml,Dockerfile*}'
# Check documentation
rg "$flag" . -g '*.{md,rst,txt,adoc}'
For each flag targeted for cleanup, produce this checklist:
## Cleanup Plan: [flag-name]
**Current state:** ON in all environments for 120+ days
**Kept branch:** enabled (remove the conditional, keep the if-body)
**Risk:** LOW (no external config dependencies)
### Steps
1. [ ] Remove flag evaluation calls in: [file list]
2. [ ] Collapse conditional branches -- keep enabled path
3. [ ] Remove flag from toggle service configuration
4. [ ] Remove flag from environment variable definitions
5. [ ] Update or remove tests that mock this flag
6. [ ] Remove flag from documentation references
7. [ ] Verify build passes: `npm test` / `pytest` / `go test ./...`
8. [ ] Deploy to staging and verify behavior unchanged
9. [ ] Remove flag from LaunchDarkly/Unleash dashboard
### Files to modify (N total)
- `src/components/Feature.tsx` (lines 23, 45)
- `src/api/handler.py` (lines 112-130)
- `tests/test_feature.py` (lines 8, 34, 67)
lifecycle -- Plan flag rollout stagesDesign a phased rollout plan for a new feature flag, including percentage ramps, monitoring checkpoints, and full-rollout criteria.
Feature flags have different lifecycle expectations based on type:
import json
def generate_rollout_plan(flag_name, flag_type, total_users_estimate):
stages = []
if flag_type == "release":
stages = [
{"stage": "Internal", "percentage": 0, "audience": "employees only", "duration": "2-3 days", "gate": "no P0/P1 errors in logs"},
{"stage": "Canary", "percentage": 5, "audience": "random 5%", "duration": "3-5 days", "gate": "error rate < baseline + 0.1%"},
{"stage": "Early adopters", "percentage": 25, "audience": "random 25%", "duration": "3-5 days", "gate": "latency p99 within SLO"},
{"stage": "Broad", "percentage": 50, "audience": "random 50%", "duration": "2-3 days", "gate": "no support ticket spike"},
{"stage": "Full", "percentage": 100, "audience": "all users", "duration": "7 days before flag removal", "gate": "stable, schedule cleanup"},
]
elif flag_type == "experiment":
stages = [
{"stage": "Control setup", "percentage": 0, "audience": "none", "duration": "1 day", "gate": "metrics pipeline verified"},
{"stage": "Experiment", "percentage": 50, "audience": "random 50% (treatment vs control)", "duration": "2-4 weeks", "gate": "statistical significance reached"},
{"stage": "Decision", "percentage": "0 or 100", "audience": "based on results", "duration": "1 day", "gate": "team decision logged"},
]
print(json.dumps({"flag": flag_name, "type": flag_type, "stages": stages}, indent=2))
generate_rollout_plan("new-checkout-flow", "release", 50000)
# Verify metrics exist for the flag (Datadog example)
curl -s "https://api.datadoghq.com/api/v1/query?query=avg:app.feature_flag.evaluation{flag_name:new-checkout-flow}" \
-H "DD-API-KEY: $DD_API_KEY" -H "DD-APPLICATION-KEY: $DD_APP_KEY"
# Check error rates segmented by flag state (Prometheus/Grafana)
# PromQL: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m])
# filtered by: feature_flag="new-checkout-flow", flag_state="enabled"
Every flag should have an owner and a kill date.
## Lifecycle Plan: [flag-name]
**Type:** Release toggle
**Owner:** @engineer-name
**Created:** YYYY-MM-DD
**Expected full rollout:** YYYY-MM-DD
**Hard expiry (must remove by):** YYYY-MM-DD (created + 60 days)
### Rollout Schedule
| Stage | % | Audience | Duration | Gate Criteria |
|-------|---|----------|----------|---------------|
| Internal | 0% (allowlist) | Employees | 2-3 days | No P0/P1 |
| Canary | 5% | Random | 3-5 days | Error < +0.1% |
| Broad | 50% | Random | 3 days | p99 within SLO |
| Full | 100% | All | 7 days | Schedule removal |
### Rollback trigger
- Error rate exceeds baseline by > 0.5%
- p99 latency exceeds SLO
- Any P0 incident attributed to this change
### Cleanup deadline
Flag must be removed within 14 days of reaching 100%. If not removed by [hard expiry], it enters the stale backlog and accrues toggle debt.