Anthropic documents that Claude Opus 5 verifies its own work, and that telling it to double-check or to delegate verification to a subagent causes over-verification -- token cost with no quality gain. The general subtraction detector (BP-SUB-001) already surfaces those blocks for every user, with no model-awareness at all. `optimize --subtract --for-model <name>` adds the missing half. It ANNOTATES a subset of the candidates --subtract already produced; it is not a second detector and can never widen the candidate set. A second SUBTRACT_DETECTORS entry would have collided with BP-SUB-001 on de-dup, and a prose-only signal in the agent prompt would have been untestable. There is no auto-detection, by measurement rather than omission: a CLAUDE.md has no frontmatter and no resolvable target model, and this operator's own `route` skill deliberately runs a different model per session -- the same file is read by whichever model comes next. So the model is named, and the citation is reported as conditional everywhere a human sees it (agent report copy, and the Step 7a listing that is the last surface before an approval file). Precision comes from the TARGET, not the verb list. Measured across the 409-file corpus: 392 BP-SUB-001 candidates, 31 (7.9%) carry a verify verb, and 0 also carry a reflexive or delegated target. Two independent raw-text greps found 0 as well, so the zero is the corpus rather than an over-narrow regex. Those 31 verb-only blocks -- "sjekk relevante config-filer", "Type-sjekk: pyright", "To verify plugin functionality" -- are exactly the false positives a verb-only version would have produced, which is BP-JUDG-001's 7/7 failure arriving one lens over. The numbers live in the register entry's note and are pinned by a test, because a session that cannot see the measurement reads the zero as a broken detector and loosens it. `recognized` is reported separately from `matchedCount`: a typo'd model name and a genuinely clean config both yield zero, and without the distinction the CLI would report a silent no-op as good news. Dogfooded on the real machine -- `opus-5` gives recognized:true/matchedCount:0, `oppus5` gives recognized:false. source.published is absent because the guide carries no visible publish date; its absence is asserted so a later session does not invent one to match the other entries' shape. Both quoted sentences were verified verbatim 2026-08-12. The payload stays additive -- forModel and per-candidate modelScope appear only under the flag, so a plain --subtract run is byte-identical to before (asserted on the serialized bytes, since a key set to undefined passes a shallow check). Suite 1724 -> 1752 (+28). The one remaining failure is the pre-existing drift-cli --output-file crash, untouched by this work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BRuXt6tZyowi8QYNKLSHQm
284 lines
11 KiB
JavaScript
284 lines
11 KiB
JavaScript
#!/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 <path>] [--global]
|
|
* [--subtract [--for-model <name>]]
|
|
*
|
|
* `--for-model <name>` annotates the subtraction candidates a named model
|
|
* documents as redundant (BP-PROMPT-001). It never widens the candidate set,
|
|
* and there is deliberately no auto-detection: a CLAUDE.md has no frontmatter
|
|
* and no statically-resolvable target model, so the model must be named.
|
|
*
|
|
* Exit codes: 0=ok, 3=unrecoverable error. Zero external dependencies.
|
|
*/
|
|
|
|
import { resolve, sep } from 'node:path';
|
|
import { readFile, stat } from 'node:fs/promises';
|
|
import { writeOutputFile } from './lib/write-output.mjs';
|
|
import { discoverConfigFiles } from './lib/file-discovery.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 { subtractionCandidates, SUBTRACT_DETECTORS } from './lib/subtraction-prefilter.mjs';
|
|
import { matchModelScope, normalizeModel } from './lib/prompting-model-scope.mjs';
|
|
import { scan as optScan } from './optimization-lens-scanner.mjs';
|
|
import { requireValidArgs } from './lib/cli-args.mjs';
|
|
|
|
/** Flag surface, measured 2026-08-09. Anything else is exit 3. */
|
|
const ARG_SPEC = {
|
|
boolean: ['--global', '--subtract'],
|
|
// `--for-model` is a VALUE flag, so a bare `--for-model` is reported as
|
|
// "needs a value" rather than "unknown flag" — the two are different failures
|
|
// and reporting the wrong one hides which mistake the caller made.
|
|
value: ['--output-file', '--for-model'],
|
|
};
|
|
|
|
// Files under `.claude/plugins/` are shipped by an installed plugin — vendored
|
|
// CLAUDE.md plus its bundled tests/fixtures and examples. They are not the user's
|
|
// authored config, so a mechanism-fit suggestion against them is not actionable
|
|
// (the user can't edit a file the plugin overwrites on update). Excluded from the
|
|
// lens regardless of active/stale version. (M-BUG-11; mirrors the M-BUG-2 rule
|
|
// that keeps plugin-bundled config out of the conflict detector.)
|
|
const PLUGIN_TREE_MARKER = `.claude${sep}plugins${sep}`;
|
|
const isPluginBundled = (file) => (file.absPath || '').includes(PLUGIN_TREE_MARKER);
|
|
|
|
/** 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);
|
|
if (!requireValidArgs(args, ARG_SPEC)) return;
|
|
let targetPath = '.';
|
|
let outputFile = null;
|
|
let includeGlobal = false;
|
|
let subtract = false;
|
|
let targetModel = null;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '--global') includeGlobal = true;
|
|
else if (args[i] === '--subtract') subtract = true;
|
|
else if (args[i] === '--output-file' && args[i + 1]) outputFile = args[++i];
|
|
else if (args[i] === '--for-model' && args[i + 1]) targetModel = 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.exitCode = 3;
|
|
return;
|
|
}
|
|
} catch {
|
|
process.stderr.write(`Error: path does not exist: ${absPath}\n`);
|
|
process.exitCode = 3;
|
|
return;
|
|
}
|
|
|
|
// Load the register once; tolerate its absence (deterministic half still runs).
|
|
let register = null;
|
|
try {
|
|
register = loadRegister();
|
|
} catch {
|
|
register = null;
|
|
}
|
|
|
|
const rawDiscovery = await discoverConfigFiles(absPath, { includeGlobal });
|
|
// Scope the lens to the user's authored config: drop plugin-bundled files for
|
|
// BOTH halves of the motor (the OPT scanner reads discovery.files directly).
|
|
const discovery = {
|
|
...rawDiscovery,
|
|
files: (rawDiscovery.files || []).filter((f) => !isPluginBundled(f)),
|
|
};
|
|
|
|
// ── 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 = [];
|
|
// Opt-in only: the subtraction axis asks a different question and must not
|
|
// fire on a plain `/config-audit optimize` run (brief §7 q3).
|
|
const subtractCands = [];
|
|
const subtractEntry = subtract && register ? confirmedEntry(register, 'BP-SUB-001') : null;
|
|
// Model-scoped annotation entries (BP-PROMPT-001 and any later sibling). These
|
|
// never produce candidates of their own — they only tag candidates the
|
|
// BP-SUB-001 detector above already surfaced.
|
|
const promptEntries =
|
|
subtract && register
|
|
? (register.entries || []).filter(
|
|
(e) => e.category === 'prompting-fit' && e.confidence === 'confirmed',
|
|
)
|
|
: [];
|
|
// `recognized` is reported separately from the match count so a typo'd model
|
|
// name is distinguishable from a config that genuinely carries nothing.
|
|
const modelRecognized =
|
|
!!targetModel &&
|
|
promptEntries.some((e) =>
|
|
(e.modelScope || []).some((m) => normalizeModel(m) === normalizeModel(targetModel)),
|
|
);
|
|
let modelMatchedCount = 0;
|
|
|
|
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;
|
|
|
|
if (subtractEntry) {
|
|
for (const cand of subtractionCandidates(body)) {
|
|
const modelScope = matchModelScope(cand.text, targetModel, promptEntries);
|
|
if (modelScope) modelMatchedCount++;
|
|
subtractCands.push({
|
|
file: file.absPath,
|
|
line: bodyStartLine - 1 + cand.startLine,
|
|
endLine: bodyStartLine - 1 + cand.endLine,
|
|
lineCount: cand.lineCount,
|
|
lensCheck: cand.lensCheck,
|
|
mechanism: cand.mechanism,
|
|
signalText: cand.text,
|
|
register: {
|
|
id: subtractEntry.id,
|
|
claim: subtractEntry.claim,
|
|
recommendation: subtractEntry.recommendation || null,
|
|
severity: subtractEntry.severity || 'low',
|
|
source: subtractEntry.source,
|
|
},
|
|
// Spread only when matched: a run without --for-model must not grow
|
|
// even a key set to undefined.
|
|
...(modelScope ? { modelScope } : {}),
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const cand of prefilterClaudeMd(body)) {
|
|
const entry = register ? confirmedEntry(register, cand.registerId) : null;
|
|
if (!entry) continue; // never surface an unverifiable recommendation
|
|
candidates.push({
|
|
// Absolute path: unique + readable. relPath collides across scopes
|
|
// (a repo-root `CLAUDE.md` and the user-global `~/.claude/CLAUDE.md`
|
|
// both relPath to `CLAUDE.md`), which would send the agent's Read() to
|
|
// the wrong file. (M-BUG-11)
|
|
file: 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;
|
|
}, {}),
|
|
},
|
|
};
|
|
|
|
// Additive ONLY under --subtract: a plain run's payload must stay byte-identical.
|
|
if (subtract) {
|
|
payload.subtract = {
|
|
enabled: true,
|
|
candidates: subtractCands,
|
|
register: subtractEntry
|
|
? [
|
|
{
|
|
id: subtractEntry.id,
|
|
lensCheck: subtractEntry.lensCheck,
|
|
claim: subtractEntry.claim,
|
|
recommendation: subtractEntry.recommendation || null,
|
|
mechanism: subtractEntry.mechanism || null,
|
|
severity: subtractEntry.severity || 'low',
|
|
source: subtractEntry.source,
|
|
},
|
|
]
|
|
: [],
|
|
detectors: SUBTRACT_DETECTORS.map((d) => ({ ...d })),
|
|
};
|
|
// Present ONLY when a model was named — a plain --subtract run stays
|
|
// byte-identical to the pre-flag payload.
|
|
if (targetModel) {
|
|
payload.subtract.forModel = {
|
|
requested: targetModel,
|
|
recognized: modelRecognized,
|
|
matchedCount: modelMatchedCount,
|
|
};
|
|
}
|
|
payload.counts.subtractCandidates = subtractCands.length;
|
|
}
|
|
|
|
const json = JSON.stringify(payload, null, 2);
|
|
if (outputFile) {
|
|
await writeOutputFile(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.exitCode = 3;
|
|
});
|
|
}
|