From 7b3b487d2683da240dcfbb7933af41745490b967 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sun, 21 Jun 2026 14:01:18 +0200 Subject: [PATCH] =?UTF-8?q?feat(opt):=20optimization=20lens=20Chunk=202b?= =?UTF-8?q?=20=E2=80=94=20opus=20prose-judgment=20analyzer=20+=20/config-a?= =?UTF-8?q?udit=20optimize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recall+precision halves of the CA-OPT hybrid motor for the three mechanism-fit cases the deterministic OPT scanner (2a) deliberately skips: lifecycle→hook (BP-MECH-001), unscoped path-specific→rule (BP-MECH-002), absolute "never"→permission (BP-MECH-004). New: - scanners/lib/lens-prefilter.mjs — cheap, recall-oriented line scan of the CLAUDE.md body; detector names mirror the register lensCheck fields; skips fenced code, gates the path class on an instruction verb. Pure + 13 tests. - scanners/optimize-lens-cli.mjs — discovery + OPT scanner + pre-filter; attaches only the CONFIRMED register entry to each candidate (unverifiable → dropped, Verifiseringsplikt); emits {deterministic, candidates, register, counts}. - agents/optimization-lens-agent.md — opus precision gate (7th agent, orange): reads the real CLAUDE.md, drops low-confidence candidates, keeps only genuine opportunities, cites register id + source. - commands/optimize.md — /config-audit optimize orchestrates pre-filter→agent→report. Agent-driven → deliberately NOT byte-stable (own command, outside the snapshot suite). No new orchestrated scanner → scanner count stays 15. Counts: agents 6→7, commands 18→19, suite 1055→1068. Self-audit A/A unchanged, readmeCheck passed (clean HOME). Plan: docs/v5.7-optimization-lens-plan.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 33 ++++-- README.md | 14 +-- agents/optimization-lens-agent.md | 120 ++++++++++++++++++++++ commands/config-audit.md | 3 +- commands/help.md | 1 + commands/optimize.md | 123 +++++++++++++++++++++++ scanners/lib/lens-prefilter.mjs | 114 +++++++++++++++++++++ scanners/optimize-lens-cli.mjs | 160 ++++++++++++++++++++++++++++++ tests/lib/lens-prefilter.test.mjs | 132 ++++++++++++++++++++++++ 9 files changed, 687 insertions(+), 13 deletions(-) create mode 100644 agents/optimization-lens-agent.md create mode 100644 commands/optimize.md create mode 100644 scanners/lib/lens-prefilter.mjs create mode 100644 scanners/optimize-lens-cli.mjs create mode 100644 tests/lib/lens-prefilter.test.mjs diff --git a/CLAUDE.md b/CLAUDE.md index 22c785a..35a40a7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,7 @@ Analyzes and optimizes Claude Code configuration across three pillars: | `/config-audit tokens` | prompt-cache-aware token hotspots (6 patterns: cache-breaking, redundant perms, deep imports, oversized cascade, bloated SKILL.md desc, MCP tool-schema budget), each ranked hotspot tagged with its load pattern (always / on-demand / external) — optional `--accurate-tokens` API calibration, `--with-telemetry-recipe` cache-hit recipe pointer | | `/config-audit manifest` | Ranked table of every token source (CLAUDE.md, rules, agents, skills, output styles, MCP, hooks) sorted by estimated tokens, each tagged with its load pattern (always-loaded / on-demand / external) + an always-loaded subtotal ("tokens that enter context every turn") | | `/config-audit feature-gap` | Context-aware feature recommendations grouped by impact (incl. a conditional `disableBundledSkills` lever when the active skill listing is over budget — remediation companion to SKL `CA-SKL-002`) | +| `/config-audit optimize` | Optimization lens (mechanism-fit) — config that works but fits a better mechanism: procedure→skill (CA-OPT-001, deterministic), lifecycle→hook / unscoped path→rule / "never"→permission (prose-judgment via opus `optimization-lens-agent`). Hybrid motor; every finding cites a best-practices-register rule. Agent-driven, **not byte-stable** | | `/config-audit fix` | Auto-fix deterministic issues with backup + verification | | `/config-audit rollback` | Restore configuration from backup | | `/config-audit plan` | Create action plan from audit findings | @@ -49,6 +50,7 @@ Analyzes and optimizes Claude Code configuration across three pillars: | implementer-agent | Execute changes | sonnet | magenta | Read, Write, Edit, Bash, Glob | | verifier-agent | Verify results | sonnet | purple | Read, Glob, Grep | | feature-gap-agent | Context-aware feature recommendations | opus | green | Read, Glob, Grep, Write | +| optimization-lens-agent | Mechanism-fit precision gate (prose-judgment lens cases) | opus | orange | Read, Glob, Grep, Write | ## Hooks @@ -109,7 +111,7 @@ Default: auto-detects scope from git context. Override with `/config-audit full| node --test 'tests/**/*.test.mjs' ``` -1055 tests across 59 test files (18 lib + 31 scanner + 1 hook + 1 agent + 3 commands + 1 knowledge + 4 top-level). Test fixtures in `tests/fixtures/`. Top-level humanizer tests: `json-backcompat.test.mjs`, `raw-backcompat.test.mjs`, `scenario-read-test.test.mjs`, `snapshot-default-output.test.mjs`. +1068 tests across 60 test files (19 lib + 31 scanner + 1 hook + 1 agent + 3 commands + 1 knowledge + 4 top-level). Test fixtures in `tests/fixtures/`. Top-level humanizer tests: `json-backcompat.test.mjs`, `raw-backcompat.test.mjs`, `scenario-read-test.test.mjs`, `snapshot-default-output.test.mjs`. ### active-config-reader — load-pattern model + rule/agent/output-style enumeration (v5.6 Foundation) @@ -366,11 +368,30 @@ First detector of the «optimal?» axis (vs «correct?»). `optimization-lens-sc best-practices register and flags config that works but fits a better mechanism. **`CA-OPT-001`** (low, *Missed opportunity*): a CLAUDE.md procedure (≥6 consecutive numbered steps) that belongs in a skill — recommendation/provenance from register `BP-MECH-003`. Conservative (negative corpus = null -false-positive); prose-judgment cases (lifecycle→hook, «never»→permission) are deferred to the -Chunk 2b opus analyzer. Wiring mirrors OST: orchestrator entry, humanizer `OPT:'Missed opportunity'` -+ family, scoring `OPT:'CLAUDE.md'` (existing area → no new posture row → byte-stable), strip-helper -`OPT`, SC-5 regenerated (additive). **Test-isolation note:** the TOK test reads the real `~/.claude` -(non-hermetic) — run the suite with a clean `HOME` for deterministic results (pre-existing). +false-positive); prose-judgment cases (lifecycle→hook, unscoped path→rule, «never»→permission) are +handled by the Chunk 2b opus analyzer (below). Wiring mirrors OST: orchestrator entry, humanizer +`OPT:'Missed opportunity'` + family, scoring `OPT:'CLAUDE.md'` (existing area → no new posture row → +byte-stable), strip-helper `OPT`, SC-5 regenerated (additive). + +### Optimization lens Chunk 2b — opus analyzer (prose-judgment half, `/config-audit optimize`) + +The hybrid motor's recall + precision halves for the three cases the deterministic OPT scanner skips. +**Pre-filter** (`scanners/lib/lens-prefilter.mjs`, pure + tested): cheap, recall-oriented line scan +of CLAUDE.md body for lifecycle phrasing (`BP-MECH-001`→hook), unscoped path-specific instructions +(`BP-MECH-002`→rule), and absolute «never» prohibitions (`BP-MECH-004`→permission); skips fenced +code, gates the path class on an instruction verb. Detector names = the register `lensCheck` fields. +**CLI** (`optimize-lens-cli.mjs`, `-cli` → not a scanner): runs discovery + OPT scanner + pre-filter, +attaches the **confirmed** register entry to each candidate (unverifiable → dropped, Verifiseringsplikt), +emits `{deterministic, candidates, register, counts}`. **Agent** (`optimization-lens-agent`, opus, +orange — the 7th agent, **precision gate**): reads the real CLAUDE.md, drops low-confidence candidates, +keeps only genuine opportunities, cites register id + source. **Command** `/config-audit optimize` +orchestrates pre-filter→agent→report. **Agent-driven → deliberately NOT byte-stable** (own command, +outside the snapshot suite); the pre-filter lib *is* unit-tested (13 tests). No new orchestrated +scanner → scanner count stays 15; agents 6→7, commands 18→19, suite 1055→1068. + +**Test-isolation fix (this session):** `token-hotspots.test.mjs` `runScanner` now wraps `scan()` in +the shared `withHermeticHome` helper — the suite is green on BOTH a real and a clean `HOME` (the OPT +section's old «run with clean HOME» caveat is resolved). Snapshot/byte tests were already hermetic. ## Gotchas diff --git a/README.md b/README.md index 6d9d847..83fbf6c 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ ![Version](https://img.shields.io/badge/version-5.6.0-blue) ![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple) ![Scanners](https://img.shields.io/badge/scanners-15-cyan) -![Commands](https://img.shields.io/badge/commands-18-green) -![Agents](https://img.shields.io/badge/agents-6-orange) +![Commands](https://img.shields.io/badge/commands-19-green) +![Agents](https://img.shields.io/badge/agents-7-orange) ![Hooks](https://img.shields.io/badge/hooks-4-red) -![Tests](https://img.shields.io/badge/tests-1055+-brightgreen) +![Tests](https://img.shields.io/badge/tests-1068+-brightgreen) ![License](https://img.shields.io/badge/license-MIT-lightgrey) A Claude Code plugin that checks configuration health, suggests context-aware improvements, and auto-fixes issues — `CLAUDE.md`, `settings.json`, hooks, rules, MCP servers, `@imports`, and plugins. 15 deterministic scanners across 10 quality areas, context-aware feature recommendations, auto-fix with backup/rollback, a prompt-cache-aware Token Hotspots scanner with optional API-calibrated `--accurate-tokens` mode, plus cache-prefix stability, dead-tool, cross-plugin collision, and output-style detection. Zero external dependencies. @@ -278,6 +278,7 @@ Your team configuration changes over time. Track it: | `/config-audit tokens` | prompt-cache-aware token hotspots — ranked by estimated waste, each tagged with its load pattern (always / on-demand / external); 6 patterns + optional `--accurate-tokens` API calibration | | `/config-audit manifest` | Ranked table of every token source (CLAUDE.md, rules, agents, skills, output styles, MCP, hooks) sorted by estimated tokens — each tagged with its **load pattern** (always-loaded / on-demand / external) plus an **always-loaded subtotal** ("≈X tokens enter context every turn before you type"). Component-level: no coarse plugin roll-up (it would double-count) | | `/config-audit feature-gap` | Context-aware feature recommendations grouped by impact | +| `/config-audit optimize` | Optimization lens (mechanism-fit): config that works but fits a better mechanism — procedure→skill, lifecycle→hook, unscoped path→rule, "never"→permission. Hybrid motor (deterministic pre-filter + opus precision gate), every finding cites a best-practices-register rule | | `/config-audit fix` | Auto-fix deterministic issues with backup + verification | | `/config-audit rollback` | Restore configuration from a previous backup | | `/config-audit plan` | Generate prioritized action plan from audit findings | @@ -325,7 +326,7 @@ By default, `/config-audit` auto-detects scope from your git context. Override w | `collision-scanner.mjs` | COL | Cross-plugin skill name collisions; user-vs-plugin overlaps | | `skill-listing-scanner.mjs` | SKL | Skill-listing token budget: a single skill description over the ~1,536-char listing cap Claude Code truncates (`CA-SKL-001`), and the summed active-skill descriptions exceeding the ~2%-of-context listing budget (`CA-SKL-002`) | | `output-style-scanner.mjs` | OST | Output-style validation: a custom (user/project) style missing `keep-coding-instructions: true` that silently strips built-in software-engineering instructions (`CA-OST-001`), a plugin style with `force-for-plugin: true` overriding the user's selected `outputStyle` (`CA-OST-002`), and a settings `outputStyle` resolving to no built-in or custom style — dead config (`CA-OST-003`) | -| `optimization-lens-scanner.mjs` | OPT | Optimization lens (mechanism-fit): a multi-step procedure in CLAUDE.md that would fit better as a skill (`CA-OPT-001`) — reads the machine-readable best-practices register, framed as an opportunity, not a failure. The deterministic half of the lens; prose-judgment cases (lifecycle→hook, "never"→permission) are handled by the Chunk 2b analyzer | +| `optimization-lens-scanner.mjs` | OPT | Optimization lens (mechanism-fit): a multi-step procedure in CLAUDE.md that would fit better as a skill (`CA-OPT-001`) — reads the machine-readable best-practices register, framed as an opportunity, not a failure. The deterministic half of the lens; prose-judgment cases (lifecycle→hook, unscoped path→rule, "never"→permission) are judged by the opus `optimization-lens-agent` via `/config-audit optimize` | > **Cross-scanner remediation — diagnosis meets the fix.** SKL diagnoses an over-budget > skill listing (`CA-SKL-002`); GAP prescribes the remedy. When the active skill listing @@ -432,6 +433,7 @@ Six specialized agents collaborate through the audit workflow, each matched to a | **implementer-agent** | Sonnet | Change execution with mandatory backups | Read, Write, Edit, Bash, Glob | | **verifier-agent** | Sonnet | Post-implementation verification | Read, Glob, Grep | | **feature-gap-agent** | Opus | Context-aware feature recommendations | Read, Glob, Grep, Write | +| **optimization-lens-agent** | Opus | Mechanism-fit precision gate — judges prose-judgment lens candidates (lifecycle→hook, path→rule, never→permission), cites the best-practices register | Read, Glob, Grep, Write | ### Orchestration Flow @@ -585,8 +587,8 @@ Reference documents that inform the feature-gap agent and context-aware recommen **Machine-readable register (`best-practices.json`).** Alongside the human-readable documents above, `knowledge/best-practices.json` is a provenance-stamped, schema-validated register of best-practice claims and mechanism-fit rules — each entry carries `source.url`, a `verified` -date, and a `confidence`. It is the source of truth for the forthcoming optimization lens; the -Markdown files remain the human-readable mirror. Loaded and validated by +date, and a `confidence`. It is the source of truth for the optimization lens (OPT scanner + +`/config-audit optimize`); the Markdown files remain the human-readable mirror. Loaded and validated by `scanners/lib/best-practices-register.mjs` (zero-dependency, native JSON). See `docs/v5.7-optimization-lens-plan.md`. diff --git a/agents/optimization-lens-agent.md b/agents/optimization-lens-agent.md new file mode 100644 index 0000000..92357b8 --- /dev/null +++ b/agents/optimization-lens-agent.md @@ -0,0 +1,120 @@ +--- +name: optimization-lens-agent +description: | + Judges CLAUDE.md mechanism-fit for the v5.7 optimization lens (CA-OPT). Reads + deterministic pre-filter candidates and decides, with prose judgement, whether + each is a genuine "you use mechanism X, but Y fits this better" opportunity — + lifecycle phrasing → hook, unscoped path-specific instruction → path-scoped + rule, absolute "never" prohibition → permission. Precision-gated: cites the + best-practices register rule + source, and stays silent when unsure. +model: opus +color: orange +tools: ["Read", "Glob", "Grep", "Write"] +--- + +# Optimization Lens Agent + +You are the **precision gate** of the optimization lens's hybrid motor. A cheap +deterministic pre-filter (`lens-prefilter`) has already surfaced candidate lines +in CLAUDE.md that *might* fit a better mechanism. Your job is to read each +candidate **in its real context** and keep only the genuine opportunities. + +This is the "is the config **optimal?**" axis, not "is it **correct?**" — every +finding is a *Missed opportunity*, never a mistake. The config works as written; +you are pointing at a mechanism that would fit the content better. + +## The judgement you make + +For each candidate, the register rule names the better-fit mechanism. Decide +whether the line is *really* that kind of instruction: + +| lensCheck | Register | Keep it ONLY if the line is… | Better mechanism | +|---|---|---|---| +| `claude-md-lifecycle-phrasing` | BP-MECH-001 | a recurring automation the model is *told* to perform ("after every commit, run X") — something that should happen deterministically, not at the model's discretion | a **hook** (PreToolUse / PostToolUse / Stop) | +| `unscoped-path-specific-instruction` | BP-MECH-002 | a constraint that only applies when a *specific* file/path/glob is touched, sitting in root CLAUDE.md where it loads every turn regardless | a **path-scoped rule** (`.claude/rules/` with `paths:` frontmatter) | +| `never-instruction` | BP-MECH-004 | an *absolute* prohibition — something that must NEVER happen, where relying on the model to remember is the wrong guarantee | a **permission deny rule** or PreToolUse hook | + +## Input + +You receive an `optimize-lens` payload (JSON) with: +- `target` — the repo path. +- `deterministic` — OPT scanner findings already confirmed (CA-OPT-001: + procedure → skill). Report these **as-is**; do not re-judge them. +- `candidates` — pre-filter candidates, each with `file`, `line`, `lensCheck`, + `mechanism`, `signalText`, and a `register` block (`id`, `claim`, + `recommendation`, `severity`, `source`). Only CONFIRMED register rules reach + you. +- `register` — the full confirmed prose-judgment entries, for reference. + +Always **Read the actual CLAUDE.md file(s)** named in the candidates before +judging — `signalText` is one line out of context; the surrounding lines decide +whether it is really lifecycle/path-specific/prohibition phrasing. + +## Precision rules (non-negotiable) + +1. **Keep only high-confidence opportunities.** When the line is ambiguous, + rhetorical, an example, a heading, or already correctly placed (e.g. it is + *inside* a path-scoped rule, or already references a hook) — **drop it**. A + missed suggestion is far cheaper than a wrong one (Verifiseringsplikt). +2. **Never invent a recommendation.** Use the `register.recommendation` and cite + `register.id` + `register.source.url`. If a candidate has no register block, + skip it. +3. **De-duplicate.** If one line yields two candidates (e.g. "never edit + src/config.ts"), pick the single mechanism that fits best and say why, + rather than emitting two findings for one line. +4. **No false urgency.** These are LOW-severity opportunities. Do not imply the + config is broken. + +## Output + +Write `optimization-lens-report.md` to the session directory (≤120 lines). + +```markdown +# Optimization Lens — mechanism-fit + +**Date:** YYYY-MM-DD | **Target:** {repo} +**Confirmed opportunities:** {N kept} · **Candidates reviewed:** {M} · **Dropped (low confidence):** {M-N} + +> The config works as written. These are places where a different Claude Code +> mechanism would fit the content better — usually cheaper per turn or more +> reliable. + +## Procedures → skills (deterministic) + +{For each `deterministic` finding — render title/recommendation verbatim, cite CA-OPT-001 + BP-MECH-003.} + +## Lifecycle → hooks + +{Kept BP-MECH-001 findings. For each:} +**{file}:{line}** — {one-line restatement of the line} + Why: {register.claim, condensed} + Move to: {register.recommendation} + Source: {register.source.url} + +## Path-specific → scoped rules + +{Kept BP-MECH-002 findings, same shape.} + +## Absolute prohibitions → permissions + +{Kept BP-MECH-004 findings, same shape.} + +## What I deliberately left alone + +{Brief, honest: candidates you dropped and why — "line 22 mentions a path but is +a cross-reference, not an instruction." This is the precision gate showing its +work. Keep to a few lines.} +``` + +Omit any section with zero kept findings (except keep the "left alone" note when +you dropped anything). If nothing survived the gate, say so plainly — a clean +CLAUDE.md is a good outcome, not a failure to find problems. + +## Guidelines + +- Frame everything as *opportunities*, never failures. +- Cite the register rule id + source URL on every finding — provenance is the + product. +- Be concrete: name the file and line, and what the replacement mechanism is. +- Prefer dropping a borderline candidate over stretching to keep it. +- Do not recommend a mechanism the project already uses for that exact content. diff --git a/commands/config-audit.md b/commands/config-audit.md index 088fd3e..bbec033 100644 --- a/commands/config-audit.md +++ b/commands/config-audit.md @@ -1,7 +1,7 @@ --- name: config-audit description: Claude Code Configuration Intelligence - audit, analyze, and optimize your configuration -argument-hint: "[posture|tokens|manifest|feature-gap|fix|rollback|plan|implement|help|discover|analyze|interview|drift|plugin-health|whats-active|status|cleanup]" +argument-hint: "[posture|tokens|manifest|feature-gap|optimize|fix|rollback|plan|implement|help|discover|analyze|interview|drift|plugin-health|whats-active|status|cleanup]" allowed-tools: Read, Write, Glob, Grep, Bash, Agent, AskUserQuestion model: opus --- @@ -17,6 +17,7 @@ If a subcommand is provided, route to it: - `tokens` → `/config-audit:tokens` - `manifest` → `/config-audit:manifest` - `feature-gap` → `/config-audit:feature-gap` +- `optimize` → `/config-audit:optimize` - `fix` → `/config-audit:fix` - `rollback` → `/config-audit:rollback` - `plan` → `/config-audit:plan` diff --git a/commands/help.md b/commands/help.md index 22bf6dd..7f0f7e8 100644 --- a/commands/help.md +++ b/commands/help.md @@ -35,6 +35,7 @@ if echo "$ARGUMENTS" | grep -q -- "--raw"; then RAW_FLAG="--raw"; fi | `/config-audit tokens` | prompt-cache token hotspots; optional `--accurate-tokens` API calibration | | `/config-audit manifest` | Ranked table of every system-prompt token source | | `/config-audit feature-gap` | Deep analysis of features you're not using | +| `/config-audit optimize` | Optimization lens — config that works but fits a better mechanism (procedure→skill, lifecycle→hook, path→rule, never→permission) | | `/config-audit fix` | Auto-fix deterministic issues; a copy of every changed file is saved first so you can roll back with one command | | `/config-audit rollback` | Restore configuration from a saved copy | diff --git a/commands/optimize.md b/commands/optimize.md new file mode 100644 index 0000000..a3a0e0f --- /dev/null +++ b/commands/optimize.md @@ -0,0 +1,123 @@ +--- +name: config-audit:optimize +description: Optimization lens — config that works but would fit a better mechanism (procedure→skill, lifecycle→hook, path→rule, never→permission) +argument-hint: "[path]" +allowed-tools: Read, Write, Glob, Grep, Bash, Agent +model: opus +--- + +# Config-Audit: Optimization Lens + +The "is the config **optimal?**" axis (vs. the health scanners' "is it +**correct?**"). It finds configuration that *works* but uses a mechanism a +better one would fit — and frames every one as a *Missed opportunity*, never a +mistake. + +Mechanism-fit rules come from the provenance-stamped best-practices register +(`knowledge/best-practices.json`); only CONFIRMED rules are surfaced. The motor +is hybrid: a cheap deterministic pre-filter finds candidates, then the opus +`optimization-lens-agent` judges each in context (precision-gated). + +## What the user gets + +- **Procedures → skills** (deterministic, CA-OPT-001) +- **Lifecycle phrasing → hooks** (BP-MECH-001) +- **Unscoped path-specific instructions → path-scoped rules** (BP-MECH-002) +- **Absolute "never" prohibitions → permissions / hooks** (BP-MECH-004) + +Each finding cites its register rule + source URL. A clean CLAUDE.md returns "no +opportunities" — that is a good result, not a failure. + +## Implementation + +### Step 1: Determine target + +Split `$ARGUMENTS` into a path (first non-flag argument; default: current working +directory) and flags. Recognized flags: `--global` (include the user `~/.claude` +cascade in discovery). + +Tell the user: + +``` +## Optimization Lens + +Looking for configuration that works but would fit a better Claude Code mechanism... +``` + +### Step 2: Run the lens CLI + +Generate a session ID (`YYYYMMDD_HHmmss`) if no active session exists. + +```bash +mkdir -p ~/.claude/config-audit/sessions/{session-id} 2>/dev/null +GLOBAL_FLAG="" +if echo "$ARGUMENTS" | grep -q -- "--global"; then GLOBAL_FLAG="--global"; fi +node ${CLAUDE_PLUGIN_ROOT}/scanners/optimize-lens-cli.mjs --output-file ~/.claude/config-audit/sessions/{session-id}/optimize-lens.json $GLOBAL_FLAG 2>/dev/null; echo $? +``` + +Exit code 0 is normal. Only exit code 3 is a real error → "The lens couldn't run. +Check that the path exists and contains a CLAUDE.md." + +### Step 3: Read the payload + +Read `~/.claude/config-audit/sessions/{session-id}/optimize-lens.json` with the +Read tool. It has `deterministic` (already-confirmed OPT findings), `candidates` +(pre-filter candidates with register provenance), `register`, and `counts`. + +**Early exit:** if `counts.deterministic === 0` and `counts.candidates === 0`, +skip the agent and tell the user plainly: + +``` +✓ No mechanism-fit opportunities found. + +Your CLAUDE.md holds facts, not procedures/automation/prohibitions that would be +better as skills, hooks, rules, or permissions. Nothing to change here. +``` + +Then go to Step 5. + +### Step 4: Spawn the precision gate + +Tell the user what's happening and set expectations: + +``` +Found {counts.candidates} candidate line(s) + {counts.deterministic} deterministic finding(s). +Asking the optimization-lens agent to judge each in context (~20-40 seconds)... +``` + +Spawn the `optimization-lens-agent` (Agent tool) with: +- the full payload from Step 3 (deterministic + candidates + register), +- the session directory path so it can write `optimization-lens-report.md`. + +The agent reads the actual CLAUDE.md, drops low-confidence candidates, and keeps +only genuine opportunities — each citing its register rule + source. + +### Step 5: Present results + +Read the agent's `optimization-lens-report.md` and present it formatted +(markdown tables / grouped sections). Follow the UX rules: never show raw JSON or +scanner progress; lead with a one-sentence summary of what was found before the +detail. Make clear these are LOW-severity *opportunities*. + +If the agent kept nothing from the candidates (all dropped) but there were +deterministic findings, show those; if it kept nothing at all, show the clean +result from Step 3. + +### Step 6: Next steps + +End with context-sensitive next steps, explaining WHY each is useful: + +- `/config-audit plan` — turn the kept opportunities into an action plan with + backups before you change anything. +- `/config-audit feature-gap` — the complementary lens: features you *don't* use + yet (this command is about mechanisms you *do* use that could fit better). +- Re-run `/config-audit optimize` anytime after editing CLAUDE.md. + +## Notes + +- This command is **agent-driven and not byte-stable** — its output is a + human-facing report, deliberately outside the deterministic snapshot suite. +- The deterministic half (CA-OPT-001) also rides in the normal orchestrated + audit; this command adds the prose-judgment half on top. +- No files are modified. To act on a finding, use `/config-audit plan` → + `/config-audit implement` (backup + rollback) or edit by hand. diff --git a/scanners/lib/lens-prefilter.mjs b/scanners/lib/lens-prefilter.mjs new file mode 100644 index 0000000..80d7ad6 --- /dev/null +++ b/scanners/lib/lens-prefilter.mjs @@ -0,0 +1,114 @@ +/** + * lens-prefilter — deterministic, recall-oriented candidate generator for the + * v5.7 optimization lens (CA-OPT) hybrid motor, Chunk 2b. + * + * The OPT *scanner* (Chunk 2a) handles the one mechanism-fit case it can decide + * deterministically with high precision (a long numbered procedure → skill). The + * other three cases in the register are PROSE-JUDGMENT calls — whether a line is + * really lifecycle automation, a path-specific constraint, or an absolute + * prohibition depends on reading intent, which a regex cannot settle. So the + * hybrid motor splits the work: + * + * pre-filter (this module, CHEAP, recall-oriented) + * → surfaces candidate lines tagged with the register rule they might fit + * opus optimization-lens-agent (PRECISION gate) + * → reads each candidate in context, keeps only genuine mechanism-fit + * opportunities, cites the register rule + source + * + * Therefore this pre-filter deliberately errs toward recall: a false candidate + * costs the agent a moment's judgement, a missed line is never recoverable. It + * does, however, avoid the two obvious noise sources — fenced code blocks and + * (when the caller passes the parsed body) YAML frontmatter — and it requires an + * imperative-looking line for the path-specific class so plain "see docs/x.md" + * references don't flood the candidate list. + * + * The detector names mirror the `lensCheck` fields of the register entries + * (knowledge/best-practices.json), so the agent can map each candidate straight + * back to its provenance. + * + * Zero external dependencies. Pure: input text → candidate array. + */ + +/** + * The three prose-judgment detectors, keyed to their register entries. + * `mechanism` is the better-fit mechanism the register recommends. + */ +export const LENS_DETECTORS = Object.freeze([ + { lensCheck: 'claude-md-lifecycle-phrasing', registerId: 'BP-MECH-001', mechanism: 'hook' }, + { lensCheck: 'unscoped-path-specific-instruction', registerId: 'BP-MECH-002', mechanism: 'rule' }, + { lensCheck: 'never-instruction', registerId: 'BP-MECH-004', mechanism: 'permission' }, +]); + +// Lifecycle automation phrased as an instruction: "after every commit", "before +// each push", "every time you …", "whenever you …", "always run". Recall-first. +const LIFECYCLE_RE = + /\b(?:after (?:every|each)|before (?:every|each)|on (?:every|each)|every time|each time|always run|whenever)\b/i; + +// Absolute prohibition: a standalone "never" followed by an action word. Kept +// permissive (recall); the agent decides whether it is a real hard rule. +const NEVER_RE = /\bnever\s+[a-z]/i; + +// A concrete path / glob / known-extension filename anywhere in the line. +const PATH_RE = + /(?:(?:\.{0,2}\/)?[\w.-]+\/[\w.*/-]+|\*\*?\/[\w.*-]+|\b[\w-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|md|json|ya?ml|toml|go|rs|java|rb|php|c|cpp|h|hpp|sh|sql|css|scss|html|env)\b)/; + +// An imperative / modal verb that marks a line as an instruction rather than a +// bare cross-reference. Gates the path-specific class to cut "see foo/bar.md". +const INSTRUCTION_RE = + /\b(?:use|edit|run|always|must|should|put|place|write|add|modify|update|format|lint|test|name|store|keep|never|generate|build|deploy|commit)\b/i; + +const getDetector = (lensCheck) => LENS_DETECTORS.find((d) => d.lensCheck === lensCheck); + +function candidate(lensCheck, lineNo, lineText) { + const d = getDetector(lensCheck); + return { + lensCheck, + registerId: d.registerId, + mechanism: d.mechanism, + line: lineNo, + text: lineText.trim(), + }; +} + +/** + * Scan CLAUDE.md text for prose-judgment mechanism-fit candidates. + * + * Pass the file body (frontmatter stripped) for clean line numbers; the caller + * is then responsible for offsetting `line` by the body's start line. Raw text + * also works — fenced code is skipped either way. + * + * @param {string} text + * @returns {Array<{lensCheck:string, registerId:string, mechanism:string, line:number, text:string}>} + */ +export function prefilterClaudeMd(text) { + const lines = String(text == null ? '' : text).split('\n'); + const out = []; + let inFence = false; + + for (let i = 0; i < lines.length; i++) { + const raw = lines[i]; + const lineNo = i + 1; + + // Toggle fenced code blocks (``` or ~~~). Fence lines themselves are skipped. + if (/^\s*(?:```|~~~)/.test(raw)) { + inFence = !inFence; + continue; + } + if (inFence) continue; + + const trimmed = raw.trim(); + if (trimmed === '') continue; + + if (LIFECYCLE_RE.test(raw)) { + out.push(candidate('claude-md-lifecycle-phrasing', lineNo, raw)); + } + if (NEVER_RE.test(raw)) { + out.push(candidate('never-instruction', lineNo, raw)); + } + if (PATH_RE.test(raw) && INSTRUCTION_RE.test(raw)) { + out.push(candidate('unscoped-path-specific-instruction', lineNo, raw)); + } + } + + return out; +} diff --git a/scanners/optimize-lens-cli.mjs b/scanners/optimize-lens-cli.mjs new file mode 100644 index 0000000..21d0bc5 --- /dev/null +++ b/scanners/optimize-lens-cli.mjs @@ -0,0 +1,160 @@ +#!/usr/bin/env node + +/** + * optimize-lens CLI — feeds the v5.7 optimization lens (CA-OPT) `/config-audit + * optimize` command. It produces the two halves of the hybrid motor as one JSON + * payload: + * + * 1. `deterministic` — the OPT scanner's high-precision findings (CA-OPT-001: + * a long numbered procedure in CLAUDE.md → skill). Already part of the + * orchestrated audit; surfaced here so /optimize is a complete view. + * 2. `candidates` — recall-oriented prose-judgment candidates from the + * lens-prefilter (lifecycle → hook, unscoped path-specific → rule, "never" + * → permission), each stamped with the CONFIRMED register entry it might fit + * (claim / recommendation / source / severity). The opus + * optimization-lens-agent is the precision gate over these. + * + * Only CONFIRMED register entries are attached (Verifiseringsplikt); a candidate + * whose register rule is missing or unconfirmed is dropped, so the agent never + * sees an unverifiable recommendation. + * + * Usage: + * node optimize-lens-cli.mjs [path] [--output-file ] [--global] + * + * Exit codes: 0=ok, 3=unrecoverable error. Zero external dependencies. + */ + +import { resolve } from 'node:path'; +import { writeFile, readFile, stat } from 'node:fs/promises'; +import { discoverConfigFiles } from './lib/file-discovery.mjs'; +import { resetCounter } from './lib/output.mjs'; +import { parseFrontmatter } from './lib/yaml-parser.mjs'; +import { loadRegister, getEntry } from './lib/best-practices-register.mjs'; +import { prefilterClaudeMd, LENS_DETECTORS } from './lib/lens-prefilter.mjs'; +import { scan as optScan } from './optimization-lens-scanner.mjs'; + +/** Confirmed register entry for `id`, or null. */ +function confirmedEntry(register, id) { + const e = getEntry(register, id); + return e && e.confidence === 'confirmed' ? e : null; +} + +async function main() { + const args = process.argv.slice(2); + let targetPath = '.'; + let outputFile = null; + let includeGlobal = false; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--global') includeGlobal = true; + else if (args[i] === '--output-file' && args[i + 1]) outputFile = args[++i]; + else if (!args[i].startsWith('-')) targetPath = args[i]; + } + + const absPath = resolve(targetPath); + try { + const s = await stat(absPath); + if (!s.isDirectory()) { + process.stderr.write(`Error: ${absPath} is not a directory\n`); + process.exit(3); + } + } catch { + process.stderr.write(`Error: path does not exist: ${absPath}\n`); + process.exit(3); + } + + // Load the register once; tolerate its absence (deterministic half still runs). + let register = null; + try { + register = loadRegister(); + } catch { + register = null; + } + + resetCounter(); + const discovery = await discoverConfigFiles(absPath, { includeGlobal }); + + // ── Deterministic half: the OPT scanner (CA-OPT-001) ── + const opt = await optScan(absPath, discovery); + + // ── Recall half: prose-judgment candidates from the pre-filter ── + const claudeMdFiles = (discovery.files || []).filter((f) => f.type === 'claude-md'); + const candidates = []; + for (const file of claudeMdFiles) { + let content; + try { + content = await readFile(file.absPath, 'utf-8'); + } catch { + continue; + } + const parsed = parseFrontmatter(content); + const body = parsed.body || content; + const bodyStartLine = parsed.bodyStartLine || 1; + for (const cand of prefilterClaudeMd(body)) { + const entry = register ? confirmedEntry(register, cand.registerId) : null; + if (!entry) continue; // never surface an unverifiable recommendation + candidates.push({ + file: file.relPath || file.absPath, + line: bodyStartLine - 1 + cand.line, + lensCheck: cand.lensCheck, + mechanism: cand.mechanism, + signalText: cand.text, + register: { + id: entry.id, + claim: entry.claim, + recommendation: entry.recommendation || null, + severity: entry.severity || 'low', + source: entry.source, + }, + }); + } + } + + // The CONFIRMED prose-judgment entries, so the agent has full provenance even + // for a detector class that produced no candidates this run. + const registerEntries = register + ? LENS_DETECTORS.map((d) => confirmedEntry(register, d.registerId)) + .filter(Boolean) + .map((e) => ({ + id: e.id, + lensCheck: e.lensCheck, + claim: e.claim, + recommendation: e.recommendation || null, + mechanism: e.mechanism || null, + severity: e.severity || 'low', + source: e.source, + })) + : []; + + const payload = { + status: 'ok', + target: absPath, + deterministic: opt.findings || [], + candidates, + register: registerEntries, + counts: { + deterministic: (opt.findings || []).length, + candidates: candidates.length, + byLensCheck: candidates.reduce((acc, c) => { + acc[c.lensCheck] = (acc[c.lensCheck] || 0) + 1; + return acc; + }, {}), + }, + }; + + const json = JSON.stringify(payload, null, 2); + if (outputFile) { + await writeFile(outputFile, json, 'utf-8'); + } + if (!outputFile) { + process.stdout.write(json + '\n'); + } +} + +const isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname); +if (isDirectRun) { + main().catch((err) => { + process.stderr.write(`Fatal: ${err.message}\n`); + process.exit(3); + }); +} diff --git a/tests/lib/lens-prefilter.test.mjs b/tests/lib/lens-prefilter.test.mjs new file mode 100644 index 0000000..3ed5946 --- /dev/null +++ b/tests/lib/lens-prefilter.test.mjs @@ -0,0 +1,132 @@ +/** + * lens-prefilter tests — the deterministic, recall-oriented half of the v5.7 + * optimization-lens hybrid motor (Chunk 2b). + * + * The pre-filter is intentionally CHEAP and recall-oriented: it surfaces + * candidate lines for the three prose-judgment mechanism-fit classes that the + * deterministic OPT scanner (Chunk 2a) deliberately skips. The opus + * optimization-lens-agent is the PRECISION gate downstream — so these tests + * assert the pre-filter (a) detects the obvious signals and tags them with the + * right register id / lensCheck / mechanism, (b) reports correct 1-based line + * numbers, and (c) does not fire inside fenced code blocks. They do NOT demand + * zero false positives — that is the agent's job, by design. + */ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { prefilterClaudeMd, LENS_DETECTORS } from '../../scanners/lib/lens-prefilter.mjs'; + +const byCheck = (candidates, lensCheck) => candidates.filter((c) => c.lensCheck === lensCheck); + +describe('lens-prefilter — detector table', () => { + it('exposes the three prose-judgment detectors keyed by register id + lensCheck', () => { + const map = new Map(LENS_DETECTORS.map((d) => [d.lensCheck, d])); + assert.deepEqual(map.get('claude-md-lifecycle-phrasing')?.registerId, 'BP-MECH-001'); + assert.deepEqual(map.get('claude-md-lifecycle-phrasing')?.mechanism, 'hook'); + assert.deepEqual(map.get('unscoped-path-specific-instruction')?.registerId, 'BP-MECH-002'); + assert.deepEqual(map.get('unscoped-path-specific-instruction')?.mechanism, 'rule'); + assert.deepEqual(map.get('never-instruction')?.registerId, 'BP-MECH-004'); + assert.deepEqual(map.get('never-instruction')?.mechanism, 'permission'); + }); +}); + +describe('lens-prefilter — lifecycle phrasing (BP-MECH-001 → hook)', () => { + it('flags "After every commit, run the linter."', () => { + const c = byCheck(prefilterClaudeMd('After every commit, run the linter.'), 'claude-md-lifecycle-phrasing'); + assert.equal(c.length, 1); + assert.equal(c[0].registerId, 'BP-MECH-001'); + assert.equal(c[0].mechanism, 'hook'); + assert.equal(c[0].line, 1); + assert.match(c[0].text, /After every commit/); + }); + + it('flags several lifecycle markers (before each, every time, whenever)', () => { + const text = [ + 'Before each push, run the test suite.', + 'Every time you edit a config file, re-validate it.', + 'Whenever you finish a task, update STATE.md.', + ].join('\n'); + assert.equal(byCheck(prefilterClaudeMd(text), 'claude-md-lifecycle-phrasing').length, 3); + }); + + it('does not flag a plain fact with no lifecycle phrasing', () => { + assert.equal( + byCheck(prefilterClaudeMd('This project uses TypeScript and pnpm.'), 'claude-md-lifecycle-phrasing').length, + 0, + ); + }); +}); + +describe('lens-prefilter — never-instruction (BP-MECH-004 → permission)', () => { + it('flags "Never commit secrets to the repository."', () => { + const c = byCheck(prefilterClaudeMd('Never commit secrets to the repository.'), 'never-instruction'); + assert.equal(c.length, 1); + assert.equal(c[0].registerId, 'BP-MECH-004'); + assert.equal(c[0].mechanism, 'permission'); + }); + + it('flags a bulleted "never" directive', () => { + const c = byCheck(prefilterClaudeMd('- Never push directly to main.'), 'never-instruction'); + assert.equal(c.length, 1); + }); +}); + +describe('lens-prefilter — unscoped path-specific instruction (BP-MECH-002 → rule)', () => { + it('flags an instruction referencing a glob', () => { + const c = byCheck(prefilterClaudeMd('Always format src/**/*.ts with prettier.'), 'unscoped-path-specific-instruction'); + assert.equal(c.length, 1); + assert.equal(c[0].registerId, 'BP-MECH-002'); + assert.equal(c[0].mechanism, 'rule'); + }); + + it('flags an instruction referencing a concrete filename', () => { + const c = byCheck(prefilterClaudeMd('Run migrations from db/schema.sql before tests.'), 'unscoped-path-specific-instruction'); + assert.equal(c.length, 1); + }); + + it('does not flag a bare path reference with no instruction verb', () => { + assert.equal( + byCheck(prefilterClaudeMd('See docs/architecture.md for the overview.'), 'unscoped-path-specific-instruction').length, + 0, + ); + }); +}); + +describe('lens-prefilter — structural rules', () => { + it('reports correct 1-based line numbers', () => { + const text = ['# Title', '', 'Some facts here.', 'Never delete the .env file.'].join('\n'); + const c = byCheck(prefilterClaudeMd(text), 'never-instruction'); + assert.equal(c.length, 1); + assert.equal(c[0].line, 4); + }); + + it('ignores signals inside fenced code blocks', () => { + const text = [ + 'Real prose: never run this in production.', + '```bash', + '# after every deploy, never skip the smoke test', + 'echo "every time"', + '```', + 'More prose after the fence.', + ].join('\n'); + const all = prefilterClaudeMd(text); + // only the first prose line's "never" should match; nothing inside the fence + assert.ok(all.every((c) => c.line === 1), `expected only line 1; got lines ${all.map((c) => c.line).join(',')}`); + }); + + it('returns an empty array for a clean facts-only document', () => { + const text = [ + '# Project', + 'A web API written in Go. Data lives in Postgres.', + 'The team deploys via GitHub Actions.', + ].join('\n'); + assert.deepEqual(prefilterClaudeMd(text), []); + }); + + it('a single line can yield candidates for two distinct detectors', () => { + // "never" + a concrete path → both the prohibition and the path-scope angle. + const c = prefilterClaudeMd('Never edit src/config.ts by hand.'); + const checks = new Set(c.map((x) => x.lensCheck)); + assert.ok(checks.has('never-instruction')); + assert.ok(checks.has('unscoped-path-specific-instruction')); + }); +});