flagrm 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,52 @@ contain breaking changes.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-10-23
11
+
12
+ ### Added
13
+
14
+ - `flagrm stale [flags...]` and the `launchdarkly:` config section: reads a
15
+ LaunchDarkly project (read-only, token from `LD_API_TOKEN`) and says which
16
+ flags are ready to remove from code (`remove`), not in this repository
17
+ (`unreferenced`) or should stay (`keep`, with reasons), with `--json` and
18
+ `--md`. Exit 3 when LaunchDarkly can't be reached or refuses the request
19
+ (rejected token, no access to the project, unknown project key).
20
+ The `flagrm-remove` skill uses it instead of asking whether a flag set in
21
+ LaunchDarkly is ON everywhere; `examples/ci/` has scheduled GitHub Actions
22
+ and Azure Pipelines workflows that open a removal PR per ready flag, with
23
+ an advisory `/flagrm-review` by a second, read-only agent in the PR body.
24
+ - `flagrm doctor` warns while an old `flagrm-verify` skill is still installed.
25
+
26
+ ### Changed
27
+
28
+ - **Breaking:** the `/flagrm-verify` skill is replaced by `/flagrm-review`.
29
+ Optional, in a new session: it runs `flagrm verify`, stops on a failure, and
30
+ on a pass reviews the diff since the baseline (the ON path kept, deleted
31
+ tests OFF-path only, nothing unrelated changed, no lost side effects). It
32
+ reports findings, edits nothing and writes no commit message; that stays
33
+ with `/flagrm-remove`, which now points to the review. Run `flagrm update`:
34
+ it deletes the `flagrm-verify` skills and Copilot prompt file that flagrm
35
+ installed (edits made to them are lost) and installs `flagrm-review`.
36
+ `flagrm verify` itself is unchanged.
37
+ - README: how to install without a `package.json` (globally or with `npx`),
38
+ and what to do when the UI and the backend are in separate repositories.
39
+
40
+ ## [0.4.1] - 2026-10-12
41
+
42
+ ### Security
43
+
44
+ - Replaced `fast-glob` with `tinyglobby`, which drops the vulnerable `braces`
45
+ dependency (GHSA-vfj7-8cjw-p6xm) from installs.
46
+ - The Stop hook no longer lets a removal finish unverified when the config's
47
+ `exclude` (or anything else it scans) changed since the baseline: widening
48
+ `exclude` used to hide the removal's edits from the hook.
49
+ - A `git.sha` in `baseline.json` that isn't a commit id is refused, and no
50
+ revision starting with `-` reaches git, where `--output=<file>` would have
51
+ written a file.
52
+ - SECURITY.md says what flagrm guarantees and what the Stop hook doesn't:
53
+ it guards against finishing a removal by accident, not an agent working
54
+ around it.
55
+
10
56
  ## [0.4.0] - 2026-10-11
11
57
 
12
58
  ### Added
@@ -271,6 +317,8 @@ removes the flag through the `flagrm-remove` skill, and flagrm checks the result
271
317
  `doctor` warns about an incremental `dotnet build`, which hides existing
272
318
  warnings from the dead-code comparison.
273
319
 
320
+ [0.5.0]: https://github.com/dino-cosic/flagrm/releases/tag/v0.5.0
321
+ [0.4.1]: https://github.com/dino-cosic/flagrm/releases/tag/v0.4.1
274
322
  [0.4.0]: https://github.com/dino-cosic/flagrm/releases/tag/v0.4.0
275
323
  [0.3.0]: https://github.com/dino-cosic/flagrm/releases/tag/v0.3.0
276
324
  [0.2.1]: https://github.com/dino-cosic/flagrm/releases/tag/v0.2.1
package/README.md CHANGED
@@ -2,18 +2,31 @@
2
2
 
