`optimize --subtract` has only ever proposed. `--apply` executes the blocks the operator picks, behind a backup whose coverage is verified and a scope gate the engine enforces rather than describes. The open design decision from plan §C6 was settled by two measurements, not by taste. It is NOT a fix-engine action: the subtraction axis appears nowhere in scan-orchestrator or optimization-lens-scanner, so verifyFixes' re-scan would mark every removal `verified` whether or not it happened -- a success-shaped no-op, the same shape that made restoreBackup silently do nothing. It is NOT a plan/implement step either: that pipeline needs a finding code, and OPT declares exactly one, for the deterministic check. The approval artifact is written by main context, not by the lens agent. That is where the operator's decision actually happens, and it keeps the feature off the still-unmeasured agent write surface (M-BUG-18 lists optimize as open). Three properties are load-bearing, and each was seen red against its own defect: removals validate against the ORIGINAL content and apply in descending line order; the range check is not redundant with the text check (`line: 0` makes `slice(-1, 0)` empty, so an empty text MATCHES and `splice(-1, 1)` deletes the file's last line); and createBackup skips a nonexistent path while still returning an id, so manifest coverage is asserted before a byte changes. Two guards were green on their own defect and were fixed after measuring: `/\b80\s*%\b/` never matches "80% of the file" -- `%` is a non-word character, so the trailing `\b` demands a word character next. And the caller-arm sweep passed vacuously against HEAD, iterating an empty list; only the added non-emptiness assertion caught it. The floor is repeated, not moved: floor-exclusion still vetoes before anything is proposed, and the engine refuses a load-bearing block again so a hand-built approval cannot route around it. `mv` to `_archive/` is a file-level rule and does not apply to a block excision -- the timestamped backup is the recovery artifact, and a second copy with no restorer would be worse than none. strongestGate moves into write-scope.mjs so the gate ordering has one owner. Dogfooded DRY-RUN against the real ~/.claude/CLAUDE.md: 29 candidates, gate refused all 29 with exit 0 until the scope was approved, then 29/29 spans validated with nothing written. ~789 tokens, ~18% of the file -- corroborating the #40 fasit's ~850, and well short of what a deletion feature is tempted to promise. Suite 1625 -> 1659/0. Frozen v5.0.0 and default-output baselines untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017A6vrtPKsVuM4DJ27p7jzw
114 lines
12 KiB
Markdown
114 lines
12 KiB
Markdown
# Config-Audit Plugin
|
|
|
|
Claude Code Configuration Intelligence — know if your config is correct, find what could improve it, fix it automatically. Three pillars: **Health** (deterministic scanners), **Opportunities** (context-aware recommendations), **Action** (auto-fix with backup/rollback).
|
|
|
|
Per-command flags, patterns, and feature lists live in `README.md` and `/config-audit help`. This file carries what's invariant for working on the plugin.
|
|
|
|
**Positioning vs. built-in `/doctor` (measured 2026-08-03, binding):** we are the deterministic/reproducible/all-scope/zero-quota side; `/doctor` is usage-weighted one-shot judgment. Never build a feature whose whole value is duplicating a `/doctor` check — see README «config-audit vs. the built-in /doctor» and `docs/v5.13-model-routing-effort-deadref-plan.md` §A.
|
|
|
|
## Commands
|
|
|
|
### Core (just run `/config-audit` to get started)
|
|
|
|
| Command | Description |
|
|
|---------|-------------|
|
|
| `/config-audit` | Full audit with auto-scope detection |
|
|
| `/config-audit posture` | A-F health scorecard (10 quality areas) |
|
|
| `/config-audit tokens` | Prompt-cache-aware token hotspots, each tagged with its load pattern; cache-aware |
|
|
| `/config-audit manifest` | Ranked table of every token source + always-loaded subtotal |
|
|
| `/config-audit feature-gap` | Context-aware feature recommendations grouped by impact |
|
|
| `/config-audit optimize` | Mechanism-fit lens (procedure→skill, lifecycle→hook, path→rule, never→permission). Agent-driven, **not byte-stable**. `--subtract` adds the subtraction axis (what no longer earns its always-loaded rent, `BP-SUB-001`) — opt-in, proposes only; `--subtract --apply` executes the removals the operator picks |
|
|
| `/config-audit fix` | Auto-fix deterministic issues with backup + verification |
|
|
| `/config-audit rollback` | Restore configuration from backup |
|
|
| `/config-audit plan` | Create action plan from findings |
|
|
| `/config-audit implement` | Execute plan with backups + auto-verify |
|
|
| `/config-audit help` | Show all commands |
|
|
|
|
### Additional
|
|
|
|
| Command | Description |
|
|
|---------|-------------|
|
|
| `/config-audit drift` | Compare current config against saved baseline |
|
|
| `/config-audit plugin-health` | Audit plugin structure, frontmatter, cross-plugin coherence |
|
|
| `/config-audit whats-active` | Read-only inventory of active plugins/skills/agents/MCP/hooks/CLAUDE.md (with token estimates, and `model`/`effort` per agent) |
|
|
| `/config-audit knowledge-refresh` | Refresh the best-practices register (stale check + web poll). Human-approved writes; **not byte-stable** |
|
|
| `/config-audit campaign` | Machine-wide audit ledger + token bill across repos. Human-approved writes; **not byte-stable** |
|
|
| `/config-audit discover` | Run discovery phase only |
|
|
| `/config-audit analyze` | Run analysis phase only |
|
|
| `/config-audit interview` | Gather user preferences (opt-in) |
|
|
| `/config-audit status` | Show current session state |
|
|
| `/config-audit cleanup` | Clean up old sessions |
|
|
|
|
## Agents
|
|
|
|
| Agent | Role | Model | Color | Tools |
|
|
|-------|------|-------|-------|-------|
|
|
| scanner-agent | Find config files | sonnet | cyan | Read, Glob, Grep, Write |
|
|
| analyzer-agent | Generate report | sonnet | blue | Read, Glob, Grep, Write |
|
|
| planner-agent | Create action plan | opus | yellow | Read, Glob, Write |
|
|
| implementer-agent | Execute changes | sonnet | magenta | Read, Write, Edit, Bash, Glob |
|
|
| verifier-agent | Verify results | sonnet | purple | Read, Glob, Grep |
|
|
| feature-gap-agent | Feature recommendations | opus | green | Read, Glob, Grep, Write |
|
|
| optimization-lens-agent | Mechanism-fit precision gate | opus | orange | Read, Glob, Grep, Write |
|
|
|
|
## Hooks
|
|
|
|
| Event | Script | Purpose |
|
|
|-------|--------|---------|
|
|
| PreToolUse | `auto-backup-config.mjs` | Backup config files before Edit/Write |
|
|
| PostToolUse | `post-edit-verify.mjs` | Verify after Edit/Write, block on new critical/high |
|
|
| SessionStart | `session-start.mjs` | Check for active (unfinished) sessions |
|
|
| Stop | `stop-session-reminder.mjs` | Remind about current session phase |
|
|
|
|
## Reference docs (read on demand)
|
|
|
|
- `docs/scanner-internals.md` — scanner inventory, lib modules, action engines, knowledge base, per-scanner/per-block implementation notes (design rationale, primary-source verification, byte-stability lessons)
|
|
- `docs/humanizer.md` — plain-language output (v5.1.0), humanizer vocabularies, output modes
|
|
|
|
## Plain-Language Output (v5.1.0)
|
|
|
|
Default output of all commands routes through `humanizeEnvelope` (`lib/humanizer.mjs`), decorating each finding with `userImpactCategory`, `userActionLanguage`, and `relevanceContext`. `--raw` and `--json` bypass the humanizer for byte-stable v5.0.0 output. Full detail: `docs/humanizer.md`.
|
|
|
|
## Suppressions
|
|
|
|
Create `.config-audit-ignore` at project root — one exact ID or glob per line (`CA-SET-003`, `CA-GAP-*`). Suppressed findings are tracked in the envelope's `suppressed_findings` for audit trail. Disable with `--no-suppress`.
|
|
|
|
## Architecture
|
|
|
|
Workflow: `/config-audit → discover + analyze (auto) → plan → implement → verify`. Auto-detects scope from git context; override with `full|repo|home|current`; `--delta` for incremental. Session state lives under `~/.claude/config-audit/sessions/{id}/` (scope.yaml, discovery.json, state.yaml, findings/, analysis-report.md, action-plan.md, backups/, implementation-log.md).
|
|
|
|
Finding ID format: `CA-{SCANNER}-{NNN}` — e.g. `CA-CML-001`, `CA-SET-003`, `CA-HKV-002`, `CA-RUL-005`, `CA-TOK-005`, `CA-CPS-001`, `CA-SKL-001`, `CA-OST-001`, `CA-OPT-001`, `CA-AGT-001`.
|
|
|
|
**GAP dimensions vs. levers (invariant).** `GAP_CHECKS` holds the 24 *dimensions* — always evaluated, always counted in the utilization denominators (`TIER_COUNTS` / `TOTAL_DIMENSIONS` in `scoring.mjs`, and `TITLE_TO_ID` there). A *lever* is a finding the scanner emits after the loop and only under a measured condition; it carries no tier, never enters those denominators, and is registered in the exported `LEVERS` object (code + title in one place, because the finding-code guard needs the code and the humanizer-coverage guard needs the title). Adding a dimension moves every user's utilization score and can flip the reported `segment` in a frozen baseline — adding a lever cannot. When a check is only meaningful for configs that already have some feature, it is a lever.
|
|
|
|
**`{NNN}` names the CHECK, never the emission position (invariant).** `scanners/lib/finding-codes.mjs` is the single authority: every `finding()` call passes a `code`, and an undeclared or missing one **throws** — there is no counter fallback, because a fallback lets a half-converted scanner ship IDs that look valid. Adding a check takes the next free number for that scanner, never the next source-order position; removing one moves its key to `RETIRED_CODES` and its number is never reissued. IDs are therefore **not unique per finding** — one check failing in three files emits three findings sharing an ID, and `(id, file, line)` is the instance key that `fix-engine` verification uses. Frozen `v5.0.0` baselines mask IDs (`tests/helpers/mask-finding-ids.mjs`) instead of re-deriving them; the check→number pairs are pinned exhaustively in `tests/lib/finding-codes.test.mjs`.
|
|
|
|
## Conventions
|
|
|
|
Enforced conventions live in `.claude/rules/` (auto-loaded as project instructions):
|
|
- `ux-rules.md` — output/narration/formatting for all commands (never dump raw JSON, narrate before each step, space-separated command suggestions)
|
|
- `command-development.md` — required command frontmatter + `plugin:action` naming
|
|
- `agent-development.md` — agent frontmatter + "when to use" conventions
|
|
- `state-management.md` — update `state.yaml` after every workflow phase
|
|
|
|
Coding style: scanners are zero-dependency Node ESM; new findings use the `CA-{SCANNER}-{NNN}` ID format; byte-stable CLIs are verified against frozen `tests/snapshots/v5.0.0/` baselines.
|
|
|
|
**Write-scope gate (invariant).** Every write target is classified by `scanners/lib/write-scope.mjs` before it reaches an approval surface, and the **scope class decides the gate's strength — never the command asking**. Five command-owned policies would drift apart the way five copies of the lever table did. `SCOPE_CLASSES` is the single source for class, gate (`silent`/`disclose`/`require-ok`), wording and predicate; templates render `disclosures[]` from `write-scope-cli.mjs` rather than restating what a class means. Two orderings in that object are load-bearing and were measured, not reasoned about: `plugin-managed` before `user-scope` (both `~/.claude/config-audit/` and legacy `~/.config-audit/` are live, so the other order fires the gate on every session write and gets it switched off), and `user-scope` before `cross-repo` (`~/.claude/.git` exists, so a plain `.git`-upward walk calls `~/.claude/CLAUDE.md` merely "another repo" and silently downgrades the strongest gate). `disclose` ≠ `require-ok`: `campaign export` is cross-repo *by design*, so tightening it into a refusal breaks the feature. Distinct from the `require-target-dir.mjs` guard, which asks whether a scan **root** is readable (exit 3) — a different invariant, not to be merged.
|
|
|
|
**Subtraction floor (invariant).** `optimize --subtract` is the only lens that proposes removing config, so `scanners/lib/floor-exclusion.mjs` runs as a deterministic pre-step *before* the judge — a load-bearing block is never a candidate, and that guarantee must not be moved into the agent prompt. Two rules follow from it: (1) **staleness is not a deletion signal** — an outdated version pin inside a floor block is a `drift`/`CA-CML` dead-reference concern; (2) **tier 2 ≠ tier 3** — a compensatory block that keeps earning its place returns, and reporting it as dead weight is wrong even when the label matches. Norwegian keywords need the Unicode boundaries in `subtraction-prefilter.mjs`; JS `\b` is ASCII-only, so `/\bunngå\b/` silently never matches.
|
|
|
|
**Subtraction write path (invariant).** `--apply` routes through `scanners/lib/subtraction-write.mjs`, never through `fix-engine` or the `plan`/`implement` pipeline, and both exclusions are **measured**: the subtraction axis is absent from the orchestrated envelope, so `verifyFixes`' re-scan would mark every removal `verified` whether or not it happened (a success-shaped no-op), and the findings pipeline needs a finding code — which names a deterministic check, not a prose judgement. Three properties are load-bearing and each has a guard seen red against its own defect: removals are validated against the ORIGINAL content and applied in **descending** line order (an ascending pass shifts later spans out from under themselves); the **range** check is not redundant with the text check (`line: 0` makes `slice(-1, 0)` empty, so an empty `text` matches and `splice(-1, 1)` deletes the file's LAST line); and `createBackup` skips a nonexistent path while still returning an id, so coverage of every file about to be written is **asserted from the manifest** before a byte changes. The floor is *repeated* here, not moved: `floor-exclusion` still vetoes before anything is proposed, and the engine refuses a load-bearing block again so a hand-built approval cannot route around it. The archive rule (`mv` to `_archive/`) is file-level and does not apply to a block excision — the timestamped backup is the recovery artifact, and inventing a second copy with no restorer behind it would be worse than none.
|
|
|
|
## Testing
|
|
|
|
```bash
|
|
node --test 'tests/**/*.test.mjs'
|
|
```
|
|
|
|
Test fixtures in `tests/fixtures/`. Per-scanner and per-build-block implementation notes (design rationale, primary-source verification, byte-stability lessons) live in `docs/scanner-internals.md` → **Implementation notes**.
|
|
|
|
## Gotchas
|
|
|
|
- Session directories accumulate — use `/config-audit cleanup` to manage
|
|
- Scanners run on Node.js ≥ 18 (uses node:test, node:fs/promises)
|
|
- Plugin CLAUDE.md files in node_modules should be excluded via scope
|