Feat: Add skill scaffolding and Flag cleanup skill (#1)

This commit is contained in:
Ramon Niebla
2026-02-03 07:43:05 -08:00
committed by GitHub
parent bb869ffa63
commit d67914a5ca
24 changed files with 1501 additions and 2 deletions
+1
View File
@@ -0,0 +1 @@
* @launchdarkly/team-fm-next
+22
View File
@@ -0,0 +1,22 @@
---
name: Bug report
about: Report a problem with an agent skill
labels: bug
---
## Summary
## Steps to Reproduce
1.
2.
## Expected
## Actual
## Environment
- Agent client:
- Skill name:
- Version or commit:
+13
View File
@@ -0,0 +1,13 @@
---
name: Feature request
about: Propose a new skill or enhancement
labels: enhancement
---
## Summary
## Problem Statement
## Proposed Solution
## Alternatives Considered
+12
View File
@@ -0,0 +1,12 @@
## Summary
-
## Testing
- [ ] Not applicable
- [ ] Manual (describe below)
## Notes
-
+66
View File
@@ -0,0 +1,66 @@
name: Release Skills
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: "Version tag (e.g., v1.0.0)"
required: true
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
else
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
- name: Create skill zips
run: |
mkdir -p dist/skills
find skills -name "SKILL.md" | while read skill_file; do
skill_dir=$(dirname "$skill_file")
skill_name=$(basename "$skill_dir")
zip_name="${skill_name}.zip"
(cd "$skill_dir/.." && zip -r "../../dist/skills/$zip_name" "$skill_name")
echo "Created: dist/skills/$zip_name"
done
- name: Generate release notes
run: |
echo "## Skills" >> release_notes.md
echo "" >> release_notes.md
for zip in dist/skills/*.zip; do
name=$(basename "$zip" .zip)
echo "- \`$name\`" >> release_notes.md
done
echo "" >> release_notes.md
echo "See README for install options." >> release_notes.md
- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.version.outputs.version }}
name: Release ${{ steps.version.outputs.version }}
body_path: release_notes.md
files: |
dist/skills/*.zip
draft: false
prerelease: false
+21
View File
@@ -0,0 +1,21 @@
name: Validate Skills
on:
pull_request:
push:
branches:
- main
- master
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Validate SKILL.md files
run: python3 scripts/validate_skills.py
- name: Run validation tests
run: python3 -m unittest discover -s tests
- name: Check skills.json catalog
run: python3 scripts/generate_catalog.py --check
+8
View File
@@ -0,0 +1,8 @@
.DS_Store
.env
.idea/
.vscode/
node_modules/
dist/
__pycache__/
*.pyc
+7
View File
@@ -0,0 +1,7 @@
# Changelog
All notable changes to this repository will be documented in this file.
## Unreleased
- Initial public release of LaunchDarkly agent skills
+48
View File
@@ -0,0 +1,48 @@
# Contributing to LaunchDarkly Agent Skills
Thanks for contributing! This repo is a public collection of LaunchDarkly agent skills and playbooks.
## Quick Start
1. Fork and clone the repo.
2. Create a branch for your change.
3. Follow the skill conventions in `docs/skills.md`.
4. Open a PR.
## Adding a New Skill
1. Create a new directory under `skills/`:
```
skills/your-skill-name/
└── SKILL.md
```
2. Copy the template from `template/SKILL.md` and fill it in.
3. Add any references under a `references/` directory.
4. Update the skill list in `README.md`.
5. Regenerate the catalog: `python3 scripts/generate_catalog.py`.
## Skill Naming Guidelines
- Use lowercase and hyphens only.
- Keep names under 64 characters.
- Make descriptions explicit and keyword-rich.
- If skill is specific to a launchdarkly domain (i.e feature flags, ai configs, etc), please enclose the skills in a directory with the domain name.
## Local Testing
Point your agent client at the `skills/` directory. Specific setup depends on the client.
## Documentation
Keep `SKILL.md` under 500 lines. If you need more space, add reference documents.
## Versioning
Update `metadata.version` in `SKILL.md` when the skill behavior changes.
See `docs/versioning.md` for details.
## License
By contributing, you agree that your contributions will be licensed under the Apache-2.0 License.
+13
View File
@@ -0,0 +1,13 @@
Copyright 2026 LaunchDarkly, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+48 -2
View File
@@ -1,2 +1,48 @@
# agent-skills
LaunchDarkly's official collection of agent skills
# LaunchDarkly Agent Skills
LaunchDarkly's public collection of AI agent skills and playbooks. These skills encode repeatable workflows for working with LaunchDarkly, so coding agents can execute common tasks safely and consistently.
## What Is This Repo?
Agent Skills are modular, text-based playbooks that teach an agent how to perform a workflow. This repo is designed to be a public, open-source home for LaunchDarkly skills and to align with the emerging Agent Skills Open Standard.
## Available Skills
| Skill | Description |
|-------|-------------|
| `feature-flags/launchdarkly-flag-cleanup` | Safely remove flags from code using LaunchDarkly as the source of truth |
| `skill-authoring/create-skill` | Add a new skill to the LaunchDarkly agent-skills repo following conventions |
## Quick Start (Local)
```bash
# Clone the repo
git clone https://github.com/launchdarkly/agent-skills.git
cd agent-skills
# If your agent supports skills.sh installs:
npx skills add launchdarkly/agent-skills
# Or manually copy a skill into your agent's skills path:
cp -r skills/feature-flags/launchdarkly-flag-cleanup <your-agent-skills-dir>/
```
Then ask your agent something like:
```
Remove the `new-checkout-flow` feature flag from this codebase
```
## Install via skills.sh CLI
```bash
npx skills add <owner/repo>
```
## Contributing
See `CONTRIBUTING.md` for how to add new skills and the conventions we follow.
## License
Apache-2.0
+5
View File
@@ -0,0 +1,5 @@
# Reporting and Fixing Security Issues
Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our HackerOne program: https://hackerone.com/launchdarkly?type=team. LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty.
Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors.
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
import argparse
import json
import pathlib
import sys
ROOT = pathlib.Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from scripts import validate_skills # noqa: E402
CATALOG_PATH = ROOT / "skills.json"
def parse_metadata_version(frontmatter_lines: list[str]) -> str | None:
in_metadata = False
for line in frontmatter_lines:
if line.strip() == "metadata:":
in_metadata = True
continue
if in_metadata:
if not line.startswith((" ", "\t")):
in_metadata = False
continue
if ":" not in line:
continue
key, raw_value = line.strip().split(":", 1)
if key.strip() == "version":
return validate_skills.normalize_value(raw_value.strip())
return None
def read_marketplace(skill_dir: pathlib.Path) -> dict:
marketplace_path = skill_dir / "marketplace.json"
if not marketplace_path.is_file():
return {}
try:
return json.loads(marketplace_path.read_text(encoding="utf-8"))
except Exception:
return {}
def build_catalog() -> dict:
skill_files = [
p
for p in ROOT.rglob(validate_skills.SKILL_GLOB)
if p.is_file() and not validate_skills.is_excluded(p)
]
catalog_entries = []
for skill_file in skill_files:
text = skill_file.read_text(encoding="utf-8")
lines = text.splitlines()
parsed, err = validate_skills.parse_frontmatter(lines)
if err:
raise ValueError(f"{skill_file.relative_to(ROOT)}: {err}")
frontmatter_lines, _body_lines = parsed
fields, _present = validate_skills.parse_frontmatter_fields(frontmatter_lines)
name = fields.get("name")
description = fields.get("description")
license_name = fields.get("license")
compatibility = fields.get("compatibility")
if not name or not description:
raise ValueError(
f"{skill_file.relative_to(ROOT)}: missing required frontmatter fields"
)
metadata_version = parse_metadata_version(frontmatter_lines)
marketplace = read_marketplace(skill_file.parent)
entry = {
"name": name,
"description": description,
"path": skill_file.parent.relative_to(ROOT).as_posix(),
}
if metadata_version:
entry["version"] = metadata_version
elif isinstance(marketplace.get("version"), str):
entry["version"] = marketplace["version"]
if license_name:
entry["license"] = license_name
if compatibility:
entry["compatibility"] = compatibility
tags = marketplace.get("tags")
if isinstance(tags, list) and tags:
entry["tags"] = tags
catalog_entries.append(entry)
catalog_entries.sort(key=lambda item: item["name"])
return {"skills": catalog_entries}
def main() -> int:
parser = argparse.ArgumentParser(description="Generate skills.json catalog.")
parser.add_argument(
"--check",
action="store_true",
help="Fail if skills.json is out of date.",
)
args = parser.parse_args()
catalog = build_catalog()
content = json.dumps(catalog, indent=2) + "\n"
if args.check:
if not CATALOG_PATH.is_file():
print("skills.json does not exist.")
return 1
existing = CATALOG_PATH.read_text(encoding="utf-8")
if existing != content:
print("skills.json is out of date. Run scripts/generate_catalog.py")
return 1
print("skills.json is up to date.")
return 0
CATALOG_PATH.write_text(content, encoding="utf-8")
print(f"Wrote {CATALOG_PATH.relative_to(ROOT)}")
return 0
if __name__ == "__main__":
sys.exit(main())
+155
View File
@@ -0,0 +1,155 @@
#!/usr/bin/env python3
import pathlib
import re
import sys
ROOT = pathlib.Path(__file__).resolve().parents[1]
SKILL_GLOB = "**/SKILL.md"
EXCLUDED_DIRS = {"template"}
NAME_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
MAX_NAME_LENGTH = 64
MAX_DESC_LENGTH = 1024
MAX_COMPAT_LENGTH = 500
def is_excluded(path: pathlib.Path) -> bool:
return any(part in EXCLUDED_DIRS for part in path.parts)
def parse_frontmatter(lines):
if not lines or lines[0].strip() != "---":
return None, "Missing opening frontmatter delimiter '---' on first line"
end_idx = None
for idx in range(1, len(lines)):
if lines[idx].strip() == "---":
end_idx = idx
break
if end_idx is None:
return None, "Missing closing frontmatter delimiter '---'"
frontmatter_lines = lines[1:end_idx]
body_lines = lines[end_idx + 1 :]
return (frontmatter_lines, body_lines), None
def normalize_value(value: str) -> str | None:
if value in {"", "|", ">", "|-", ">-"}:
return None
if (
(value.startswith('"') and value.endswith('"'))
or (value.startswith("'") and value.endswith("'"))
):
return value[1:-1]
return value
def parse_frontmatter_fields(frontmatter_lines: list[str]) -> tuple[dict, set]:
fields: dict[str, str | None] = {}
present: set[str] = set()
for line in frontmatter_lines:
if not line.strip():
continue
if line.startswith((" ", "\t")):
continue
match = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", line)
if not match:
continue
key = match.group(1)
raw_value = match.group(2).strip()
present.add(key)
fields[key] = normalize_value(raw_value)
return fields, present
def validate_skill(path: pathlib.Path) -> list[str]:
errors = []
try:
text = path.read_text(encoding="utf-8")
except Exception as exc:
return [f"Failed to read file: {exc}"]
lines = text.splitlines()
parsed, err = parse_frontmatter(lines)
if err:
return [err]
frontmatter_lines, body_lines = parsed
fields, present = parse_frontmatter_fields(frontmatter_lines)
name_value = fields.get("name")
if "name" not in present:
errors.append("Frontmatter missing required field: name")
elif not name_value:
errors.append("Frontmatter field 'name' must be a non-empty string")
else:
if len(name_value) > MAX_NAME_LENGTH:
errors.append(f"Frontmatter field 'name' exceeds {MAX_NAME_LENGTH} chars")
if not NAME_PATTERN.match(name_value):
errors.append(
"Frontmatter field 'name' must be lowercase letters, numbers, "
"and single hyphens only"
)
if path.parent.name != name_value:
errors.append(
"Frontmatter field 'name' must match the parent directory name"
)
description_value = fields.get("description")
if "description" not in present:
errors.append("Frontmatter missing required field: description")
elif not description_value:
errors.append("Frontmatter field 'description' must be a non-empty string")
elif len(description_value) > MAX_DESC_LENGTH:
errors.append(
f"Frontmatter field 'description' exceeds {MAX_DESC_LENGTH} chars"
)
compatibility_value = fields.get("compatibility")
if "compatibility" in present:
if not compatibility_value:
errors.append(
"Frontmatter field 'compatibility' must be a non-empty string"
)
elif len(compatibility_value) > MAX_COMPAT_LENGTH:
errors.append(
f"Frontmatter field 'compatibility' exceeds {MAX_COMPAT_LENGTH} chars"
)
body_text = "\n".join(body_lines).strip()
if not body_text:
errors.append("Missing markdown content after frontmatter")
return errors
def main():
skill_files = [
p for p in ROOT.rglob(SKILL_GLOB) if p.is_file() and not is_excluded(p)
]
if not skill_files:
print("No SKILL.md files found.")
return 1
all_errors = []
for path in skill_files:
errors = validate_skill(path)
if errors:
for err in errors:
all_errors.append(f"{path.relative_to(ROOT)}: {err}")
if all_errors:
print("Skill validation failed:")
for err in all_errors:
print(f"- {err}")
return 1
print(f"Validated {len(skill_files)} SKILL.md files successfully.")
return 0
if __name__ == "__main__":
sys.exit(main())
+31
View File
@@ -0,0 +1,31 @@
{
"skills": [
{
"name": "create-skill",
"description": "Add a new skill to the LaunchDarkly agent-skills repo. Use when creating a new SKILL.md, updating the skills catalog, and aligning with repo conventions.",
"path": "skills/skill-authoring/create-skill",
"version": "0.1.0",
"license": "Apache-2.0",
"compatibility": "Works in repositories following the Agent Skills open standard"
},
{
"name": "launchdarkly-flag-cleanup",
"description": "Safely automate feature flag cleanup workflows using the LaunchDarkly MCP server. Use when removing flags from code, cleaning up stale flags, assessing removal readiness, or creating PRs that preserve production behavior.",
"path": "skills/feature-flags/launchdarkly-flag-cleanup",
"version": "1.0.0-alpha",
"license": "Apache-2.0",
"compatibility": "Requires LaunchDarkly MCP server (@launchdarkly/mcp-server)",
"tags": [
"launchdarkly",
"feature-flags",
"feature-management",
"stale-flags",
"tech-debt",
"cleanup",
"code-removal",
"devops",
"mcp"
]
}
]
}
@@ -0,0 +1,93 @@
# LaunchDarkly Flag Cleanup Skill
An Agent Skill for safely automating feature flag cleanup workflows using LaunchDarkly as the source of truth.
## Overview
This skill teaches agents how to:
- Determine if a feature flag is ready for removal
- Calculate the correct forward value to preserve production behavior
- Safely remove flag references from code
- Create well-documented pull requests
## Installation (Local)
For now, install by placing this skill directory where your agent client loads skills.
Examples:
- **Generic**: copy `skills/feature-flags/launchdarkly-flag-cleanup/` into your client's skills path
## Prerequisites
This skill requires the LaunchDarkly MCP server to be configured in your environment.
### Configure MCP Server
**Claude Code (`~/.claude/mcp.json`):**
```json
{
"mcpServers": {
"launchdarkly": {
"command": "npx",
"args": ["-y", "@launchdarkly/mcp-server", "start"],
"env": {
"LD_ACCESS_TOKEN": "your-api-key"
}
}
}
}
```
**Cursor (`.cursor/mcp.json`):**
```json
{
"mcpServers": {
"launchdarkly": {
"command": "npx",
"args": ["-y", "@launchdarkly/mcp-server", "start"],
"env": {
"LD_ACCESS_TOKEN": "your-api-key"
}
}
}
}
```
## Usage
Once installed, the skill activates automatically when you ask about flag cleanup:
```
Remove the `new-checkout-flow` feature flag
```
```
Is the `dark-mode` flag ready to be cleaned up?
```
```
Clean up stale feature flags in this codebase
```
## Structure
```
launchdarkly-flag-cleanup/
├── SKILL.md
├── marketplace.json
├── README.md
└── references/
├── pr-template.md
└── sdk-patterns.md
```
## Related
- [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,146 @@
---
name: launchdarkly-flag-cleanup
description: "Safely automate feature flag cleanup workflows using the LaunchDarkly MCP server. Use when removing flags from code, cleaning up stale flags, assessing removal readiness, or creating PRs that preserve production behavior."
license: Apache-2.0
compatibility: Requires LaunchDarkly MCP server (@launchdarkly/mcp-server)
metadata:
author: launchdarkly
version: "1.0.0-alpha"
---
# LaunchDarkly Flag Cleanup
A workflow for safely removing feature flags from codebases while preserving production behavior. This skill uses LaunchDarkly as the source of truth to determine removal readiness and the correct forward value.
## Prerequisites
This skill requires the LaunchDarkly MCP server to be configured in your environment.
**Required MCP tools:**
- `get-environments`
- `get-feature-flag`
- `get-flag-status-across-environments`
- `get-code-references`
## Core Principles
1. **Safety First**: Always preserve current production behavior.
2. **LaunchDarkly as Source of Truth**: Never guess. Query the actual configuration.
3. **Clear Communication**: Explain reasoning in PR descriptions.
4. **Follow Conventions**: Respect existing code style and structure.
## Flag Removal Workflow
### Step 1: Identify Critical Environments
Use `get-environments` with the project key to find environments marked as critical (typically `production`, `staging`, or user-specified).
### Step 2: Fetch Flag Configuration
Use `get-feature-flag` to retrieve the full configuration. Extract:
- `variations`: Possible values
- Per critical environment:
- `on`: Whether enabled
- `fallthrough.variation`: Variation index when no rules match
- `offVariation`: Variation index when flag is off
- `rules`: Targeting rules (complexity indicator)
- `targets`: Individual context targets
### Step 3: Determine Forward Value
The **forward value** replaces the flag in code.
| Scenario | Forward Value |
|----------|---------------|
| All critical envs ON, same fallthrough, no rules/targets | Use `fallthrough.variation` |
| All critical envs OFF, same offVariation | Use `offVariation` |
| Critical envs differ in ON/OFF state | **NOT SAFE** - stop |
| Critical envs serve different variations | **NOT SAFE** - stop |
### Step 4: Assess Removal Readiness
Use `get-flag-status-across-environments` to check lifecycle status.
**READY** if ALL true:
- Status is `launched` or `active` in all critical environments
- Same variation served across all critical environments
- No targeting rules or individual targets in critical environments
- Flag is not already archived/deprecated
**PROCEED WITH CAUTION** if:
- Status is `inactive` (no recent traffic)
- Zero evaluations in last 7 days (confirm with user)
**NOT READY** if:
- Status is `new` (still rolling out)
- Different variations across critical environments
- Complex targeting rules exist
- Critical environments differ in ON/OFF state
### Step 5: Check Code References
Use `get-code-references` to identify repositories. If the current repo isn't listed, inform the user. Note the count of other repositories for awareness.
### Step 6: Remove Flag from Code
Search for all references and replace with the forward value:
1. **Find evaluation patterns:**
- `variation('flag-key', ...)`
- `boolVariation('flag-key', ...)`
- `featureFlags['flag-key']`
- SDK-specific and wrapper patterns
2. **Replace with forward value:**
- Preserve the branch matching the forward value
- Remove the dead branch and related code
- If assigned to a variable, replace with the value directly
3. **Clean up:**
- Remove unused imports/constants
- Avoid unrelated refactors
- Double-check for orphaned exports or files created solely for the flag
(unused components, hooks, helpers, styles, and test files)
- If the repo uses an unused-export tool (e.g., lint rules, Knip, ts-prune),
run it and remove any flag-related orphans it reports
**Example transformation (forward value = true):**
```typescript
// Before
const showNewCheckout = await ldClient.variation('new-checkout-flow', user, false);
if (showNewCheckout) {
return renderNewCheckout();
} else {
return renderOldCheckout();
}
// After
return renderNewCheckout();
```
### Step 7: Create Pull Request
Use the template in `references/pr-template.md` for a structured PR description including removal summary, readiness assessment, and risk analysis.
## Edge Cases
| Situation | Action |
|-----------|--------|
| Flag not found | Inform user, check for typos |
| Already archived | Ask if code cleanup still needed |
| Multiple SDK patterns | Search all: `variation()`, `boolVariation()`, `variationDetail()`, `allFlags()` |
| Dynamic flag keys (`flag-${id}`) | Warn that automated removal may be incomplete |
| Different default values in code | Flag as inconsistency in PR |
| Orphaned exports/files remain | Run unused-export checks and remove dead files |
## What NOT to Do
- Don't change code unrelated to flag cleanup.
- Don't refactor or optimize beyond flag removal.
- Don't remove flags still being rolled out.
- Don't guess the forward value.
## Related Resources
- [PR Template](references/pr-template.md)
- [SDK Patterns](references/sdk-patterns.md)
@@ -0,0 +1,22 @@
{
"name": "launchdarkly-flag-cleanup",
"description": "Safely automate feature flag cleanup workflows using LaunchDarkly MCP server",
"version": "1.0.0-alpha",
"author": "LaunchDarkly",
"repository": "https://github.com/launchdarkly/agent-skills",
"skills": ["./"],
"tags": [
"launchdarkly",
"feature-flags",
"feature-management",
"stale-flags",
"tech-debt",
"cleanup",
"code-removal",
"devops",
"mcp"
],
"requirements": {
"mcp-servers": ["@launchdarkly/mcp-server"]
}
}
@@ -0,0 +1,136 @@
# PR Template for Flag Removal
Use this template when creating pull requests for flag cleanup.
```markdown
## Flag Removal: `{flag-key}`
### Removal Summary
- **Forward Value**: `{variation value being preserved}`
- **Critical Environments**: {list environments}
- **Status**: ✅ Ready for removal / ⚠️ Proceed with caution / ❌ Not ready
### Removal Readiness Assessment
**Configuration Analysis:**
| Environment | State | Serving | Rules | Targets |
|-------------|-------|---------|-------|---------|
| production | ON/OFF | `{value}` | none/present | none/count |
| {other env} | ON/OFF | `{value}` | none/present | none/count |
**Lifecycle Status:**
| Environment | Status | Evaluations (7d) |
|-------------|--------|------------------|
| production | launched/active/inactive/new | {count} |
| {other env} | launched/active/inactive/new | {count} |
**Code References:**
- Repositories with references: `{count}`
- This PR addresses: `{current repo}`
- Other repos requiring cleanup: `{list if any}`
### Changes Made
- Removed flag evaluation calls: `{count}` occurrences
- Files modified: `{list files}`
- Preserved behavior: `{describe what code now does}`
- Cleaned up: `{list dead code removed}`
### Risk Assessment
{Explain why this change is safe. Address:}
- Why the forward value is correct
- Any edge cases considered
- Impact on other environments (if any)
### Reviewer Checklist
- [ ] Forward value matches production behavior
- [ ] All flag references removed
- [ ] No unrelated changes included
- [ ] Tests pass (if applicable)
- [ ] Dead code properly removed
### Post-Merge Actions
- [ ] Archive flag in LaunchDarkly (after deployment confirmed)
- [ ] Notify other teams if they have code references
```
## Example: Ready for Removal
```markdown
## Flag Removal: `new-checkout-flow`
### Removal Summary
- **Forward Value**: `true`
- **Critical Environments**: production, prod-eu
- **Status**: ✅ Ready for removal
### Removal Readiness Assessment
**Configuration Analysis:**
| Environment | State | Serving | Rules | Targets |
|-------------|-------|---------|-------|---------|
| production | ON | `true` | none | none |
| prod-eu | ON | `true` | none | none |
**Lifecycle Status:**
| Environment | Status | Evaluations (7d) |
|-------------|--------|------------------|
| production | launched | 142,531 |
| prod-eu | launched | 89,203 |
**Code References:**
- Repositories with references: 2
- This PR addresses: `checkout-service`
- Other repos requiring cleanup: `mobile-app`
### Changes Made
- Removed flag evaluation calls: 3 occurrences
- Files modified: `CheckoutController.ts`, `CheckoutService.ts`, `checkout.test.ts`
- Preserved behavior: Always renders new checkout experience
- Cleaned up: Removed `renderOldCheckout()` function and related imports
### Risk Assessment
This change is safe because:
- Both production environments serve `true` to 100% of traffic
- Flag has been at 100% for 47 days with no issues
- No targeting rules or individual overrides exist
- The new checkout flow has been fully validated
### Post-Merge Actions
- [ ] Archive flag in LaunchDarkly (after deployment confirmed)
- [ ] Create follow-up ticket for mobile-app cleanup
```
## Example: Proceed with Caution
```markdown
## Flag Removal: `legacy-api-endpoint`
### Removal Summary
- **Forward Value**: `false`
- **Critical Environments**: production
- **Status**: ⚠️ Proceed with caution
### Removal Readiness Assessment
**Configuration Analysis:**
| Environment | State | Serving | Rules | Targets |
|-------------|-------|---------|-------|---------|
| production | OFF | `false` | none | none |
**Lifecycle Status:**
| Environment | Status | Evaluations (7d) |
|-------------|--------|------------------|
| production | inactive | 0 |
⚠️ **Warning**: Zero evaluations in the last 7 days. This flag may be:
- Dead code that's safe to remove
- Used by a batch job or infrequent process
- Referenced but never called
**Recommendation**: Verify with the team that this code path is truly unused before merging.
```
@@ -0,0 +1,242 @@
# SDK Patterns Reference
Common flag evaluation patterns by SDK and language. Search for these patterns when finding flag references.
## JavaScript/TypeScript (Node.js)
```typescript
// Standard evaluation
ldClient.variation('flag-key', context, defaultValue);
ldClient.boolVariation('flag-key', context, false);
ldClient.stringVariation('flag-key', context, 'default');
ldClient.numberVariation('flag-key', context, 0);
ldClient.jsonVariation('flag-key', context, {});
// With details (includes reason)
ldClient.variationDetail('flag-key', context, defaultValue);
ldClient.boolVariationDetail('flag-key', context, false);
// All flags
ldClient.allFlagsState(context);
```
## JavaScript/TypeScript (Browser/React)
```typescript
// React SDK hooks
const { flags } = useFlags();
const flagValue = flags['flag-key'];
const flagValue = flags.flagKey; // camelCase access
// useLDClient hook
const ldClient = useLDClient();
ldClient.variation('flag-key', defaultValue);
// withLDConsumer HOC
this.props.flags['flag-key']
this.props.ldClient.variation('flag-key', defaultValue);
// Direct client usage
LDClient.variation('flag-key', defaultValue);
```
## Python
```python
# Standard evaluation
ld_client.variation('flag-key', context, default_value)
ld_client.bool_variation('flag-key', context, False)
ld_client.string_variation('flag-key', context, 'default')
ld_client.int_variation('flag-key', context, 0)
ld_client.float_variation('flag-key', context, 0.0)
ld_client.json_variation('flag-key', context, {})
# With details
ld_client.variation_detail('flag-key', context, default_value)
ld_client.bool_variation_detail('flag-key', context, False)
# All flags
ld_client.all_flags_state(context)
```
## Go
```go
// Standard evaluation
ldClient.BoolVariation("flag-key", context, false)
ldClient.StringVariation("flag-key", context, "default")
ldClient.IntVariation("flag-key", context, 0)
ldClient.Float64Variation("flag-key", context, 0.0)
ldClient.JSONVariation("flag-key", context, ldvalue.Null())
// With details
ldClient.BoolVariationDetail("flag-key", context, false)
ldClient.StringVariationDetail("flag-key", context, "default")
// All flags
ldClient.AllFlagsState(context)
```
## Java/Kotlin
```java
// Standard evaluation
ldClient.boolVariation("flag-key", context, false);
ldClient.stringVariation("flag-key", context, "default");
ldClient.intVariation("flag-key", context, 0);
ldClient.doubleVariation("flag-key", context, 0.0);
ldClient.jsonValueVariation("flag-key", context, LDValue.ofNull());
// With details
ldClient.boolVariationDetail("flag-key", context, false);
ldClient.stringVariationDetail("flag-key", context, "default");
// All flags
ldClient.allFlagsState(context);
```
## Ruby
```ruby
# Standard evaluation
ld_client.variation('flag-key', context, default_value)
ld_client.bool_variation('flag-key', context, false)
ld_client.string_variation('flag-key', context, 'default')
ld_client.number_variation('flag-key', context, 0)
ld_client.json_variation('flag-key', context, {})
# With details
ld_client.variation_detail('flag-key', context, default_value)
# All flags
ld_client.all_flags_state(context)
```
## .NET (C#)
```csharp
// Standard evaluation
ldClient.BoolVariation("flag-key", context, false);
ldClient.StringVariation("flag-key", context, "default");
ldClient.IntVariation("flag-key", context, 0);
ldClient.FloatVariation("flag-key", context, 0.0f);
ldClient.DoubleVariation("flag-key", context, 0.0);
ldClient.JsonVariation("flag-key", context, LdValue.Null);
// With details
ldClient.BoolVariationDetail("flag-key", context, false);
ldClient.StringVariationDetail("flag-key", context, "default");
// All flags
ldClient.AllFlagsState(context);
```
## Common Wrapper Patterns
Many teams create abstraction layers. Search for these patterns too:
```typescript
// Service wrappers
featureFlagService.isEnabled('flag-key');
featureFlagService.getValue('flag-key');
featureFlagService.getFlag('flag-key');
FeatureFlags.isEnabled('flag-key');
// Constants/enums
FLAGS.NEW_CHECKOUT_FLOW
FeatureFlag.NEW_CHECKOUT_FLOW
FEATURE_FLAGS['flag-key']
// Decorator patterns (Python/Java)
@feature_flag('flag-key')
@FeatureFlag("flag-key")
// Configuration files
feature_flags:
flag-key: true
```
## Search Strategies
When searching for flag references, use multiple patterns:
```bash
# Exact string match
grep -r "'flag-key'" .
grep -r '"flag-key"' .
# Case variations (kebab-case to camelCase)
grep -r "flagKey" .
# Partial matches for wrapper usage
grep -r "flag-key\|flagKey\|FLAG_KEY" .
# Check constants files
grep -r "flag-key" . --include="*.constants.*"
grep -r "flag-key" . --include="*flags*"
```
## Removal Patterns
### Boolean flag (forward value = true)
```typescript
// Before
if (ldClient.variation('flag-key', user, false)) {
doNewThing();
} else {
doOldThing();
}
// After
doNewThing();
```
### Boolean flag (forward value = false)
```typescript
// Before
if (ldClient.variation('flag-key', user, false)) {
doNewThing();
} else {
doOldThing();
}
// After
doOldThing();
```
### String/multivariate flag
```typescript
// Before
const variant = ldClient.variation('flag-key', user, 'control');
switch (variant) {
case 'new':
return renderNew();
case 'experimental':
return renderExperimental();
default:
return renderControl();
}
// After (forward value = 'new')
return renderNew();
```
### Early return pattern
```typescript
// Before
const enabled = ldClient.variation('flag-key', user, false);
if (!enabled) {
return null;
}
return <NewFeature />;
// After (forward value = true)
return <NewFeature />;
// After (forward value = false)
return null;
```
@@ -0,0 +1,32 @@
# Create Skill (LaunchDarkly)
This skill helps contributors add new skills to the LaunchDarkly agent-skills repository.
## Overview
The workflow covers:
- Selecting a category and skill name
- Creating `SKILL.md` using the template
- Updating `README.md` and `skills.json`
- Validating with the repo scripts
## Usage
Ask:
```
Add a new skill for <workflow> in the LaunchDarkly agent-skills repo
```
## Structure
```
create-skill/
├── SKILL.md
└── README.md
```
## Related
- [Agent Skills Specification](https://agentskills.io/specification)
- [skills.sh](https://skills.sh/)
@@ -0,0 +1,76 @@
---
name: create-skill
description: "Add a new skill to the LaunchDarkly agent-skills repo. Use when creating a new SKILL.md, updating the skills catalog, and aligning with repo conventions."
license: Apache-2.0
compatibility: Works in repositories following the Agent Skills open standard
metadata:
author: launchdarkly
version: "0.1.0"
---
# Create a LaunchDarkly Skill
This skill guides contributors through adding a new skill to the LaunchDarkly agent-skills repository, following the open standard and local repo conventions.
## Prerequisites
- Access to the LaunchDarkly agent-skills repo
- Familiarity with the workflow you want to encode
## Steps
1. **Pick a category and name**
- Choose a category under `skills/` (for example, `feature-flags`, `ai-config`).
- Create a directory `skills/<category>/<skill-name>/`.
- Ensure `<skill-name>` is lowercase with hyphens, and matches the `name` field exactly.
2. **Create `SKILL.md`**
- Copy `template/SKILL.md` into the new skill directory.
- Fill in required frontmatter: `name`, `description`.
- Keep `SKILL.md` under 500 lines and move deep details to `references/`.
3. **Add supporting files**
- If needed, add `references/` and optional `scripts/` or `assets/`.
- Keep reference files small and focused for on-demand loading.
4. **Update repo docs**
- Add the skill to the table in `README.md`.
- If the skill requires specific tooling, document it clearly in the skill.
5. **Update the catalog**
- Run `python3 scripts/generate_catalog.py` to update `skills.json`.
6. **Validate**
- Run `python3 scripts/validate_skills.py`.
- Run `python3 -m unittest discover -s tests`.
## Guidelines
- Follow the Agent Skills spec for naming and frontmatter.
- Make “when to use this” explicit in the description.
- Avoid internal-only links or tools unless the skill is internal-only.
## Examples
### Example: Add an AI config skill
**User**: "Add a skill to guide creating AI Configs"
**Expected behavior**:
1. Create `skills/ai-configs/create-ai-config/`.
2. Fill `SKILL.md` using the template.
3. Add references if needed.
4. Update `README.md` and `skills.json`.
5. Run validation scripts.
## Edge Cases
- **Name mismatch**: If `name` doesnt match the folder name, fix the folder or frontmatter.
- **Overlong SKILL.md**: Move detailed content into `references/`.
- **Missing catalog update**: Regenerate `skills.json` before committing.
## References
- `README.md`
- `docs/skills.md`
- `docs/versioning.md`
+60
View File
@@ -0,0 +1,60 @@
---
name: skill-name
description: A clear description of what this skill does and when the agent should use it. Include keywords that help the agent identify relevant tasks.
compatibility: Specify platform requirements (for example, "Works on all platforms" or "Requires LaunchDarkly MCP server").
metadata:
author: your-name
version: "0.1.0"
---
# Skill Title
Brief overview of what this skill helps accomplish.
## Prerequisites
List any requirements:
- Required tools or CLIs
- Required MCP servers
- Required permissions or access
## Steps
Describe the workflow the agent should follow:
1. **First step**: What to do first
2. **Second step**: What comes next
3. **Continue**: As needed
## Guidelines
- Key principle or best practice
- Another guideline
- Things to watch out for
## Examples
### Example 1: Basic usage
**User**: Example user request
**Expected behavior**:
1. What the agent should do
2. Expected outcome
### Example 2: Edge case
**User**: Another example
**Expected behavior**:
1. How to handle this case
## Edge Cases
- **Scenario A**: How to handle it
- **Scenario B**: How to handle it
## References
Link to any additional documentation:
- [External resources](https://example.com)
+116
View File
@@ -0,0 +1,116 @@
import tempfile
import textwrap
import unittest
from pathlib import Path
from scripts import validate_skills
class ValidateSkillsTests(unittest.TestCase):
def setUp(self):
self._temp_dirs = []
def tearDown(self):
for temp_dir in self._temp_dirs:
temp_dir.cleanup()
def test_parse_frontmatter_missing_opening(self):
lines = ["name: test", "---", "content"]
parsed, err = validate_skills.parse_frontmatter(lines)
self.assertIsNone(parsed)
self.assertIn("opening frontmatter", err)
def test_parse_frontmatter_missing_closing(self):
lines = ["---", "name: test", "description: ok"]
parsed, err = validate_skills.parse_frontmatter(lines)
self.assertIsNone(parsed)
self.assertIn("closing frontmatter", err)
def test_validate_skill_requires_name_and_description(self):
content = textwrap.dedent(
"""\
---
name: test-skill
---
# Title
Body
"""
)
path = self._write_temp_skill(content)
errors = validate_skills.validate_skill(path)
self.assertIn("description", " ".join(errors))
def test_validate_skill_requires_body(self):
content = textwrap.dedent(
"""\
---
name: test-skill
description: ok
---
"""
)
path = self._write_temp_skill(content)
errors = validate_skills.validate_skill(path)
self.assertIn("markdown content", " ".join(errors))
def test_validate_skill_name_must_match_directory(self):
content = textwrap.dedent(
"""\
---
name: test-skill
description: ok
---
# Title
Body
"""
)
path = self._write_temp_skill(content, dir_name="other-skill")
errors = validate_skills.validate_skill(path)
self.assertIn("parent directory", " ".join(errors))
def test_validate_skill_name_constraints(self):
content = textwrap.dedent(
"""\
---
name: Test-Skill
description: ok
---
# Title
Body
"""
)
path = self._write_temp_skill(content, dir_name="Test-Skill")
errors = validate_skills.validate_skill(path)
self.assertIn("lowercase", " ".join(errors))
def test_validate_skill_happy_path(self):
content = textwrap.dedent(
"""\
---
name: test-skill
description: ok
---
# Title
Body
"""
)
path = self._write_temp_skill(content)
errors = validate_skills.validate_skill(path)
self.assertEqual(errors, [])
def _write_temp_skill(self, content: str, dir_name: str = "test-skill") -> Path:
temp_dir = tempfile.TemporaryDirectory()
self._temp_dirs.append(temp_dir)
skill_dir = Path(temp_dir.name) / dir_name
skill_dir.mkdir(parents=True, exist_ok=True)
path = skill_dir / "SKILL.md"
path.write_text(content, encoding="utf-8")
return path
if __name__ == "__main__":
unittest.main()