3
3
  [![CI](https://github.com/dino-cosic/flagrm/actions/workflows/ci.yml/badge.svg)](https://github.com/dino-cosic/flagrm/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/flagrm)](https://www.npmjs.com/package/flagrm)
5
+ [![Node.js](https://img.shields.io/node/v/flagrm)](https://nodejs.org)
6
+ [![License](https://img.shields.io/npm/l/flagrm)](LICENSE)
5
7
 
6
- Remove feature flags with your AI coding agent, safely.
8
+ **Remove feature flags with your AI coding agent, safely.**
7
9
 
8
- flagrm gives Claude Code, GitHub Copilot and Codex a flag-removal skill and the
9
- guardrails to check the result. It records a baseline before the edit, then
10
- gates the removal with `flagrm verify`, which fails while the flag is still
11
- referenced, the build breaks, a test or lint regresses, or the removal leaves
12
- unused code behind.
10
+ [Website](https://flagrm.com) · [Quick start](#quick-start) · [How it works](#how-it-works) · [Changelog](CHANGELOG.md)
13
11
 
14
- The agent does the reasoning and the edits: finding the flag, its aliases and
15
- wrappers, removing the OFF path, and refactoring methods the flag was passed
16
- into. flagrm decides when the work is done.
12
+ Removing a feature flag looks easy: delete the `if`, keep the ON branch. In a
13
+ real codebase the flag hides behind wrappers and aliases, reaches templates and
14
+ config files, has tests for the OFF path, and leaves behind methods nothing
15
+ calls anymore. An AI agent handles most of that well, but it decides on its own
16
+ when it's done, and that's when it misses things.
17
+
18
+ flagrm gives Claude Code, GitHub Copilot and Codex a flag-removal skill and a
19
+ deterministic definition of "done":
20
+
21
+ - **The agent does the reasoning and the edits.** It finds the flag, its
22
+ aliases and wrappers, removes the OFF path, and refactors the code the flag
23
+ was passed into. flagrm never edits your source code.
24
+ - **flagrm decides when the removal is finished.** It records a baseline before
25
+ the first edit, then `flagrm verify` fails while the flag is still
26
+ referenced, the build breaks, a test or lint regresses, or the removal
27
+ leaves unused code behind.
28
+ - **The agent doesn't stop early.** In Claude Code, a Stop hook keeps the
29
+ agent from finishing until `verify` passes for the exact files on disk.
17
30
 
18
31
  Angular and .NET projects work out of the box. Any other stack works through the
19
32
  generic adapter.
@@ -21,18 +34,6 @@ generic adapter.
21
34
  > **Status:** flagrm is at version 0.4. Until 1.0, a minor release may contain
22
35
  > breaking changes.
23
36
 
24
- ## Requirements
25
-
26
- - Node.js 22 or later
27
- - git: `baseline` and `verify` compare against the commit the removal started from
28
- - An AI coding agent that reads skills: Claude Code, GitHub Copilot or Codex
29
- (the Stop hook is Claude Code only)
30
-
31
- On Windows, the Stop hook runs in Git Bash, or in PowerShell when Git Bash is
32
- not installed. In PowerShell, `npx` needs an execution policy that allows
33
- scripts (`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`). Otherwise the
34
- hook fails and does not guard the removal. `flagrm verify` still works.
35
-
36
37
  ## Quick start
37
38
 
38
39
  ```sh
@@ -48,12 +49,8 @@ agent:
48
49
  /flagrm-remove NewCheckout
49
50
  ```
50
51
 
51
- The agent ends with a short overview (checks, changed files, tests deleted or
52
- renamed) and a proposed commit message; it commits only when you ask. Several
53
- flags at once: `/flagrm-remove NewCheckout DarkMode` asks the ON question and
54
- whether to commit each flag once, then removes them one after another. To
55
- check a removal later, run `/flagrm-verify NewCheckout`. The overview looks
56
- like this (abridged):
52
+ The agent ends with a short overview and a proposed commit message; it commits
53
+ only when you ask. The overview looks like this (abridged):
57
54
 
58
55
  ```md
59
56
  ### flagrm verify: NewCheckout ✅ pass
@@ -74,66 +71,126 @@ like this (abridged):
74
71
  **Tests renamed (1):** `CheckoutComponent › places the order when NewCheckout is on` → `CheckoutComponent › places the order`
75
72
  ```
76
73
 
77
- Tests that no longer run are sorted by why: deleted in the diff (the OFF
78
- path's tests), renamed without the flag in the name, or left out by a test
79
- command changed since the baseline. Only a test that no longer runs for none
80
- of these reasons is a warning, for the agent to explain in its notes.
74
+ - **Several flags:** `/flagrm-remove NewCheckout DarkMode` asks once whether
75
+ they are all ON and whether to commit each, then removes them one after
76
+ another, one commit each.
77
+ - **Independent review:** `/flagrm-review NewCheckout` in a new session runs
78
+ `flagrm verify`, then checks what it can't: that the ON path is the code
79
+ that stayed and that each deleted test covered only the OFF path. It
80
+ reports findings and edits nothing.
81
+ - **Tests that no longer run** are sorted by why: deleted in the diff (the OFF
82
+ path's tests), renamed without the flag in the name, or left out by a test
83
+ command changed since the baseline. Only a test that no longer runs for none
84
+ of these reasons is a warning, for the agent to explain in its notes.
85
+
86
+ ### Requirements
87
+
88
+ - Node.js 22 or later
89
+ - git: `baseline` and `verify` compare against the commit the removal started from
90
+ - An AI coding agent that reads skills: Claude Code, GitHub Copilot or Codex
91
+ (the Stop hook is Claude Code only)
92
+
93
+ No `package.json` (a pure .NET or Go repository)? Install globally with
94
+ `npm install -g flagrm`, or run `npx flagrm`. The Stop hook runs
95
+ `npx --no-install flagrm hook stop`, so it needs a local or a global install.
96
+
97
+ On Windows, the Stop hook runs in Git Bash, or in PowerShell when Git Bash is
98
+ not installed. In PowerShell, `npx` needs an execution policy that allows
99
+ scripts (`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`). Otherwise the
100
+ hook fails and does not guard the removal. `flagrm verify` still works.
81
101
 
82
102
  ## How it works
83
103
 
104
+ ```mermaid
105
+ flowchart LR
106
+ A["flagrm init<br/>once per repository"] --> B["/flagrm-remove flag"]
107
+ B --> C["flagrm baseline<br/>git commit, build, tests, names"]
108
+ C --> D["agent edits"]
109
+ D --> E{"flagrm verify"}
110
+ E -- fail --> D
111
+ E -- pass --> F["overview +<br/>commit message"]
112
+ H["Stop hook"] -. blocks finishing until verify passes .-> E
113
+ ```
114
+
84
115
  1. **`flagrm init`** asks which AI coding tools to set up (Claude Code,
85
116
  GitHub Copilot, Codex), with the ones the repository already uses
86
- pre-selected; `--tool claude-code,codex` answers without asking, and
87
- without a terminal it takes the detected tools (all three when none is).
88
- It writes `flagrm.config.yaml` for the Angular and .NET projects it finds,
89
- with the choice as `tools:`, and adds `.flagrm/` to `.gitignore`. Then per
90
- tool: for Claude Code the `flagrm-remove` and `flagrm-verify` skills in
91
- `.claude/skills/` and a Stop hook in `.claude/settings.json`; for Copilot
92
- the skills in `.github/skills/` and `/flagrm-remove` and `/flagrm-verify`
93
- prompt files in `.github/prompts/`; for Codex the skills in
94
- `.agents/skills/` and a pointer in `AGENTS.md`.
117
+ pre-selected. It writes `flagrm.config.yaml` for the Angular and .NET
118
+ projects it finds, adds `.flagrm/` to `.gitignore`, and installs each
119
+ tool's files:
120
+
121
+ | Tool | Files |
122
+ |---|---|
123
+ | Claude Code | `flagrm-remove` and `flagrm-review` skills in `.claude/skills/`, Stop hook in `.claude/settings.json` |
124
+ | GitHub Copilot | skills in `.github/skills/`, `/flagrm-remove` and `/flagrm-review` prompt files in `.github/prompts/` |
125
+ | Codex | skills in `.agents/skills/`, a pointer in `AGENTS.md` |
126
+
127
+ `--tool claude-code,codex` answers without asking; without a terminal it
128
+ takes the detected tools (all three when none is). The choice is saved as
129
+ `tools:`. `init` never overwrites or deletes a file.
95
130
  2. **`/flagrm-remove <flag>`**: the agent checks that the tree is clean and runs
96
131
  `flagrm baseline <flag>`, which records the git commit, the build and test
97
132
  results, and the flag's names. It finds every usage with its own search,
98
133
  records wrappers and aliases with `flagrm baseline <flag> --name`, removes
99
- the flag, and repeats `flagrm verify <flag>` until it passes. It ends with
100
- the overview and a commit message. With several flags it asks once whether
101
- they are all ON and whether to commit each, then removes them in order,
102
- one commit each.
134
+ the flag, and repeats `flagrm verify <flag>` until it passes.
103
135
  3. **The Stop hook** (Claude Code) keeps the agent from finishing while a
104
136
  removal is in progress and not verified. In progress means a file git
105
137
  tracks changed on top of the baseline commit (untracked files and
106
138
  flagrm's own setup files don't count, so a removal paused on a question to
107
- the user doesn't block), or the last verify used `--skip`.
108
- 4. **`/flagrm-verify <flag>`** checks a removal later: runs
109
- `flagrm verify <flag> --md` and adds notes and a commit message.
110
-
111
- ## Commands
112
-
113
- | Command | What it does | Exit codes |
114
- |---|---|---|
115
- | `init` | Set up the config and `.gitignore` entry, and each chosen AI tool's files (asks, or `--tool claude-code,copilot,codex`; saved as `tools:`). Never overwrites or deletes | 0, 2 usage error |
116
- | `doctor` | Check the config, the build, test and lint commands (marking adapter defaults; `--run` runs them and fails when tests write no readable `testResults`), the .NET SDK `global.json` asks for, `cargo` and Docker where projects need them, git, `.gitignore`, installed skills and hook, and abandoned baselines | 0 ok, 1 problems |
117
- | `scope` | For each .NET project, show what the machine lacks and which projects a solution filter would leave out; `--write` creates `flagrm.slnf`, adds it to `.git/info/exclude` and points the project's build and test at it | 0 nothing missing, 1 something missing |
118
- | `update` | Refresh the installed skills, prompt files, `AGENTS.md` block and hook from this flagrm version, for the config's `tools:` | 0 |
119
- | `list` | List every feature flag in the project: definitions, configured state per environment, reference counts; flow names include renamed imports of a flag registry (`import { FeatureFlags as AppFeatureFlags }`) and, in Angular, the fields, getters, methods and locals holding a flag and a child component's input bound to one | 0 |
120
- | `baseline <flag>` | Before any edit, record the git commit, build, test and lint results and the flag's names in `.flagrm/<flag>/baseline.json`. In .NET and Angular code the names include the locals, fields, getters, parameters and methods the flag's value travels through when they name the flag (a local, parameter or field is checked only in its own file, and an Angular component's template); generic or ambiguous ones are listed as `suggestedNames`, with `parameterizedTests` that pass the value as a literal. `--file` (repeatable) limits `--name` entries to those files (and records them as wrappers). `--name X --kind alias\|wrapper` adds names to an existing baseline (a name whose declaration reads only other flags is refused); `--json` prints a summary (test counts, at most 20 failed test names; the file keeps everything); `--force` starts over, but only while the code is unedited since the baseline commit; `--accept config,failures` acknowledges, also mid-removal, a config change or the baseline's failing tests so verify stops warning about them; `--no-checks` skips build, tests and lint; build, test and lint results of a passing verify on the same tree and commands are reused (`--no-reuse` runs them) | 0, 2 usage error |
121
- | `verify <flag>` | Gate the removal with `leftovers`, `dead-code`, `build`, `tests`, `lint` and `coverage`. Writes `.flagrm/<flag>/verify.json`. `--md` prints the overview, `--json` the full result, `--skip` skips checks, `--strict` fails on warnings; reuses its own or another flag's passing runs on the same tree and commands (`--no-reuse` runs them) | 0 pass, 1 fail, 2 usage error |
122
- | `hook stop` | The Claude Code Stop hook installed by `init` | always 0 |
123
-
124
- `list`, `baseline` and `verify` accept `--json`, `--config <path>`, and
125
- `--include` / `--exclude <glob>`.
139
+ the user doesn't block), the config changed in what it scans or ignores
140
+ (such as `exclude`), or the last verify used `--skip`. It guards against
141
+ finishing by accident, not an agent working around it: see
142
+ [SECURITY.md](SECURITY.md#what-it-doesnt-guarantee).
143
+ 4. **`/flagrm-review <flag>`** (optional), in a new session, reviews the
144
+ removal: it runs `flagrm verify <flag> --json` and stops on a failure; on
145
+ a pass it reads the diff since the baseline commit and checks that the ON
146
+ path was kept, deleted tests covered only the OFF path, and nothing
147
+ unrelated changed or lost a side effect. Its findings never change the
148
+ verify result. A new session matters: an agent reviewing its own removal
149
+ checks its own reasoning.
126
150
 
127
151
  ## What `verify` checks
128
152
 
129
153
  | Check | Fails when |
130
154
  |---|---|
131
- | `leftovers` | A recorded name (the flag literal, its constants, wrappers the agent recorded) is still in code or config. A mention in a comment is a warning, and so is a test that checks text the removal deleted from the code (a test of the OFF path that never names the flag), or deleted text whose replacement on the same line no test checks (lost ON coverage) |
132
- | `dead-code` | The compiler reports new unused code (locals, imports, private members) in changed files. A method, property, field or type the removal left named only where it is declared is a warning (compilers don't report public members) |
155
+ | `leftovers` | A recorded name (the flag literal, its constants, wrappers the agent recorded) is still in code or config |
156
+ | `dead-code` | The compiler reports new unused code (locals, imports, private members) in changed files |
133
157
  | `build` | A project's build command fails |
134
- | `tests` | A test that passed at the baseline fails. Tests that already failed at the baseline are known failures and only warn; without `testResults` they are read from the test log (Karma, Jest, Vitest, `dotnet test`), and count as known only when the log names every failing test. A test that no longer runs though the diff neither deletes nor renames it and the test config is unchanged, and a changed test file none of whose tests ran, are warnings for the agent to account for. Without `testResults`, deleted and renamed tests come from the changed test files, and a test count that dropped by more than the diff deletes is a warning (info when the changed test files use data rows such as `[InlineData]`, `TestCase`, `DataRow` or `.each`, whose cases count separately, or when the test config changed since the baseline) |
135
- | `lint` | The project's `lint` command fails where it passed at the baseline. A lint that already failed at the baseline only warns, unless its log has error lines the baseline's didn't (line and column numbers ignored); a missing package script or ESLint config is a setup problem and fails |
136
- | `coverage` | A changed file that no project's build, test or lint command checks (outside every project with a command, or outside its `covers` globs) is a warning, so it fails only with `--strict`. Files already dirty at the baseline and unchanged since don't count, nor do projects whose commands `--skip` left out |
158
+ | `tests` | A test that passed at the baseline fails |
159
+ | `lint` | The project's `lint` command fails where it passed at the baseline |
160
+ | `coverage` | A changed file that no build, test or lint command checks (a warning, so it fails only with `--strict`) |
161
+
162
+ In typed code, deleting the flag's definition turns every missed reference into
163
+ a compile error, so the build does most of the work. `leftovers` covers what the
164
+ compiler cannot see: config files, templates, comments and wrappers.
165
+
166
+ <details>
167
+ <summary>Warnings and edge cases per check</summary>
168
+
169
+ - **`leftovers`**: a mention in a comment is a warning, and so is a test that
170
+ checks text the removal deleted from the code (a test of the OFF path that
171
+ never names the flag), or deleted text whose replacement on the same line no
172
+ test checks (lost ON coverage).
173
+ - **`dead-code`**: a method, property, field or type the removal left named
174
+ only where it is declared is a warning (compilers don't report public
175
+ members).
176
+ - **`tests`**: tests that already failed at the baseline are known failures and
177
+ only warn; without `testResults` they are read from the test log (Karma,
178
+ Jest, Vitest, `dotnet test`), and count as known only when the log names
179
+ every failing test. A test that no longer runs though the diff neither
180
+ deletes nor renames it and the test config is unchanged, and a changed test
181
+ file none of whose tests ran, are warnings for the agent to account for.
182
+ Without `testResults`, deleted and renamed tests come from the changed test
183
+ files, and a test count that dropped by more than the diff deletes is a
184
+ warning (info when the changed test files use data rows such as
185
+ `[InlineData]`, `TestCase`, `DataRow` or `.each`, whose cases count
186
+ separately, or when the test config changed since the baseline).
187
+ - **`lint`**: a lint that already failed at the baseline only warns, unless its
188
+ log has error lines the baseline's didn't (line and column numbers ignored);
189
+ a missing package script or ESLint config is a setup problem and fails.
190
+ - **`coverage`**: a file counts as unchecked when it is outside every project
191
+ with a command, or outside its `covers` globs. Files already dirty at the
192
+ baseline and unchanged since don't count, nor do projects whose commands
193
+ `--skip` left out.
137
194
 
138
195
  The baseline also records the configuration it was taken with: each project's
139
196
  resolved build, test and lint commands, the files they name (a `.slnf`, a
@@ -141,9 +198,78 @@ resolved build, test and lint commands, the files they name (a `.slnf`, a
141
198
  of it changed since, the check it affects warns and shows the difference: a
142
199
  narrower test command makes "no new failures" mean less.
143
200
 
144
- In typed code, deleting the flag's definition turns every missed reference into
145
- a compile error, so the build does most of the work. `leftovers` covers what the
146
- compiler cannot see: config files, templates, comments and wrappers.
201
+ </details>
202
+
203
+ ## Commands
204
+
205
+ | Command | What it does | Exit codes |
206
+ |---|---|---|
207
+ | `init` | Set up the config, `.gitignore` entry and each chosen AI tool's files | 0, 2 usage error |
208
+ | `doctor` | Check the config, commands, toolchain, git setup, installed skills and hook | 0 ok, 1 problems |
209
+ | `scope` | Show which .NET projects this machine can't build and write a solution filter without them | 0 nothing missing, 1 something missing |
210
+ | `update` | Refresh the installed skills, prompt files, `AGENTS.md` block and hook | 0 |
211
+ | `list` | List every feature flag: definitions, configured state per environment, reference counts | 0 |
212
+ | `baseline <flag>` | Before any edit, record the git commit, build, test and lint results and the flag's names | 0, 2 usage error |
213
+ | `verify <flag>` | Gate the removal with the [checks above](#what-verify-checks) | 0 pass, 1 fail, 2 usage error |
214
+ | `stale [flags...]` | Ask LaunchDarkly which flags are ready to remove from code | 0 evaluated, 2 usage error, 3 LaunchDarkly unreachable or refused the request (token, access, project key) |
215
+ | `hook stop` | The Claude Code Stop hook installed by `init` | always 0 |
216
+
217
+ `list`, `baseline`, `verify` and `stale` accept `--json`, `--config <path>`, and
218
+ `--include` / `--exclude <glob>`.
219
+
220
+ <details>
221
+ <summary>Command details and options</summary>
222
+
223
+ - **`init`**: asks which tools to set up, or `--tool claude-code,copilot,codex`;
224
+ saved as `tools:`. Never overwrites or deletes.
225
+ - **`doctor`**: checks the build, test and lint commands (marking adapter
226
+ defaults; `--run` runs them and fails when tests write no readable
227
+ `testResults`), the .NET SDK `global.json` asks for, `cargo` and Docker
228
+ where projects need them, git, `.gitignore`, installed skills and hook, and
229
+ abandoned baselines.
230
+ - **`scope`**: for each .NET project, shows what the machine lacks and which
231
+ projects a solution filter would leave out; `--write` creates
232
+ `flagrm.slnf`, adds it to `.git/info/exclude` and points the project's build
233
+ and test at it. See [When the machine can't build everything](#when-the-machine-cant-build-everything).
234
+ - **`update`**: refreshes files from this flagrm version, for the config's
235
+ `tools:`. It replaces the `flagrm-verify` skill and prompt file of earlier
236
+ versions with `flagrm-review`, deleting them only when flagrm installed
237
+ them.
238
+ - **`list`**: flow names include renamed imports of a flag registry
239
+ (`import { FeatureFlags as AppFeatureFlags }`) and, in Angular, the fields,
240
+ getters, methods and locals holding a flag and a child component's input
241
+ bound to one.
242
+ - **`baseline <flag>`**: writes `.flagrm/<flag>/baseline.json`. In .NET and
243
+ Angular code the names include the locals, fields, getters, parameters and
244
+ methods the flag's value travels through when they name the flag (a local,
245
+ parameter or field is checked only in its own file, and an Angular
246
+ component's template); generic or ambiguous ones are listed as
247
+ `suggestedNames`, with `parameterizedTests` that pass the value as a
248
+ literal.
249
+ - `--name X --kind alias|wrapper` adds names to an existing baseline (a name
250
+ whose declaration reads only other flags is refused); `--file`
251
+ (repeatable) limits `--name` entries to those files (and records them as
252
+ wrappers).
253
+ - `--json` prints a summary (test counts, at most 20 failed test names; the
254
+ file keeps everything).
255
+ - `--force` starts over, but only while the code is unedited since the
256
+ baseline commit.
257
+ - `--accept config,failures` acknowledges, also mid-removal, a config change
258
+ or the baseline's failing tests so verify stops warning about them.
259
+ - `--no-checks` skips build, tests and lint. Build, test and lint results of
260
+ a passing verify on the same tree and commands are reused (`--no-reuse`
261
+ runs them).
262
+ - **`verify <flag>`**: writes `.flagrm/<flag>/verify.json`. `--md` prints the
263
+ overview, `--json` the full result, `--skip` skips checks, `--strict` fails
264
+ on warnings. Reuses its own or another flag's passing runs on the same tree
265
+ and commands (`--no-reuse` runs them).
266
+ - **`stale [flags...]`**: reads the config's `launchdarkly:` section
267
+ (read-only) and sorts every flag, or the LaunchDarkly keys or code names
268
+ given, into `remove`, `unreferenced` or `keep` with reasons. `--md` prints a
269
+ markdown summary, `--json` the full result. See
270
+ [LaunchDarkly and CI](#launchdarkly-and-ci).
271
+
272
+ </details>
147
273
 
148
274
  ## Configuration
149
275
 
@@ -182,38 +308,66 @@ path. `build` and `test` are resolved one at a time:
182
308
  | a command | that command |
183
309
  | `false` | none, not even the default |
184
310
 
185
- `testResults` (JUnit XML, TRX or Jest JSON; a path or a glob) lets `verify`
186
- compare individual tests against the baseline. Only files written during
187
- the run are read, and every matching file is merged, so a sharded run can
188
- use `./test-results/*.xml`. The default .NET test command writes TRX to
189
- `.flagrm/test-results/<project>/`. So does the default Angular test command,
190
- when the workspace's runner can write results without setup: JUnit for
191
- Angular's Vitest builder and Vitest, Jest JSON for Jest, and JUnit for Karma
192
- once karma.conf configures `karma-junit-reporter`. A `test` command of your
193
- own needs its own `testResults`, and `flagrm doctor` says what to add.
194
- Without result files, `verify` reads failing test names and counts from the
195
- test log and deleted tests from the diff, which is coarser: passing tests
196
- aren't named. `timeout` (seconds) fails a project's build or test command
197
- that runs longer, for example a test runner left in watch mode. `tools` lists
198
- the AI coding tools whose files `init` and `update` manage (`claude-code`,
199
- `copilot`, `codex`); dropping one leaves its files in place, and `init` names
200
- them so you can delete them.
201
-
202
- `lint` is an optional lint or type-check command (no adapter default) that
203
- `baseline` and `verify` run like `build` and `test`; use it for code the build
204
- and tests don't reach, such as end-to-end specs. A lint that already fails at
205
- the baseline only warns, unless the removal adds error lines. flagrm doesn't
206
- check formatting: put a formatter's check (`npx prettier --check .`) in
207
- `lint` if you want it. `covers` lists the files (globs relative to `path`) the
208
- project's commands check; the `coverage` check warns about a changed file no
209
- project with a command covers. Without `covers`, a project covers everything
210
- under its path.
211
-
212
- Paths matching `exclude` (relative to the config file or a project's path; a
213
- directory excludes everything under it) and flagrm's own setup files don't
214
- count as part of a removal, so generated output such as `graphify-out/` that
215
- changes during a session doesn't invalidate a passing verify or block the Stop
216
- hook. `--exclude` on the command line doesn't reach the Stop hook.
311
+ | Key | What it does |
312
+ |---|---|
313
+ | `testResults` | JUnit XML, TRX or Jest JSON (a path or a glob), so `verify` compares individual tests against the baseline |
314
+ | `lint` | Optional lint or type-check command, run like `build` and `test` |
315
+ | `covers` | Globs (relative to `path`) of the files the project's commands check |
316
+ | `timeout` | Seconds after which a build or test command fails, for example a test runner left in watch mode |
317
+ | `tools` | The AI coding tools whose files `init` and `update` manage (`claude-code`, `copilot`, `codex`) |
318
+ | `exclude` | Paths that don't count as part of a removal |
319
+
320
+ **`testResults`.** Only files written during the run are read, and every
321
+ matching file is merged, so a sharded run can use `./test-results/*.xml`. The
322
+ default .NET test command writes TRX to `.flagrm/test-results/<project>/`. So
323
+ does the default Angular test command, when the workspace's runner can write
324
+ results without setup: JUnit for Angular's Vitest builder and Vitest, Jest JSON
325
+ for Jest, and JUnit for Karma once karma.conf configures
326
+ `karma-junit-reporter`. A `test` command of your own needs its own
327
+ `testResults`, and `flagrm doctor` says what to add. Without result files,
328
+ `verify` reads failing test names and counts from the test log and deleted
329
+ tests from the diff, which is coarser: passing tests aren't named.
330
+
331
+ **`lint`** has no adapter default. Use it for code the build and tests don't
332
+ reach, such as end-to-end specs. A lint that already fails at the baseline only
333
+ warns, unless the removal adds error lines. flagrm doesn't check formatting:
334
+ put a formatter's check (`npx prettier --check .`) in `lint` if you want it.
335
+
336
+ **`covers`.** The `coverage` check warns about a changed file no project with a
337
+ command covers. Without `covers`, a project covers everything under its path.
338
+
339
+ **`tools`.** Dropping a tool leaves its files in place, and `init` names them so
340
+ you can delete them.
341
+
342
+ **`exclude`.** Paths matching `exclude` (relative to the config file or a
343
+ project's path; a directory excludes everything under it) and flagrm's own
344
+ setup files don't count as part of a removal, so generated output such as
345
+ `graphify-out/` that changes during a session doesn't invalidate a passing
346
+ verify or block the Stop hook. `--exclude` on the command line doesn't reach
347
+ the Stop hook.
348
+
349
+ ### LaunchDarkly
350
+
351
+ An optional `launchdarkly:` section lets `flagrm stale` read the project's
352
+ flags from LaunchDarkly (see [LaunchDarkly and CI](#launchdarkly-and-ci)):
353
+
354
+ ```yaml
355
+ launchdarkly:
356
+ project: shop # required: the LaunchDarkly project key
357
+ baseUrl: https://app.launchdarkly.com # default; EU and federal instances use their own
358
+ tokenEnv: LD_API_TOKEN # default; the variable holding the API token
359
+ environments: [production, staging] # default: every environment of the project
360
+ minDaysOn: 30 # default: days every environment must have served true
361
+ temporaryOnly: true # default: only flags marked temporary qualify
362
+ tags: { include: [], exclude: [permanent] } # must have every include tag and no exclude tag
363
+ map: { checkout-v2: NewCheckout } # LaunchDarkly key → code name, when the spelling differs
364
+ ```
365
+
366
+ The API token is read only from the variable `tokenEnv` names; a `token` or
367
+ `apiKey` in the file is an error. It is sent only to `baseUrl`, which must use
368
+ `https` (plain `http` only to `localhost`). A reader role is enough: flagrm
369
+ never writes to LaunchDarkly. Without the section nothing changes, and
370
+ `flagrm doctor` warns when the token variable isn't set.
217
371
 
218
372
  ### When the machine can't build everything
219
373
 
@@ -238,6 +392,69 @@ For .NET, keep `--no-incremental` in `build`. An incremental build skips
238
392
  up-to-date projects and prints none of their warnings, so the dead-code check
239
393
  would have no baseline warnings to compare against.
240
394
 
395
+ ## LaunchDarkly and CI
396
+
397
+ With a [`launchdarkly:` section](#launchdarkly) in the config, `flagrm stale`
398
+ reads the project's flags from LaunchDarkly and compares them with the code
399
+ (`flagrm list`'s inventory). Each flag gets one verdict:
400
+
401
+ | Verdict | Meaning |
402
+ |---|---|
403
+ | `remove` | Meets every condition, and one code name in this repository matches it |
404
+ | `unreferenced` | No code name in this repository matches it. Another repository may still read it, so this is not a suggestion to archive it |
405
+ | `keep` | Anything else, with the reasons: still targeted somewhere, serving `false`, on for too few days, permanent, a tag, an ambiguous name, data flagrm couldn't read, or a code name with no LaunchDarkly flag (archived or deleted: the SDK serves the code's fallback, which may be OFF) |
406
+
407
+ A flag meets the conditions when it is boolean, every checked environment
408
+ serves `true` to every context (no rules, individual targets, prerequisites or
409
+ rollouts; or targeting off with `true` as the off variation) and has for at
410
+ least `minDaysOn` days, it is marked temporary (unless `temporaryOnly: false`),
411
+ and its tags pass `tags`. The days count from the latest targeting change in
412
+ the audit log; a name, description or tag edit doesn't reset them. Without one,
413
+ they count from the environment's last change of any kind (marked `*`), which
414
+ can only make a flag wait longer. Set `environments` when the project has
415
+ per-developer or short-lived environments: by default every environment must
416
+ serve `true`. A LaunchDarkly key matches a code name through `map`, then by
417
+ exact name, then ignoring case and `-`, `_`, `.` and spaces (`new-checkout`
418
+ matches `NewCheckout`); two possible matches are reported, not guessed.
419
+
420
+ `stale` exits 0 whenever it could evaluate, whatever the verdicts: read them
421
+ from `--json`. Exit 2 is a usage or config error (no section, no token, an
422
+ unknown environment or flag name). Exit 3 means LaunchDarkly couldn't be
423
+ reached or refused the request: it rejected the token, the token may not read
424
+ the project, or it doesn't know the project key. A wrong project key is
425
+ reported this way because LaunchDarkly gives the same answer when the token
426
+ belongs to another organization or `baseUrl` points at another instance. A
427
+ token with LaunchDarkly's reader role is enough; flagrm only sends GET
428
+ requests.
429
+
430
+ When the config has `launchdarkly:`, the `flagrm-remove` skill runs
431
+ `flagrm stale <flag> --json` instead of asking whether a flag set only in
432
+ LaunchDarkly is ON everywhere, so a removal can run without anyone there.
433
+ [`examples/ci/github-actions.yml`](examples/ci/github-actions.yml) and
434
+ [`examples/ci/azure-pipelines.yml`](examples/ci/azure-pipelines.yml) run it on
435
+ a weekly schedule: they list the `remove` flags, run a headless Claude Code
436
+ agent per flag, run `flagrm verify` themselves and open a pull request only
437
+ when it passes. The agent job needs the project's build toolchain, and the
438
+ pull request is for a person to review.
439
+
440
+ Before the pull request opens, a second agent runs `/flagrm-review` in its own
441
+ process, without the removal's context, as an independent review. It can't
442
+ edit the tree (no Edit or Write tool, and Bash only for `flagrm verify` and
443
+ `git diff`), and it reuses verify's build and test runs, so it costs agent
444
+ tokens but no rebuild. Its findings go into the pull request in a collapsed
445
+ "Independent review" block. The review is advisory: when it fails, the pull
446
+ request still opens and says so. Delete the step if you don't want it.
447
+
448
+ ## Separate repositories
449
+
450
+ flagrm works per git repository: the baseline is a commit of one repository,
451
+ and the config, `.flagrm/`, skills and hook live in its root. With the UI and
452
+ the backend in separate repositories, run `flagrm init` in each (the Angular
453
+ adapter in one, .NET in the other) and run `/flagrm-remove <flag>` in each.
454
+ flagrm does not coordinate the two: each repository gets its own verdict, the
455
+ agent has to record the flag's name in each (it may differ between UI and
456
+ API), and the order of the two removals is up to you.
457
+
241
458
  ## Development
242
459
 
243
460
  ```sh