Audit and inventory all feature flags declared in the Packmind codebase. Use when the user asks to list, audit, review, or inventory feature flags, asks which flags are active, wants to know what a flag gates, or asks which flags are opened to a given domain/user. Produces a synthetic markdown table with each flag key, its audience, an inferred functional description, its active/orphan status, and its usage locations.
Scanned 9/1/2026
Install to Claude Code
npx -y skills add PackmindHub/packmind --skill feature-flags-audit --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Feature Flags Audit?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/packmindhub-feature-flags-audit-7a8e4736)More formats (shields.io, HTML) on the badges page.
---
name: 'feature-flags-audit'
description: 'Audit and inventory all feature flags declared in the Packmind codebase. Use when the user asks to list, audit, review, or inventory feature flags, asks which flags are active, wants to know what a flag gates, or asks which flags are opened to a given domain/user. Produces a synthetic markdown table with each flag key, its audience, an inferred functional description, its active/orphan status, and its usage locations.'
---
# Feature Flags Audit
Produce a synthetic inventory of every feature flag declared in the Packmind codebase, with audience targeting, inferred functional descriptions, and orphan detection. The result is a single markdown table the user can scan at a glance.
## Context
Packmind uses a **custom** feature flag system shared by **both frontend and backend**. There is no third-party library (LaunchDarkly, GrowthBook, PostHog, etc.). Flags are gated by **email domain** or **exact email** via a single central registry. There is no org-based, plan-based, or percentage-rollout mechanism — audience targeting is email-based only. One runtime override does exist: an `FF_*` env kill-switch (see below) can force a flag fully on or off regardless of the email rule.
**Canonical source of truth:**
`packages/feature-flags/src/registry.ts` (the shared, browser-safe `@packmind/feature-flags` package)
This file exports:
- Flag key constants ending in `_FEATURE_KEY` (e.g. `SPACES_MANAGEMENT_FEATURE_KEY = 'spaces-management'`).
- The `FeatureFlagKey` union of all valid flag string values.
- `DEFAULT_FEATURE_DOMAIN_MAP`: a `Record<string, readonly string[]>` mapping each flag's **string value** to a list of allowed entries. Entries starting with `@` are domain rules (e.g. `@packmind.com` matches any user at that domain). Entries containing `@` but not starting with `@` are exact email matches.
- The `isFeatureFlagEnabled()` pure function used at call sites (defined in `packages/feature-flags/src/isFeatureFlagEnabled.ts`).
The registry is consumed on both sides: the frontend imports it via `@packmind/ui` (which re-exports the keys, `DEFAULT_FEATURE_DOMAIN_MAP`, and `isFeatureFlagEnabled` for back-compat; `packages/ui/src/lib/components/content/PMFeatureFlag/PMFeatureFlag.tsx` now holds only the `PMFeatureFlag` React wrapper component). The backend evaluates the same registry through the `isFeatureEnabled(flag, { userEmail })` helper in `@packmind/node-utils` (`packages/node-utils/src/featureFlags/isFeatureEnabled.ts`), which layers the `FF_*` env kill-switch on top of the shared email-domain rule.
If a flag is not declared in `DEFAULT_FEATURE_DOMAIN_MAP`, it does not exist. Do not invent flags.
## Workflow
### Step 1 — Parse the canonical registry
Read `packages/feature-flags/src/registry.ts`. Extract:
1. Every exported constant whose name ends with `_FEATURE_KEY`, along with its string value. The string value is what appears as a key in `DEFAULT_FEATURE_DOMAIN_MAP` — keep the mapping (constant name ↔ string value) in memory.
2. The full `DEFAULT_FEATURE_DOMAIN_MAP` object. For each entry, record the flag's string value and its list of allowed domains/emails.
Build an internal list of flags with three fields so far: constant name, string value, audience (array of allowed entries).
**Cross-check constants against the map.** Every `_FEATURE_KEY` constant should have a matching entry in `DEFAULT_FEATURE_DOMAIN_MAP`, and every map entry should correspond to a declared constant. A mismatch is a real misconfiguration:
- A constant declared with no map entry produces a flag that is never enabled for anyone — dead code.
- A map entry whose string value isn't tied to any exported constant means call sites can't import it symbolically and are likely using the inlined string (fragile).
If you find either case, do not drop it from the inventory — include it and surface the mismatch in the **Summary** section.
### Step 2 — Find usages for each flag
For each flag, search the codebase for where it is consumed. Use Grep:
- First pass: search for the **constant name** (e.g. `SPACES_MANAGEMENT_FEATURE_KEY`). This is the canonical import form and catches almost all usages.
- Second pass: search for the **string value** (e.g. `'spaces-management'` or `"spaces-management"`) as a safety net. Some usages may inline the string instead of importing the constant.
- Backend pass: search for `isFeatureEnabled(` (the backend helper call) across `apps/api/**`, `apps/mcp-server/**`, `apps/cli/**`, and `packages/**`. This is a **plain async helper call, not an injected port** — so search the function name, not an interface. A call looks like `await isFeatureEnabled('some-flag', { userEmail })`; the first argument tells you which flag is gated. (Backend call sites may currently be zero — the mechanism exists but the first real consumer may not have landed yet — so finding none is normal, not a bug.)
Scope the search to:
- `apps/frontend/src/**`
- `apps/frontend/app/**` — the React Router v7 file-based route tree. Route modules live here (e.g. `apps/frontend/app/routes/org.$orgSlug._protected.settings.tsx`), **not** under `src/`, and they gate flags too (typically via `isFeatureFlagEnabled(...)` to show/hide nav entries, routes, or whole pages). Omitting this directory will mis-report a live route-gated flag as an orphan.
- `apps/api/**`, `apps/mcp-server/**`, `apps/cli/**` — backend use cases may gate flags via `isFeatureEnabled(flag, { userEmail })`. Include these so a backend-gated flag is not mis-reported as an orphan.
- `packages/**` — includes the shared `@packmind/feature-flags` package (registry) and `@packmind/node-utils` (the backend helper); the registry definition files are excluded below.
Exclude:
- The canonical registry files themselves: `packages/feature-flags/src/registry.ts` and `packages/feature-flags/src/isFeatureFlagEnabled.ts` (plus the package barrel `packages/feature-flags/src/index.ts`, which only re-exports them).
- The backend helper definition itself: `packages/node-utils/src/featureFlags/isFeatureEnabled.ts` (it *defines* `isFeatureEnabled`; it is not a consumer). Count only *call sites* of the helper, not its declaration.
- The `@packmind/ui` back-compat re-export shim: `packages/ui/src/lib/components/content/PMFeatureFlag/PMFeatureFlag.tsx` (now only the React wrapper + re-exports) and its co-located test file `PMFeatureFlag.test.tsx`. (The parent barrel at `packages/ui/src/lib/components/content/index.ts` re-exports for back-compat but is not a real gate.)
- Test and spec files anywhere in the search scope: `*.test.*`, `*.spec.*`, and Storybook stories `*.stories.*`. A usage inside a test doesn't represent shipping behavior and would mislead a reader scanning the "Usage files" column. If a flag is *only* referenced from tests, treat it as an orphan (see Step 4) and note the test-only state in the description.
- Generated/build artifacts (`dist/`, `node_modules/`).
Record the list of files where each flag is actually consumed.
### Step 3 — Infer a functional description
For each flag with at least one usage, open **1 to 3 of the most representative usage files** and read enough context to understand what the flag actually gates from a user-facing perspective. Use these signals (in rough priority order):
1. The JSX wrapped inside `<PMFeatureFlag featureKeys={[FLAG]}>...</PMFeatureFlag>` — this is literally what appears or disappears.
2. The branch or value guarded by `isFeatureFlagEnabled({ featureKeys: [FLAG] })` — often an "edit" / "create" / "delete" affordance, or an entire route/tab.
3. The component or file name hosting the gated code (e.g. `SpacesManagementActions.tsx` → space management actions).
4. Button labels, section titles, or surrounding comments near the gate.
Write **one short sentence** in the functional voice — describe what the user sees (or doesn't see) when the flag is off. Avoid technical framing.
**Examples of good descriptions:**
- "Reveals the 'Move to space' action and space management UI in the recipes list."
- "Enables inline editing of change proposals in review detail pages."
- "Adds 'Propose change' links from recipes and standards to the webapp review flow."
**Examples of bad descriptions (do not write these):**
- "Wrapped by the `PMFeatureFlag` component in `SpacesManagementActions.tsx`." (tells the reader nothing functional)
- "Feature flag for spaces management." (circular, just re-states the key)
If you cannot form a confident one-liner after reading the usage sites, write a shorter description prefixed with `(uncertain)` so the user knows to double-check.
### Step 3b — Check E2E test coverage
For each flag, search `apps/e2e-tests/src/` for spec files that call `.use({ underFeatureFlag: true })`. These tests explicitly target a feature-flagged flow and represent intentional E2E coverage of the flag.
```
grep -r "underFeatureFlag: true" apps/e2e-tests/src/
```
Record which flags have associated E2E test coverage. A flag with no E2E coverage is not an orphan (orphan is determined by frontend usage, not tests), but the information is worth surfacing so the user knows which flagged features are tested end-to-end.
Add an **E2E tested** column to the output table (Step 5) with either `Yes` or `No`.
### Step 4 — Detect orphans
A flag is **orphan** if, after the two-pass search, no usage is found outside the canonical file and its test. Orphans are candidates for removal and worth surfacing — they often accumulate when a feature ships and nobody remembers to delete the gate.
**Before declaring a flag orphan, confirm the search covered `apps/frontend/app/**` (the route tree), not just `apps/frontend/src/**`.** A flag is frequently consumed only in a route module to gate a nav entry or page — missing that directory is the most common cause of a false orphan.
For orphans:
- Set status to `Orphan`.
- Set the description to `(no usage found — candidate for removal)`.
- Set the usage-files column to `—`.
### Step 5 — Render the table
Output a single markdown table. Use these columns, in this order, with these exact headers:
| Key | Audience | Functional description | Status | E2E tested | Usage files |
**Column rules:**
- **Key** — the string value (e.g. `change-proposals`), not the constant name. It's what appears in the domain map and what a PM or stakeholder recognises.
- **Audience** — the allowed entries, comma-separated, in the same form as in the map (domains keep their `@` prefix). If the list is empty, write `(none)`.
- **Functional description** — one short sentence, functional voice. See Step 3.
- **Status** — either `Active` or `Orphan`.
- **E2E tested** — `Yes` if at least one spec file in `apps/e2e-tests/src/` uses `.use({ underFeatureFlag: true })`, otherwise `No`.
- **Usage files** — comma-separated list of repo-relative paths. Truncate to the **5 most representative** ones and append `(… +N more)` if there are more. "Most representative" is a ranked preference, in this order:
1. Files containing a `<PMFeatureFlag featureKeys={[FLAG]}>...</PMFeatureFlag>` JSX gate — these are the literal "what appears when the flag is on".
2. Files calling `isFeatureFlagEnabled({ featureKeys: [FLAG] })` in a branch that guards an action, route, or tab.
3. Files that import the constant but only pass it downstream (e.g. prop-drilling, utility wiring) — lowest priority, include only if the first two tiers don't fill the 5 slots.
For orphans, write `—`.
After the table, add a short **Summary** section with:
- Total flag count, active count, orphan count.
- Any audience anomaly worth flagging — e.g. a flag opened to an external company's domain (like `@monpetitplacement.fr`, `@ninaa.io`) is usually a pilot and the user will want to notice it. Mention it briefly.
### Output format
```markdown
# Feature Flags Inventory
| Key | Audience | Functional description | Status | E2E tested | Usage files |
| --- | --- | --- | --- | --- | --- |
| space-identity | @packmind.com, @promyze.com | Reveals the space identity settings panel and related configuration affordances. | Active | No | apps/frontend/src/domain/spaces/components/SpaceGeneralSettings.tsx, apps/frontend/src/domain/spaces/components/SpaceIdentityForm.tsx |
| change-proposals | @packmind.com, @promyze.com, @monpetitplacement.fr, @ninaa.io | Enables the change proposal review flow end-to-end (navigation entry points and review detail pages). | Active | Yes | apps/frontend/src/domain/change-proposals/components/StandardReviewDetail/StandardReviewDetail.tsx, apps/frontend/src/domain/change-proposals/components/SkillReviewDetail/SkillReviewDetail.tsx, apps/frontend/src/domain/change-proposals/components/CommandReviewDetail/CommandReviewDetail.tsx (… +2 more) |
| some-old-flag | @packmind.com | (no usage found — candidate for removal) | Orphan | No | — |
**Summary:** 7 flags — 6 active, 1 orphan. `change-proposals` is opened to `@monpetitplacement.fr` and `@ninaa.io` (external pilots, worth monitoring).
```
Output only the report — no preamble, no "here is the table" sentence. The user invokes this skill to get the report itself.
## Notes and edge cases
- **Do not invent flags.** Only entries in `DEFAULT_FEATURE_DOMAIN_MAP` are real flags. If the user asks about a flag that isn't there, say so explicitly.
- **Audience targeting is email-only.** Do not describe the audience as "organizations", "plans", "teams", or "percentages" — those concepts do not exist in this system. A domain like `@packmind.com` means "any user whose email is at this domain", nothing more.
- **The registry is the audience source of truth, but not fully static.** There is no remote config or third-party service, but an `FF_*` env kill-switch does exist: the backend helper reads `Configuration.getConfig('FF_' + SCREAMING_SNAKE(flag))` (e.g. `FF_CHANGE_PROPOSALS_IN_WEBAPP`) — values `on`/`all`/`true` force the flag fully on, `off`/`none`/`false` force it off, and unset/empty falls through to the email-domain rule in `DEFAULT_FEATURE_DOMAIN_MAP`. So the map defines the *default* audience; an env override can win over it at runtime.
- **The backend uses feature flags too.** Scan `apps/api/`, `apps/mcp-server/`, and `apps/cli/` for backend call sites of `isFeatureEnabled(flag, { userEmail })` (the `@packmind/node-utils` helper). This is a **plain async function call, not an injected port** — search for the function name, not an interface. Backend call sites may currently be zero (the mechanism is in place ahead of its first real consumer), so finding none is expected, not a violation.
- **Frontend code is split across two roots.** Components and domain logic live in `apps/frontend/src/`, but React Router v7 route modules live in `apps/frontend/app/routes/`. Flags are gated in both. Searching only `src/` is the classic miss — e.g. `MARKETPLACES_FEATURE_KEY` is consumed solely in `apps/frontend/app/routes/org.$orgSlug._protected.settings.tsx` to gate the Settings → Distribution → Marketplaces nav entry, and would look like an orphan if `app/` were skipped.
- **`PMFeatureFlag` with empty `featureKeys={[]}` always renders.** This is a rare pattern but if you see it while reading usages, don't count it as a real gate.Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!