Commit graph

133 commits

Author SHA1 Message Date
de8a7b5d51 fix(scanners): stop discarding our own stdout when it is a pipe
process.exit() terminates immediately, but Node writes stdout asynchronously
when stdout is a pipe — everything still buffered is dropped. scan-orchestrator
measured 246 854 bytes to a file against 65 536 to a pipe (131 072 on another
run; the cut point is a flush race), so every machine consumer that pipes the
envelope got truncated, unparseable JSON. The failure reads like a corrupt file,
not like a cut-off, which is what made it survive this long. Reported by
org-ops, whose census pipes our output.

Closes the class rather than the one CLI where it was visible. campaign-cli,
campaign-export-cli, campaign-write-cli, knowledge-refresh-cli, drift-cli and
fix-cli all exited the same way on their success paths and were green only
because their payloads fit the pipe buffer today; size is not correctness. All
38 sites across 14 files now set process.exitCode and return, which is the
pattern self-audit.mjs already used.

Two contracts needed care rather than substitution: fail() is a never-returns
guard at ~25 call sites, so it throws a CliUsageError the top-level catch
renders with the identical "Error: " prefix and exit code 3; the path guards
needed an explicit return so main() stops instead of running on. Exit codes and
stderr text are unchanged, and the frozen v5.0.0 snapshots are untouched.

The class sweep is landed as a test, not as fourteen edits — it caught one site
this commit had missed. Suite 1443/0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8sS1DuDV6bUJcyumLwbvj
2026-07-31 21:40:15 +02:00
b85919f2ec fix(commands): close the promises the command templates could not keep
DEL B chunk `interview` (+ discover/status/cleanup/help). Fasit written before
the run predicted 8 defects and refuted 4 candidates; all 8 confirmed, all 4
refutations held, and three predictions turned out too narrow.

- M-BUG-36: `drift --list` reached the command as 0 bytes. drift-cli accepted
  --output-file but list mode ignored it, and the listing goes to stderr, which
  the command discards per ux-rules rule 2. Fixing the caller alone would not
  have helped.
- M-BUG-37: feature-gap's "Create backup" step ran fix-cli without --apply.
  Dry-run is the default, so no backup existed (backupId: null) while the
  command went on to edit config believing it could roll back.
- M-BUG-38: fix-cli told users to recover with scanners/rollback-cli.mjs, which
  does not exist. Dead reference in the one message read after a bad fix.
- M-BUG-21 fourth arm: five templates carried literal [--global]/[--full-machine]
  inside executable bash blocks. A bracketed placeholder does not start with a
  dash, so every scanner's arg loop takes it as the scan target.
- interview and analyze never said which session they act on; interview could
  rewind a finished session; cleanup interpolated an unvalidated id into rm -rf
  (an empty id deletes every session); status advertised a `resume` command that
  does not exist and documented an `all` argument it never parsed.

TDD: 9 red tests first, including a machine sweep for dead /config-audit
references and for bracketed flags in bash blocks. Suite 1432 -> 1441/0.
Frozen v5.0.0 snapshots untouched; --raw/--json contracts unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGvA1uUQn2hPBPMaCKK6x3
2026-07-31 21:27:07 +02:00
001090261e fix(plugin-health): make the command able to read what the scanner found
Dogfooding `/config-audit plugin-health` against a fasit registered before the
run: 11 of 12 predictions confirmed, 1 refuted with evidence, 0 deviations.
The command's default path could not produce the report it documents.

M-BUG-21 (third arm): the argument loop ended in
`else if (!args[i].startsWith('-')) targetPath = args[i]` with no unknown-flag
branch, so `--output-file /tmp/x.json` was dropped and its value became the scan
target. Worse than in drift-cli: a non-existent path discovers no plugins, so the
scanner answered "No plugins found" (info) with exit 0 — a reassuring answer, not
an error. Unknown options and a value-less `--output-file` now exit 3.

M-BUG-33: the scanner had no `--output-file` and its default-mode report goes to
stderr, which `commands/plugin-health.md` discards with `2>/dev/null` before
telling the agent to read stdout. Zero bytes captured.

M-BUG-34: per-plugin rows and the grade formula never left `scan()` — the only
grade code, `formatPluginHealthReport`, had no caller — and cross-plugin findings
were flattened behind a `category` they share with per-plugin findings. The
mandated table and Cross-Plugin section were unbuildable, so the command had to
fabricate them. `scanDetailed()` now returns them; `scan()`'s frozen v5.0.0
envelope is unchanged by construction.

M-BUG-35: `.claude-plugin/marketplace.json` was flagged as an unknown file. It is
the documented catalog location, and `"source": "./"` makes the repo root its own
plugin, so one `.claude-plugin/` legitimately holds both.

Also: `commands/posture.md` ran both optional scanners in default mode under
`2>/dev/null` and read stdout — the same class as feature-gap.md:133 in the fix
chunk. A CLI-side flag fix does not close its callers.

Tests 1420 -> 1432, red first. Frozen v5.0.0 snapshots untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhhZ8zg1amR7YCAPqiZWdt
2026-07-31 21:08:32 +02:00
05f1e954d0 fix(fix): validate the arguments, back up renames, and verify the scope it fixed
Dogfooding `/config-audit fix` against a throwaway repo copy. All eight
predictions registered in the fasit before the run were confirmed, and three
further defects surfaced that were not predicted.

- M-BUG-21, third arm: the argument loop ended in `!arg.startsWith('-') =>
  targetPath`, so an unknown flag was dropped and its value became the target.
  In `fix` that is the WRITE target under `--apply`. Unknown options and a
  value-less `--output-file` now exit 3.
- `--dry-run` was documented in the command's argument-hint and never
  implemented; `--output-file` did not exist, so `commands/fix.md` told the
  agent to Read a file nothing produced. Both now exist.
- M-BUG-31: `file-rename` was excluded from the backup set, so a renamed rule
  file had no backup entry while the command promised one and returned a
  backupId that could not restore it.
- M-BUG-32: `verifyFixes` hardcoded `includeGlobal: false`, so after a
  `--global` run every untouched user-scope finding was reported as verified.
  Reproduced against an unmodified ~/.claude/CLAUDE.md.
- M-BUG-29: a rename was applied before other fixes on the same file, which
  then failed with ENOENT while the run still exited 0. Renames sort last.
- M-BUG-30: `severityOrder[s] || 4` maps critical (0) to 4, so critical fixes
  sorted last. The old test used the same falsy fallback and agreed with the
  bug. Now `?? 4`.
- A failed fix exits 2 instead of 0, matching the other scanners' convention.

Frozen tests/snapshots/v5.0.0/ untouched; --json/--raw stdout byte-identical.
Suite 1420/0 (+10).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ3MCDCnyw7wZSPnUXVhYS
2026-07-31 18:41:27 +02:00
1182f85767 fix(drift): validate the arguments and the baseline anchor drift never checked
Dogfooding `/config-audit drift` against the machine. Fasit written before the
run; 6/6 predictions plus both F7 arms confirmed, 0 deviations.

M-BUG-21 (both arms):
The arg loop ended in `else if (!arg.startsWith('-')) targetPath = arg` with no
unknown-flag branch, so an unrecognised flag was dropped silently and its VALUE
became the scan target. `--output-file /tmp/x.json` scanned /tmp/x.json — a path
that does not exist — and reported the near-empty scan as drift, forever. The
same silence was destructive for `--save --name` with the value omitted: the
name stayed `default` and an existing baseline was overwritten. And the flag
ux-rules rule 2 requires did not exist at all: commands/drift.md ran the CLI
under `2>/dev/null` while telling the agent to read stdout, but the default
report, the --save confirmation and --list all write to stderr. All three modes
captured nothing.

M-BUG-27 (found during the run, not predicted):
diff-engine never compared the baseline's stored target_path against the current
target. The machine's `default` baseline is anchored to a test fixture, so
`/config-audit drift` diffed two unrelated trees, marked all 20 baseline
findings resolved and all 15 current ones new, and reported trend "improving".
A reassuring, entirely false signal — and the default path.

Root cause is one thing, not three: the CLI validated neither its flags nor its
anchor. Same class as the rollback chunk's "nothing agreed where a backup lives".

Fix: unknown options and value-less --name/--baseline/--output-file exit 3;
--output-file follows the posture.mjs pattern; `_baselineAnchor` rides in the
default-mode payload (stderr alone is invisible under `2>/dev/null`) while
--json/--raw stdout stays v5.0.0-shaped.

Suite 1410/0 (+12). Frozen snapshots untouched; raw/json/default backcompat green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RGrL3noVdFSUhohaKTMSN
2026-07-31 18:20:04 +02:00
8f149891c9 fix(rollback): restore the backup path contract the engine and the commands disagreed on
Pipeline step 4 dogfood. `/config-audit rollback` could not see a single one of
the four real backups on this machine, and reported "Backup not found" for one
that was sitting right there.

Four defects, one root: nothing agreed on where a backup lives or what its
manifest looks like.

- M-BUG-22 `lib/backup.mjs` resolved `~/.config-audit/backups` (pre-v2.2.0)
  while every command, agent and doc uses `~/.claude/config-audit/backups`.
  The auto-backup hook and fix-cli wrote to the first, implement to the second,
  rollback read only the first. Canonical root now, with the legacy root kept
  readable so older backups stay listable and restorable (`legacy: true`).
- M-BUG-25 `parseManifest` understood only the engine's quoted `original_path:`
  spelling, but implement hand-builds its manifest with `- backup:`/`original:`/
  `sha256:`. Every implement-made backup parsed to zero files and restoreBackup
  returned `{restored: [], failed: []}` — a success-shaped no-op. Both formats
  parse now, and a manifest with unparseable entries throws instead of
  pretending to succeed.
- M-BUG-23 both session hooks watched `~/.config-audit/sessions`, which does not
  exist; sessions live under `~/.claude/`. "Check for active sessions" had never
  fired once. It fires now.
- M-BUG-24 the suite called createBackup() against the developer's real home —
  it had left nine stray backups there, and cleanupOldBackups() deletes past ten.
  Root is overridable via CONFIG_AUDIT_BACKUP_ROOT; both test files use it.

Rollback still cannot delete files implement CREATED — no backup can hold a file
that never existed. It no longer does so silently: manifests carry a `created:`
list, restoreBackup returns `createdNotRemoved`, and rollback.md requires the
report. Automatic deletion is a destructive action and needs its own design.

Verified against backup 20260717_032636 on a throwaway copy: all three files
restore byte-exact (sha256 match), zero writes outside the copy, backup dir
unmodified. Suite 1382 -> 1398/0; frozen v5.0.0 snapshots untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SejM9RQAa1Hfuq7Ek2WfFr
2026-07-31 17:23:30 +02:00
e9921d3c9d feat(optimize): add --subtract, the subtraction axis, behind a deterministic floor
Every command so far asked an addition question — what to add, what to move,
what it costs. Nothing asked what is no longer earning its always-loaded rent.
This adds that axis as a fourth lensCheck on the existing hybrid motor rather
than a new scanner or a 22nd command: the measured payoff (~18% of one file)
justifies a mode, not machinery.

It is the only lens that proposes REMOVING config, so it carries a guarantee
the others don't need: a load-bearing block is never a candidate. Precision is
asymmetric — a missed dead line costs a few tokens per turn, a deleted one
costs a wrong remote or a broken script — so the floor is decided in code
(lib/floor-exclusion.mjs) before the opus judge sees anything, never in prose.

Granularity is the leaf block, with two structural exceptions: a paragraph
ending in ':' merges with the list it introduces, and an ordered list is a
contract whose steps inherit floor from any sibling. Unordered lists
deliberately do not inherit — a load-bearing bullet and a disposable one
routinely share a list, and container-reasoning is the error the hand-built
ground truth exists to catch.

Verified against that ground truth (built before any classifier existed), with
the comparison machine-checked rather than read by eye: zero load-bearing
blocks proposed, 11/18 deletable groups surfaced, ~756 tok ~ 18% of a ~4300
token file — inside the pre-registered band. The first run found five floor
violations the synthesized fixture missed; each got a structural rule and a
fixture shape so it cannot regress.

Three real bugs the dogfood run exposed, all now covered:
- JS \b is ASCII-only, so /\bunngå\b/ never matches — every Norwegian keyword
  ending in æ/ø/å was silently dead.
- A bare word/word is not a path; "pros/cons" vetoed the largest deletable
  block until PATH_RE was tightened to rooted paths and globs.
- "Mid-sentence" must key on a preceding lowercase letter; the loose version
  read **bold labels:** and quoted openers as entities, costing 4 of 11 groups.

BP-SUB-001 is grounded entirely in the Anthropic steering blog already cited by
BP-MECH-001..004 and asserts nothing from the talk that motivated the feature —
no "80%", no ablation figure.

Suite 1365 -> 1382/0. Frozen v5.0.0 snapshots untouched; plain optimize output
byte-identical on identical input (--subtract adds keys only when passed).
knowledge-refresh-cli's reference date moved to 2026-08-01: its premise that
every seed entry was verified 2026-06-20 expired when BP-SUB-001 got a genuine
verification date, and backdating the entry to fit the test would have been a
lie about when its source was checked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RW2haJXbxZpKivKHseSXNh
2026-07-31 16:24:51 +02:00
b4d819b72d fix(acr): pin Bash >> append discipline on shared implementation log (M-BUG-20)
implement.md spawns implementer agents in parallel batches, all appending to
the same implementation-log.md. Dogfooding showed agents satisfying 'Append
result to:' with a full-file Write — the last writer clobbered 4 of 6 entries.
Pin the mechanism in both contracts: append with Bash >> heredoc, never the
Write/Edit tool on the shared log. Shape tests pin the instruction in both
files (empirically verified: agents given the >> instruction appended safely).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 03:53:06 +02:00
0cd87e0597 fix(rul): globToRegex corrupts mid-pattern /**/ globs (M-BUG-19)
The ? -> [^/] replacement ran AFTER the {{GLOBSTAR_SLASH}} placeholder was
restored to '(?:/.+/|/)', corrupting the group opener '(?:' into '([^/]:' —
every rule pattern containing a mid-pattern '/**/' silently matched only the
zero-dir branch and live rules were flagged 'matches no files' (CA-RUL).
Found by dogfooding /config-audit implement on a throwaway repo copy: the
implementer agent's correct 'posts/**/post.md' rule was flagged dead.

Fix: run the ? replacement before placeholder restoration. Fixture outcomes
byte-identical; frozen v5.0.0 baselines untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 03:53:06 +02:00
4b7b2d9c48 fix(acr): analyze persists agent-returned report (M-BUG-18)
The Claude Code subagent harness instructs spawned agents NOT to write
report/summary/findings/analysis .md files — the parent reads the final
text message. Verified live: analyzer-agent skipped Write entirely and
returned the report inline, so analysis-report.md never landed on disk
and the plan/interview/status phases would find nothing to read.

New contract (orchestrator-writes pattern): analyzer-agent returns the
complete report as its final message; the analyze command saves it
verbatim to the session directory before presenting the summary.

Same class exists in plan/feature-gap/optimize/scanner agent pairs —
deliberately left for their own dogfood chunks (plan is judged as-is
first per the pipeline sequence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CTontYwY5JGS4nL2AuiASy
2026-07-16 20:24:12 +02:00
97867dbf37 fix(acr): AGT findings humanize to "Wasted tokens" not "Other" (M-BUG-17)
The agent-listing scanner (AGT) emits an always-loaded per-turn token cost
("Agent description is long, re-sent every turn in the always-loaded listing";
scanner category 'token-efficiency', "the dominant single always-loaded
source"). But SCANNER_TO_CATEGORY in humanizer.mjs had no AGT entry, so its
findings fell through to the 'Other' fallback (humanizer.mjs:140) — a bucket
that isn't even in the analyzer-agent's category list. Neither the scanner
prefix nor the per-finding category ('token-efficiency' is not in
CATEGORY_TO_IMPACT) resolved AGT to its true impact.

Same class as M-BUG-16/15: a finding type without its matching humanizer
mapping landing on a default that mismatches its own evidence. The analogous
SKL body finding correctly buckets "Wasted tokens"; AGT (the same always-loaded
token-waste mechanism) silently landed under the meaningless "Other".

Found during analyze-prep premise-verification of the linkedin-posts scan: the
3 AGT findings bucketed "Other" while the analogous SKL findings bucketed
"Wasted tokens".

Fix: add AGT: 'Wasted tokens' to SCANNER_TO_CATEGORY, alongside TOK/CPS/SKL.
RED-first (extended the Wasted-tokens category test to include AGT; the 'Other'
fallback test still uses a synthetic 'XXX' scanner, unaffected). Frozen v5.0.0
untouched (AGT post-dates it; humanizer bypassed for --raw/--json); no
default-output snapshot contains AGT -> 0 regen. Suite 1359/0.

Verified end-to-end on linkedin-posts: 3 AGT findings now "Wasted tokens", 0
"Other" remaining, 28 findings unchanged (category-only). All 16 orchestrator
scanner prefixes now covered by the category map (class closed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01683eAqVecv9VZfQzL8CQ9h
2026-06-30 13:51:55 +02:00
239e88cecb fix(acr): on-demand copy for oversized skill-body finding (M-BUG-16)
The skill-listing check emits a third finding for an oversized skill BODY
(v5.11 B7, RAW title "Skill body is large (loads on demand when the skill
runs)"). The body is an ON-DEMAND cost — it loads only when the skill is
invoked, not the always-loaded listing Claude reads every turn. The scanner is
careful to distinguish the two (RAW title + comment + evidence note).

But the humanizer-data SKL.static map had no entry for this title, so it fell
through to SKL._default ("A skill is using more of the listing budget than it
should"). The humanized title therefore claimed a listing-budget cost and
directly contradicted the finding's own humanized evidence ("loads ON DEMAND
only ... NOT every turn like the always-loaded listing") — the same internal
contradiction class as M-BUG-15/M-BUG-14, and the same "new finding type added
without a matching humanizer entry" gap the scanner checklist warns about.

Found by finding-granularity premise-verification of the linkedin-posts scan
before feeding it to the analyze pipeline (the prior session's pass focused on
the GAP findings and did not catch the SKL fall-through).

- humanizer-data.mjs: add SKL.static entry for "Skill body is large (loads on
  demand when the skill runs)" with on-demand-correct title ("A skill's body is
  large (it loads only when that skill runs)"), description, and recommendation.
  No listing-budget language; tier1/tier3 forbidden-word checks pass.
- RED-first tests at both layers: humanizer.test.mjs (humanizeFinding path:
  title is not the listing-budget _default, conveys on-demand body) and
  humanizer-data.test.mjs (static entry exists, on-demand-correct).

RAW envelope unaffected (humanizer bypassed for --raw/--json), frozen v5.0.0
snapshots untouched, default-output fixtures contain no oversized-body skill so
no snapshot regen. Suite 1357->1359/0 (+2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01683eAqVecv9VZfQzL8CQ9h
2026-06-30 13:30:22 +02:00
2975b0563f fix(acr): honest absence-state copy for two GAP enhancement findings (M-BUG-15)
The t2_3 ("No path-scoped rules") and t3_6 ("No subagent isolation")
feature-gap checks iterate a collection (rule files / agent files) and return
false for an EMPTY one — so they fire even when the user has zero rules / zero
subagents, the same state their presence-gap siblings flag. But the humanized
titles presupposed the feature already exists:
  - "Your rules all load on every conversation" (with zero rules)
  - "Your subagents share Claude's main work folder" (with zero subagents)
The second directly contradicts GAP-005 "You haven't set up any specialized
helper agents yet" in the same report — a user cannot simultaneously have no
subagents and have subagents that lack isolation. Found by dogfooding the
analyze pipeline against linkedin-posts (premise-verifying each finding before
trusting the analyzer-agent's report).

Decided fix = align the two titles with the house "You haven't set up X yet"
absence framing (state-neutral: honest for both the zero-state and the
has-but-unconfigured state), NOT a base-feature presence gate in the scanner —
that would change the frozen v5.0.0 marketplace-medium baseline (zero
rules/agents, freezes these gaps firing) and break the RAW byte contract.
Mirrors M-BUG-14's humanizer-layer, copy-only approach.

- humanizer-data.mjs: title "Your rules all load on every conversation" ->
  "You haven't set up path-scoped rules yet"; title "Your subagents share
  Claude's main work folder" -> "You haven't set up subagent isolation yet".
  description + recommendation unchanged. t3_5/t3_7 ("Your skills don't ...")
  left as-is: their possessive is correct in the common case where skills
  exist; the zero-skills edge is latent, not manifest here.
- RED-first unit test pins both titles existence-neutral (forbids "your rules
  all load" / "your subagents"; feature still named).

Frozen v5.0.0 snapshots untouched (RAW bypasses humanizer); default-output
snapshot regenerated (2 titles only, no collateral). Verified on linkedin-posts:
GAP-004/005/011 now all consistently absence-framed. Suite 1356->1357/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01683eAqVecv9VZfQzL8CQ9h
2026-06-30 12:57:40 +02:00
eb0b3fd29d fix(acr): size-neutral copy for "CLAUDE.md not modular" GAP (M-BUG-14)
The t2_2 feature-gap check is a pure presence check (rules file OR @import
present) with no length gate, consistent with its t2_3/t2_4/t2_5 siblings.
But the humanized copy claimed the file is "one big block" and that splitting
makes it "easier on the loading time" — an unconditional size/load-cost
overclaim. For a ~625-token non-modular CLAUDE.md the load saving is trivial,
so the description lied. Found by dogfooding feature-gap against linkedin-posts.

Decided fix = copy-softening, NOT a length gate in t2_2 (that would make it the
only size-gated check among the presence-check siblings + risks byte-stability
if a short non-modular CLAUDE.md fixture exists in snapshots).

- humanizer-data.mjs: title "one big block" -> "all live in one file";
  description drops "long"/"loading time", keeps the honest structural
  "split into linked files with @import or .claude/rules/" framing.
  recommendation unchanged.
- RED-first unit test pins the size-neutral copy (no "big"/"long"/"loading
  time" in title/desc, structural split framing kept).

Frozen v5.0.0 snapshots untouched (RAW envelope bypasses humanizer); SC-5
default-output byte-stable (no non-modular GAP finding in those fixtures).
Suite 1355->1356/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01683eAqVecv9VZfQzL8CQ9h
2026-06-30 11:02:45 +02:00
f4bf3ae2cb fix(acr): feature-gap scopes presence checks to authored config + reads settings cascade (M-BUG-13)
The GAP scanner's 25 presence checks ran over the full includeGlobal discovery, so
this plugin's own examples/optimal-setup (vendored across plugin-cache versions)
satisfied every tier-3 check — masking real feature gaps to GAP=0 on ANY target.
And the real ~/.claude/settings.json is invisible to the settings-key checks
(includeGlobal gotcha + maxFiles cap), which would flip statusLine/autoMode to
false positives once the maskers were removed.

- isAuthoredConfig: exclude plugin-bundled (~/.claude/plugins/) + nested examples/
  and tests/fixtures/ (relPath-relative, so a fixture scanned AS the target keeps
  its own files) from ctx.files + parsedSettings.
- readSettingsCascade: read the user->project->local settings cascade directly and
  merge into parsedSettings — immune to the discovery cap/gotcha.

Empty target: ~0 (masked) -> 18 humanized opportunities; no statusLine/autoMode
false positives. Frozen v5.0.0 snapshots + SC-5/6/7 byte-stable (marketplace-medium
has no nested demo trees; hermetic-HOME cascade adds nothing). Suite 1350->1355/0.
Found by dogfooding feature-gap against the machine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01683eAqVecv9VZfQzL8CQ9h
2026-06-30 10:09:10 +02:00
b58393099a fix(acr): posture --output-file humanizes findings in default mode (M-BUG-12)
feature-gap.md (Step 3-4) and posture.md (Step 3-4) read findings from
`posture.mjs --output-file` and group on the humanizer fields
(userActionLanguage / userImpactCategory / relevanceContext). But posture.mjs
only humanized the stderr scorecard — its --output-file JSON wrote the raw
v5.0.0-shape `result`, so every finding's humanizer fields were `undefined`.
Both commands silently degraded to the raw tier-fallback: v5.1.0 plain-language
output was dead for feature-gap and for posture's finding-level grouping.

Re-derived on tests/fixtures/marketplace-medium: 17 GAP findings, all three
humanizer fields undefined in the default --output-file JSON.

Fix (posture-CLI-local, surgical): humanize the output-file payload in default
mode, mirroring scan-orchestrator.mjs:277 — but posture nests the scanner
envelope under `result.scannerEnvelope` (its `result` has no top-level
`scanners` array), so humanizeEnvelope is applied to `result.scannerEnvelope`,
not `result` (the latter would no-op). --json / --raw stay raw, so the
explicit-v5.0.0-shape contract and snapshot byte-compat are preserved.

TDD: red-first test in posture-humanizer.test.mjs default-mode block asserts
GAP findings in the output file carry userActionLanguage/userImpactCategory;
a --raw --output-file guard asserts the raw shape is unchanged.

Suite 1350/0 (+2). Frozen v5.0.0 + SC-5/6/7 + default-output snapshots
byte-stable: --json/--raw bypass the humanizer (their snapshot tests use those
flags), and the default --output-file JSON is not snapshot-pinned. Committed,
not released — batches with M-BUG-11 in a later hardening release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01683eAqVecv9VZfQzL8CQ9h
2026-06-30 09:10:07 +02:00
1d63492617 fix(acr): optimize lens scopes out plugin-bundled CLAUDE.md + unique candidate paths (M-BUG-11)
The optimize lens CLI fed its precision-gate agent every CLAUDE.md that discovery
returned, including the 256 files under ~/.claude/plugins/ — vendored plugin
CLAUDE.md across every cached version (7 config-audit, 6 ms-ai-architect, 5 okr,
...) plus their bundled tests/fixtures and examples. Running `optimize --global`
on this machine produced 454 candidates across 92 "files", ~250 of them sourced
from plugin-internal files a user cannot act on (the plugin overwrites them on
update). Same class as M-BUG-2: plugin-bundled config is not the user's cascade.

Second defect: candidates were keyed by `relPath || absPath`, but relPath
collides across scopes — a repo-root `CLAUDE.md` and the user-global
`~/.claude/CLAUDE.md` both relPath to `CLAUDE.md`. The two files that actually
matter were merged into one indistinguishable bucket (21 candidates), the agent's
Read(file) would resolve the wrong one, and cache-file relPaths were not readable
relative to cwd at all.

Fix (lens-CLI-local, surgical):
- Filter isPluginBundled (absPath under `.claude/plugins/`) from discovery for
  BOTH halves of the motor (candidate loop + the OPT scanner, which reads
  discovery.files directly). Drops vendored files regardless of active/stale
  version, so excludeCache is unnecessary here.
- Key each candidate by absPath: unique + readable.
No change to file-discovery.mjs or the OPT scanner, so their byte-stable
snapshots are untouched.

Suite 1348/0 (+4: candidate scoping, real-config survives, deterministic scoping,
absolute-path identity). Frozen v5.0.0 + SC-5 snapshots untouched (the lens CLI
has no snapshot; the command is agent-driven, not byte-stable). Dogfood ~/.claude
`optimize --global`: candidates 454->45, deterministic 2->0 (both were stale
plugin-cache copies), distinct files 92->11, repo vs user-global now distinct
(12 + 9 = 21). Residual 45 includes config-audit's own tests/fixtures CLAUDE.md
(repo-specific dogfooding artifact, not a general bug — left alone).
2026-06-30 06:44:36 +02:00
e8afb148d3 fix(acr): conflict-detector segregates plugin-bundled configs (M-BUG-2)
CNF compared every discovered settings.json/hooks.json pairwise regardless of
origin, so it treated installed plugins' bundled configs — each plugin's own
settings.json/hooks.json plus its shipped test fixtures and examples under
~/.claude/plugins/ — as if they were the user's authored cascade. A "conflict"
between two plugins' bundled test fixtures is not something a user can resolve,
yet these dominated the count: 339 CNF findings on this machine (315 high-sev
permission allow/deny "conflicts", 18 duplicate-hook, 6 settings-key), almost all
sourced from plugin-internal fixtures. The Conflicts grade was F on pure noise.
Fix: CNF excludes any file whose path is under `.claude/plugins/` from conflict
analysis (new isPluginBundled predicate; absPath marker). Kept CNF-local rather
than a discovery-level skip on purpose: an active plugin's contributed
hooks.json/.mcp.json legitimately lives in plugins/cache and other scanners need
it — only conflict analysis must ignore plugin-bundled files. Same class as
M-BUG-8 (non-live config trees treated as live). Suite 1344/0 (+3: plugin-bundled
exclusion, discovery-side sanity, over-exclusion guard). Frozen v5.0.0 + SC-5
snapshots untouched (marketplace-medium has no plugins/ paths), no re-seed.
Dogfood ~/.claude CNF 339->0 (F-grade was 100% plugin-bundled noise; the ~3
genuine user-scope local settings have no actual conflicting keys, matching the
plan C5 "real surface ~3 files" prediction).

Follow-up (not in this fix): classifyScope tags plugin-bundled files by checking
basePath instead of the file's own path, so scope:'plugin' is effectively dead
for a ~/.claude-rooted scan. Fixing it would let every scanner trust the scope
field, but that is a discovery-layer change beyond this bug's scope.
2026-06-26 17:24:08 +02:00
3cf5c714a2 fix(acr): SET typo-gates unknown-key false positives (M-BUG-10)
The CC settings schema is passthrough (verified against the 2.1.193 binary): it
forwards unrecognized keys unchanged rather than rejecting them, so an arbitrary
unknown key is valid/forward-compatible, not an error — the finding's "silently
ignored" claim was factually wrong. The only real risk is a TYPO of a real key
(the intended setting then silently has no effect). Fix: flag an unknown key only
when it closely matches a known key (new levenshtein helper; edit distance <= 2,
both keys >= 4 chars); severity medium -> low; honest passthrough framing in the
scanner + humanizer. Also refreshed KNOWN_KEYS with 6 binary-verified keys
(agentPushNotifEnabled, remoteControlAtStartup, skipAutoPermissionPrompt,
skipDangerousModePermissionPrompt, skipWorkflowUsageWarning, tui). Suite 1341/0
(+12). Frozen v5.0.0 snapshots untouched (0 CA-SET findings there), no re-seed.
Dogfood ~/.claude/settings.json 6->0 (all 6 keys above were false unknown-key
findings; 0 typo flags introduced across 167 walked files).
2026-06-26 15:15:14 +02:00
7e94910566 fix(acr): token estimator discounts block-level HTML comments (M-BUG-6)
CLAUDE.md token estimates counted block-level <!-- --> HTML comments toward
always-loaded tokens, but CC strips them before injection (preserved only inside
code fences, per code.claude.com/docs/en/memory). Fix: new stripInjectedHtmlComments
+ effectiveMemoryBytes in active-config-reader; the CML cascade (walkClaudeMdCascade)
and token-hotspots now size CLAUDE.md from effective (stripped) bytes, while raw byte
figures stay honest. Block-level only — inline comments retained (conservative,
verified scope). Suite 1329/0 (+13). Frozen v5.0.0 snapshots untouched (no fixture
has <!--), no re-seed. Dogfood ~/.claude CLAUDE.md ~3386->3301 tok (~85 tok discount,
matches worklist prediction).
2026-06-26 14:29:24 +02:00
dd9db60fc9 fix(acr): CPS ignores fenced/inline code + CC-stable path vars (M-BUG-7)
CPS flagged ${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR} (CC-provided stable
paths) and {date}/timestamp tokens shown in documentation as cache-busters.
Fix: skip fenced code blocks, strip inline-code spans, and whitelist CC-stable
vars before pattern-matching. Suppress-only — frozen v5.0.0 snapshots untouched
(CPS yields findings:[] there), no re-seed. Suite 1316/0 (+6). Dogfood ~/.claude
5->2 (3 doc false-positives suppressed; 2 remaining = own volatile test fixtures).
2026-06-26 12:45:14 +02:00
bfd577aeee fix(acr): file-discovery skips backups/ dirs — never live config (M-BUG-8)
A directory named `backups` holds backup COPIES, not live config, so walking
it during a config audit produces stale findings. config-audit's own session
backups (~/.claude/config-audit/backups/<ts>/files/.../CLAUDE.md) were the
canonical case: a ~/.claude-scope audit walked 36 frozen config copies as if
live, polluting CPS (C3) and HKV/RUL (C6) results. Same non-live-noise family
as M-BUG-2.

- Add `backups` to SKIP_DIRS (broad, name-based — consistent with vendor/dist/
  .cache): the rule is general, a backups/ dir is never live config.

TDD: 2 failing tests (no config under backups/ discovered + backups/ counted
as skipped) -> green; a third asserts live config beside the backups tree is
still discovered. Full suite 1310/0 (+3). Byte-stable: no fixture is named
`backups` and `backups` appears in zero frozen snapshots, so v5.0.0 + SC-5 +
default-output outputs are unchanged. Dogfooding ~/.claude: files under
/backups/ drop from 36 -> 0; 717 live config files retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnUvKEqyEa1m9gy6Aqhdqq
2026-06-26 12:23:43 +02:00
18af5a24e9 fix(acr): RUL resolves rule glob against the rule's own project root, not the scan root (M-BUG-9)
A rule's paths:/globs: pattern scopes relative to the directory containing
the rule's .claude/, not the outer scan target. countGlobMatches globbed
against the scan root and collectProjectFiles' depth>4 cutoff never reached
deep matching files, so a live rule in a nested repo (e.g. a marketplace
checkout under ~/.claude) was wrongly flagged "matches no files / never
activates" (high) — a false F-grade for any user with rules in a nested repo.
Same scope-conflation family as M-BUG-1/2/8.

- deriveProjectRoot(ruleAbsPath): parent of the rule's .claude segment.
- collect + glob per project root (cached), relative to that root — so a
  nested repo's rule resolves against its own tree, where its files live.
- user-global rules (root === HOME) skip the no-match check: they scope
  against whatever project is active at runtime, not a fixed tree, so
  "matches 0 files here" is not a dead-rule signal (and avoids a HOME walk).

TDD: 2 failing tests (nested-repo false-positive + HOME guard) -> green.
Full suite 1307/0; frozen v5.0.0 + default-output snapshots unchanged (RUL
appears in none; the fix is a no-op when projectRoot === targetPath, i.e. the
common single-repo scan). Dogfooding C6: clears the 2 ktg-privat false
positives on the real machine and surfaces a previously-hidden genuine dead
rule (false negative) in the bundled optimal-setup example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnUvKEqyEa1m9gy6Aqhdqq
2026-06-26 10:56:39 +02:00
7f097d524f fix(acr): enumerateAgents counts only CC-registered agents — recurse + frontmatter filter + HOME dedup (M-BUG-3/4/5)
enumerateAgents previously counted every .md in an agents dir as an
always-loaded agent. Per the official CC subagents docs, CC registers a
subagent only when its frontmatter declares both name and description, scans
agents dirs recursively, and (at a HOME self-scan) must not count
~/.claude/agents twice.

- M-BUG-5: require valid name+description frontmatter; frontmatter-less files
  are registration no-ops costing 0 always-loaded tokens. Fixes the user-agent
  over-count (this machine: 13 -> 0).
- M-BUG-3: listMarkdownFiles gains opt-in recursion; enumerateAgents recurses
  so agents in subfolders (agents/review/x.md) are counted, matching CC.
- M-BUG-4: configDirs dedupes project==user paths, so a `manifest --global`
  self-scan (repoPath===$HOME) counts ~/.claude once (user scope), killing the
  spurious "project 13" double-count. Benefits rules/agents/output-styles.

TDD: 4 failing tests -> green. Full suite 1305/0; --json/--raw byte-stable,
frozen v5.0.0 + SC-5 + default-output snapshots untouched (no snapshot records
agent enumeration rows).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnUvKEqyEa1m9gy6Aqhdqq
2026-06-26 00:19:27 +02:00
be1056aac0 fix(acr): enumeratePlugins honors enabledPlugins + polyrepo cache installPaths (M-BUG-1)
active-config-reader walked ~/.claude/plugins/marketplaces and ignored both the
enabledPlugins toggle and the polyrepo cache layout. On a polyrepo machine it
counted disabled/uninstalled marketplaces plugins as "active" while MISSING the
actually-enabled plugins installed under plugins/cache. This corrupted the agent
listing and every pluginList consumer (manifest, AGT, whats-active, hooks, rules).

Now: when installed_plugins.json is present, inject only plugins that are in the
manifest AND enabledPlugins[key]===true, each resolved to its active installPath
(incl. cache/). When the manifest is absent (fixtures/pre-v2 installs), fall back
to the historic marketplaces walk rather than silently dropping config — mirrors
file-discovery.mjs's "trust installed_plugins.json" contract.

Verified on real machine: agent listing 114->104, ghost plugins (newsletter,
content-machine, harness, kiur, ...) gone, voyage/linkedin/ms-ai/okr now correctly
counted. Full suite 1301/0; byte-stable snapshots untouched (hermetic empty HOME).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrTb8ktf1XZWEVwgz5MTTo
2026-06-24 10:50:28 +02:00
45efed3dbf fix(tok): live-session caveat in Pattern H stale-cache recommendation
Pattern H ("Stale plugin-cache versions") recommended deleting stale
version dirs under ~/.claude/plugins/cache with no warning that a
currently-running session may still hold one of those versions for its
whole lifetime. "Stale" is judged against installed_plugins.json (what
NEW sessions load), so the recommendation could reproduce the exact
footgun that broke a live session during C4: deleting the dir pulls the
files out from under the running session, which then breaks and must
/exit + restart.

Extend the recommendation text with the live-session caveat. No new
finding ID/scanner, no token counts changed (recommendation string only)
-> --json/--raw stay byte-stable, frozen v5.0.0 + SC-5 + default-output
snapshots untouched. +1 test (1297).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:04:47 +02:00
cf75249b5e feat(skl,cml): --context-window auto model→window probe (v5.12 B8b) [skip-docs]
Completes the deferred B8 half. `--context-window auto` now probes the
configured model and calibrates SKL/CML budgets to its real window instead
of always falling back to the conservative advisory anchor.

- lib/context-window.mjs: pure modelToContextWindow() maps known 1M-tier
  model IDs (Fable 5, Opus 4.8/4.7/4.6, Sonnet 4.6 — verified June 2026 —
  plus the explicit [1m] tier tag, dated/provider-prefixed IDs, and the
  opus/sonnet/fable aliases) to the 1M window; unknown/unconfirmed -> null
  (caller keeps the conservative anchor). resolveContextWindow() auto branch
  now probes opts.model: recognized -> auto-probed (not advisory); unknown
  or unpinned -> auto-unresolved (advisory, pre-B8b behavior).
- lib/active-model.mjs (new): resolveActiveModel() reads the model the way
  Claude Code resolves it — shell ANTHROPIC_MODEL override, then settings
  cascade local > project > user. Injectable env, hermetic under test HOME.
- scan-orchestrator: resolves the active model only when the flag is `auto`
  and threads it into resolveContextWindow; posture inherits via runAllScanners.

Default (no flag) and explicit --context-window <n> paths ignore the model
and stay byte-stable; frozen v5.0.0 + SC-5 snapshots untouched. TDD: 17 new
tests (context-window mapping/probe + active-model cascade). Suite 1296 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 22:29:12 +02:00
2082b7d112 feat(skl,cml): --context-window calibration, advisory when unknown (v5.11 B8) [skip-docs]
SKL-002 (skill-listing budget) and CML char-budget now calibrate to a
resolved context window instead of always anchoring at 200k:

- resolveContextWindow(): --context-window <n> calibrates; 'auto' keeps the
  conservative 200k anchor but marks advisory (model→window probing deferred
  to B8b); no flag → 200k anchor, byte-identical to pre-B8 default.
- scaleForWindow(): linear off the 200k anchor (identity at the anchor).
- SKL + CML each keep an untouched default branch (window===200k && !advisory)
  for byte-stability and a calibrated branch; advisory downgrades the budget
  finding from a breach (low/medium) to info.
- Flag wired through scan-orchestrator + posture; runAllScanners resolves once
  and threads { contextWindow } to scanners (others ignore the 3rd arg).
- CPS intentionally excluded: it has no window-anchored budget (fixed
  150-line volatility heuristic), so there is nothing to calibrate.

15 new tests; e2e CLI verified (1M suppresses SKL-002, auto → info, default
unchanged); full suite 1279 green; snapshots byte-stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:44:52 +02:00
27988801be feat(skl): flag oversized skill bodies on demand (v5.11 B7) [skip-docs]
New CA-SKL-003 (low): a SKILL.md body over ~5,000 tokens (~500 lines)
should split reference content into supporting files / use context: fork.

- measureActiveSkillListing() now returns body metrics (chars/lines/tokens);
  the body was already read in full, only the frontmatter was parsed before.
- Honest framing: BODY_CALIBRATION_NOTE marks this as ON-DEMAND cost (loads
  only when the skill is invoked, NOT every turn like the always-loaded
  listing) and an estimate — hence low severity.
- 5 new tests; full suite 1262 green; snapshots byte-stable (default branch
  untouched; new finding fires only on bodies >5k tok, none in fixtures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:36:20 +02:00
fa1ddd963a feat(cps): scan @imported files for volatile cached-prefix content (v5.10 B6) [skip-docs]
CPS originally inspected only files discovery classifies as claude-md, but a
CLAUDE.md can pull arbitrary files into the cached prefix via @import — and
those targets (e.g. @shared/conventions.md) are usually not claude-md in
discovery, so their inlined content was never scanned. Neither TOK Pattern A
(top-30 of cascade files) nor the in-file CPS scan reaches past the importing
file, so volatility inside an imported file was invisible.

B6 closes the gap: for each @import whose import site sits within the
cached-prefix window (imp.line <= CACHED_PREFIX_LINES), CPS resolves the path
(resolveImportPath, mirroring import-resolver/token-hotspots semantics), reads
the target, and runs findVolatileLines over its first 150 lines. A hit emits a
distinct medium finding — "Volatile content in @imported file breaks cached
prefix" — keyed on the resolved file, evidence naming the importer.

Scope boundaries (deliberate):
- One hop only; imports-of-imports stay with IMP (deep-chain owner).
- No lines-1-30 skip for imported content — that exclusion is root-file-specific
  to avoid Pattern A overlap, which never reaches imported files.
- No double-reporting: an import resolving to a discovered claude-md is skipped
  (own iteration); a reportedImports set dedupes a target imported by several
  CLAUDE.md files.

Dropped from B6 (per plan verdict): confident behavioral cache-buster detection
(opusplan/model-switch is runtime, not static config) and jq-transcript
automation. "No overstated behavioral finding ships" — even the permitted
opusplan info-advisory was left out; the @import extension is the whole of B6.

Byte-stability: the in-file finding keeps the same condition + byte-identical
evidence/description (continue-skip refactored to if-emit, behaviour-preserving);
new findings fire only on a volatile import, which no frozen v5.0.0 fixture has.
docs: README + scanner-internals CPS rows + full B6 note; CLAUDE.md kept lean
([skip-docs]). Suite 1254 -> 1257 green; snapshots + SC-5 untouched.
Version/badges/CHANGELOG wait for the v5.10 release cut.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:45:12 +02:00
d2c45a3bb8 feat(hooks): additionalContext injection advisory + filter-before lever (v5.10 B5) [skip-docs]
HKV now flags hooks that build hookSpecificOutput.additionalContext from
un-grepped command output as an INFO advisory (weight 0, never severity-bearing
— excluded from the self-audit nonInfo set). That field enters Claude's context
every time the hook fires (plain stdout on exit 0 does not), so an unfiltered
payload is a recurring per-turn token cost.

- New lib scanners/lib/hook-additional-context.mjs: pure assessHookAdditionalContext
  (unit-tested, no IO) + IO wrapper assessHookContextForRepo (walk hooks->scripts).
  Heuristic: additionalContext + verbose-prone capture (cat/git log/execSync/…) &&
  no filter (grep/head/jq/.slice). Deliberately low precision -> advisory only.
- HKV: emits the advisory inline on scripts it already reads (after the M5 verbose
  check). Additive, info severity -> frozen v5.0.0 + SC-5 snapshots untouched.
- feature-gap: new filterHookLeverFinding companion, fires ONLY when >=1 chatty
  hook is detected — surfaces the filter-before-Claude-reads lever (CC
  filter-test-output.sh). Silent otherwise (opportunity, not noise).
- docs: README HKV + GAP scanner rows; scanner-internals.md HKV row + full B5
  implementation note. CLAUDE.md kept lean ([skip-docs]); B5 fully documented in
  README + scanner-internals.

Mechanism verified 2026-06-23 against code.claude.com/docs context-window.md
(additionalContext enters context; plain stdout does not). Suite 1239 -> 1254 green.
Version/badges/CHANGELOG wait for the v5.10 release cut.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:28:33 +02:00
8f7e196046 feat(tokens): MCP tool-schema deferral check + CLI-over-MCP lever (v5.10 B4)
By default Claude Code defers MCP tool schemas (names-only, ~120 tok; full
schemas on demand via tool search). CA-TOK-006 detects config-file signals that
force the FULL schemas into the always-loaded prefix every turn:
  - settings.json env.ENABLE_TOOL_SEARCH="false"          (high)
  - "ToolSearch" in permissions.deny                       (high)
  - configured model is a Haiku model                      (medium)
  - per-server .mcp.json alwaysLoad:true (CC v2.1.121+)    (high)
auto[:N] is threshold mode (info, not a trigger).

New engine lib/mcp-deferral.mjs: pure assessMcpDeferral({settings,mcpServers})
(unit-tested, no IO) + thin IO wrapper assessMcpDeferralForRepo shared by TOK and
GAP. Severity scales with aggregate forced-upfront tokens (medium-confidence
reasons cap at medium). feature-gap cliOverMcpLeverFinding fires only as a
companion to CA-TOK-006 (prefer gh/aws/gcloud over MCP for common ops).

Honest scoping (Verifiseringsplikt): triggers on config files ONLY — never
process.env shell vars. Vertex / custom ANTHROPIC_BASE_URL / runtime /model
switch are launch state (would flap snapshots machine-dependently), so they are
DISCLOSED in every finding, not triggered. Tool-level anthropic/alwaysLoad and
claude.ai connectors likewise disclosed. Mechanism verified 2026-06-23 against
code.claude.com/docs (context-window.md, mcp.md#configure-tool-search +
#exempt-a-server-from-deferral, costs.md); the prefix-cache invalidation claim
was NOT-CONFIRMED in docs and is not asserted.

alwaysLoad added to CA-MCP VALID_SERVER_FIELDS (no longer flagged as unknown).
active-config-reader surfaces per-server alwaysLoad. Byte-stable: CA-TOK-006
fires only on new conditions; frozen v5.0.0 + SC-5 snapshots untouched.
Tests 1215 -> 1239 (engine 16, integration 5, lever 2, mcp-field guard 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:59:14 +02:00
ba9f82f952 feat(tokens): stale plugin-cache disk-cleanup finding, honestly labelled Dead config (v5.9 B3b)
The B3a cache filter already exposes discovery.staleCacheVersions; surface them
as a finding so the user knows the superseded plugin versions are safe to delete.

Honesty (Verifiseringsplikt): the finding loads on ZERO turns, so it must NOT
read as a per-turn token cost. TOK normally humanizes to "Wasted tokens"; a new
per-finding category override ('plugin-cache-hygiene' -> "Dead config") plus a
dedicated humanizer translation ("Old plugin versions are sitting on disk (safe
to delete) ... cost zero tokens per turn ... housekeeping, not a performance
problem") keep the prose accurate instead of the generic "using more space"
default. evidence carries the explicit "zero live-context impact" note.

- token-hotspots: Pattern H emits CA-TOK (low) from discovery.staleCacheVersions
  when stale versions exist (`--global`); silent otherwise.
- humanizer: CATEGORY_TO_IMPACT lets a finding's category override the
  scanner-default impact label (raw `category` field unchanged -> --json/--raw
  byte-stable). humanizer-data: honest static translation for the finding.
- Tests: finding fires/severity/category, lists stale keys + zero-impact note,
  silent when none; humanizer override -> Dead config; honest translation locked.
- Docs: tokens command render note (disk-cleanup, not a token problem) +
  --no-exclude-cache flag; README + CLAUDE.md rows (7 patterns, cache-aware).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:17:55 +02:00
a371832688 feat(discovery): version-aware cache filtering + --exclude-cache flag (v5.9 B3a) [skip-docs]
Stale ~/.claude/plugins/cache versions polluted token-hotspots ranking and
inflated CNF duplicate-hook findings with config that loads on zero turns.
installPaths point INTO the cache, so a blunt "skip all of plugins/cache"
would drop ACTIVE plugins — the filter is therefore version-aware: it reads
the adjacent installed_plugins.json, keeps active version dirs, drops only
stale ones (and exposes them via discovery.staleCacheVersions for B3b).

- file-discovery: cacheVersionKey() + applyCacheFilter() (active vs stale via
  installed_plugins.json; HOME-independent, derives the manifest from the cache
  path); discoverConfigFiles/Multi gain { excludeCache } + staleCacheVersions.
  Absent/unparseable manifest -> no filtering (never silently drop live config).
- token-hotspots-cli + scan-orchestrator: --exclude-cache (default ON for these
  live-cost scans) / --no-exclude-cache restores the full walk.
- Tests: cacheVersionKey unit cases; stale-dropped/active-kept/no-manifest
  discovery cases; CNF-drop proof (soft-spot verified, not assumed:
  include=1 -> exclude=0 duplicate-hook findings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:57:22 +02:00
d664b70520 feat(campaign): refresh-tokens live cross-repo token sweep (v5.9 B2b-2) [skip-docs]
The IO half of the machine-wide token roll-up (B2a shipped the pure data model).
New campaign-write-cli subcommand 'refresh-tokens': for every tracked repo it runs
readActiveConfig → buildManifest → splitManifestByOwnership, stores each repo's
per-repo delta via setRepoTokens, and captures the HOME-derived shared global layer
ONCE (from the first repo that reads cleanly) via setSharedGlobal.

Capturing shared once is the structural counted-once guard: a repo that fails to
read is recorded in 'skipped' and its delta omitted, never aborting the sweep.
Idempotent — a re-sweep replaces (setters overwrite), never accumulates. Determinism
unchanged: the clock is still read only via --reference-date.

TDD: 3 integration tests first (RED → GREEN) against a fixture with a dominant global
CLAUDE.md + two repos. Counted-once guard asserts each stored delta stays a small
fraction of the shared layer (a double-fold would make each delta EXCEED shared).
Caught a per-repo-MCP misclassification (fixed in 82f881a) + a test-side shape slip
(rollUp flattens its token buckets to numbers; stored summaries keep {tokens,count}).
Suite 1198 green (1195 + 3). Manifest + all frozen snapshots untouched.

[skip-docs]: internal sweep CLI (campaign plumbing), no user-facing surface yet. The
rendered token-bill in commands/campaign.md + CLAUDE.md rows land in B2b-3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:23:15 +02:00
82f881afc4 fix(manifest): classify ~/.claude.json:projects MCP as per-repo delta, not shared [skip-docs]
readClaudeJsonProjectSlice(repoPath) returns the slice keyed to the SPECIFIC
repo path (exact / longest-prefix match), so MCP servers under
~/.claude.json:projects are per-repo: they load only in their own project and
differ across repos. B2b-1 wrongly grouped them with the shared global layer
(reasoning from file location, not the keyed slice). The live sweep captures
the shared layer ONCE from the first repo — folding a per-repo slice into it
would silently drop every other repo's claude.json MCP servers.

Corrected: SHARED_GLOBAL_SOURCES = {user, managed}; the only machine-global
MCP is plugin-provided (plugin: prefix). Per-repo MCP (.mcp.json AND the
~/.claude.json project slice) → delta. Tests updated.

[skip-docs]: internal classifier correction, no user-facing surface yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:14:28 +02:00
872b8ac281 feat(manifest): ownership split for machine-wide token roll-up (v5.9 B2b-1) [skip-docs]
Pure classifier splitManifestByOwnership(sources) → {shared, delta}, the
FS-free core that B2b's live cross-repo sweep will feed into the campaign
ledger's setSharedGlobal/setRepoTokens (B2a).

classifyOwnership maps each source string to its layer:
  shared : user | managed | plugin:* | ~/.claude.json:projects (global MCP)
  delta  : project | local | .mcp.json | @import | unrecognized

Anything not positively global falls to delta, so a source is never silently
folded into the once-counted shared layer (a wrong fold HIDES machine-wide
cost; a wrong delta is at worst visibly attributed to a repo). Both layers
carry the canonical summarizeByLoadPattern shape so the ledger setters consume
them verbatim.

TDD: 6 unit tests first (RED → GREEN). buildManifest/CLI output unchanged →
manifest snapshot byte-identical. Suite 1195 green (1189 + 6).

[skip-docs]: internal pure export only, no user-facing surface yet. User-facing
docs (commands/campaign.md + CLAUDE.md manifest/campaign rows) land in B2b-3
when refresh-tokens + the rendered token-bill ship.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:10:03 +02:00
cefa751990 feat(campaign): machine-wide always-loaded token roll-up — pure layer (v5.9 B2a)
Extend the campaign ledger with a machine-wide always-loaded token bill, the
pure data-model + aggregation half of B2. Deliberately does NOT do live I/O:
the cross-repo readActiveConfig sweep that populates real numbers is B2b.

Design — shared layer counted ONCE, structurally:
- Ledger root gets an optional `sharedGlobal` summary (the always-loaded layer
  paid in every repo: global CLAUDE.md + agent listing + global MCP + unscoped
  global rules + active plugins' always components), stored ONCE via the new
  setSharedGlobal().
- Each repo entry gets an optional `tokens` summary (its PER-REPO delta only),
  set via the new setRepoTokens(); addRepo now seeds `tokens: null` (mirrors
  findingsBySeverity).
- rollUp() stays PURE and gains a `tokens` aggregate: machineWide = sharedGlobal
  + Σ(per-repo deltas), so the shared layer is counted exactly once by
  construction — the structural guard against the historic double-count bug.
  Plus a `byRepo` table ranked DESC by always-loaded cost ("most expensive
  repos") with a deterministic name tie-break.

Summary shape mirrors manifest's summarizeByLoadPattern exactly
({always|onDemand|external|unknown: {tokens,count}}) so B2b wires in trivially.

Keeps rollUp pure (no filesystem I/O) → byte-stable, respects the THIN campaign
motor invariant. Chosen over the plan's literal "run readActiveConfig inside
rollUp" which would break both (operator-approved deviation).

- 8 new ledger tests incl. the counted-once regression guard + byRepo ranking.
- Updated addRepo shape test (+tokens:null) and campaign-cli EMPTY_ROLLUP.
- No frozen snapshot affected (campaign is v5.7, absent from v5.0.0 baselines).
  Suite green: 1189 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:57:31 +02:00
2a3cb537f9 feat(scanner): add AGT per-agent description bloat advisory (v5.9 B1-rest)
Flag any single active agent whose description exceeds a 500-char soft cap
(the same bloat threshold TOK pattern F applies to SKILL.md descriptions).
Every char of an agent description re-enters context in the always-loaded
agent listing on every turn, so a long one is a per-turn cost.

Framing is deliberately ADVISORY, severity low: unlike CA-SKL-001 agents
have NO verified per-description cap, so nothing is dropped — this is a bloat
advisory, never a truncation claim. Per-agent advisories are emitted BEFORE
the aggregate roll-up (mirrors SKL 001->002).

- PER_AGENT_DESC_SOFT_CAP (=500) added to lib/agent-listing-budget.mjs as the
  single source of truth, disclosed as a heuristic.
- Per-agent loop reuses measureActiveAgentListing()'s agents[] (name, source,
  pluginName, path, descLength) — no new enumeration.
- 5 new tests (fire/strict-boundary/no-truncation-claim/recommendation/order).

Byte-stable: AGT already stripped from frozen v5.0.0 baselines; SC-5 unchanged
(HOME-scoped, hermetic HOME has no agents). Suite green: 1181 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:27:24 +02:00
fcfb2979ef feat(scanner): add AGT agent-listing always-loaded budget finding (v5.9 B1)
New AGT scanner flags the aggregate always-loaded cost of the agent listing
(every active agent's name+description is injected each turn so the model
knows what it can delegate to). Mirrors the SKL skill-listing pattern but
encodes the key honesty caveat: the agent-listing mechanism is INFERRED
(agents are absent from Claude Code's documented context breakdown), so the
token figure is an UPPER-BOUND estimate and the aggregate budget is a
config-audit heuristic anchored on 200k — not a CC-documented allotment.

- scanners/agent-listing-scanner.mjs + scanners/lib/agent-listing-budget.mjs
- 8 TDD tests (tests/scanners/agent-listing-scanner.test.mjs)
- AGT folds into the Token Efficiency health area (scoring.mjs)
- byte-stability: AGT added to strip-added-scanner (frozen v5.0.0 baselines
  left untouched, same as OST/OPT); SC-5 default-output snapshot refreshed
- suite 1168 -> 1176 green

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:02:59 +02:00
319e5541c9 feat(campaign): plan export + execution-by-reuse (v5.7 Fase 2 Block 4c)
Completes Block 4 (4b backlog + 4c export/execution). Asymmetric: plan
export is new testable code; execution is pure reuse of the existing
per-repo implement/rollback (no new execution machinery), per the plan's
"reuse existing backup/rollback".

Plan export ("planer følger arbeidsstedet"):
- scanners/lib/campaign-export.mjs (pure, now injected, 8 tests):
  planExportPath(repo,sessionId) -> <repo>/docs/config-audit-plan-<sessionId>.md
  (sessionId-keyed so same-day re-audits never collide);
  buildPlanExportDocument({...,now}) -> provenance header + verbatim plan.
- scanners/campaign-export-cli.mjs (-cli, read-only by default, 10 tests):
  --repo resolves the repo's linked session, reads its action-plan.md,
  assembles the doc, emits {exportable,problems,targetPath,document}. Two
  gates -> exit 1 advisory: no-session-linked / no-action-plan. Writes the
  file ONLY under opt-in --write (byte-faithful copy; the LLM never re-types
  a 200-line plan). --sessions-dir override for hermetic tests; exit 0/1/3.

Command: commands/campaign.md gains an `export <path>` mode (Step 6:
preview -> approve -> --write), then routes the user to the existing
/config-audit implement (backup + verify) + rollback + set-status
implemented. Nothing auto-written (Verifiseringsplikt).

Byte-stable: lib + -cli + command-doc only -> scanner count stays 15,
agents 7, commands 21 (export is a mode, not a new command), SC-5 +
backcompat suite untouched. suite 1150->1168. Block 4a (migrateLedger)
still deferred to the first breaking schema change.

Docs: CLAUDE.md section + badge 1150->1168/65->67 files; README badge +
campaign row + Testing prose (fixed stale 1055/59 -> true 1168/67).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:08:04 +02:00
49833aded8 feat(campaign): cross-repo prioritized backlog (v5.7 Fase 2 Block 4b)
buildBacklog(ledger) pure transform + read-only campaign-cli payload field
+ command rendering. The single machine-wide pick-list: per-repo (the ledger
tracks severity counts, not individual findings), severity-weighted
(SEVERITY_WEIGHTS c1000/h100/m10/l1), deterministic tie-break, excludes
implemented/pending/zero-finding repos.

No schema change, no new scanner -> scanner count stays 15, snapshot/backcompat
byte-stable. suite 1138->1150 (lib +9, campaign-cli +3). README badge 1091+->1150+.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 02:44:21 +02:00
ee0c762151 feat(campaign): /config-audit campaign — human-approved write-CLI + command (v5.7 Fase 2 Block 3c)
Completes the THIN machine-wide campaign surface (ledger + roll-up + status).
The write half of the campaign motor, mirroring how knowledge-refresh gates
register writes:

- scanners/campaign-write-cli.mjs (-cli → NOT a scanner; 11 tests): init/add/
  set-status, each a thin wrapper over the invariant-enforcing lib transforms
  (createLedger/addRepo/setRepoStatus) + saveLedger — path-normalization/dedup,
  idempotent add, status-lifecycle guard and updatedDate bump never hand-rolled.
  init refuses to clobber an existing/corrupt ledger (exit 1, file untouched);
  add auto-inits + reports added vs skipped; set-status takes --findings/--session.
  Deterministic: --reference-date is the only clock read, injected as `now`.
  Exit 0=write, 1=advisory no-op, 3=error.
- commands/campaign.md (opus, no Web): thin orchestrator — always reports first
  (read-only campaign-cli), then proposes init/add/set-status and invokes ONE
  write-CLI subcommand only on explicit human approval (Verifiseringsplikt; never
  hand-edits the JSON). add --discover finds git repos under a root to pick from.

Not byte-stable (own command, outside snapshot suite) like /config-audit optimize
+ knowledge-refresh. Both CLIs are -cli → scanner count stays 15, snapshot suite
untouched. commands 20→21, suite 1127→1138 (+11), test files 64→65.

Docs: CLAUDE.md §campaign-write-cli + command table + Testing badge; README
commands badge 20→21 + table row; help.md + router wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:25:15 +02:00
4be7a16788 feat(campaign): read-only campaign-cli ledger reporter (v5.7 Fase 2 Block 3b)
Read-only reporter over the durable campaign-ledger core (Block 3a). Mirrors
knowledge-refresh-cli: the deterministic, READ-ONLY half of the campaign motor.

- scanners/campaign-cli.mjs (-cli → not an orchestrated scanner): loadLedger +
  validateLedger + rollUp, emits {status, initialized, ledgerPath, schemaVersion,
  createdDate, updatedDate, repos, rollUp} JSON. NEVER writes — a missing ledger
  is reported gracefully (initialized:false), never created. init + status
  transitions belong to the Block 3c command layer (human-approved writes).
- --ledger-file overrides default path (deterministic testing); --output-file
  mirrors the sibling. Exit: 0 = initialized & valid, 1 = not initialized
  (advisory), 3 = error (parse/corrupt/invalid).
- 8 tests (tests/scanners/campaign-cli.test.mjs). Suite 1119→1127 green hermetic;
  scanner count stays 15 (-cli), SC-5 byte-stable.
- CLAUDE.md: §campaign-cli added; Testing badge corrected 1091/62 → 1127/64
  (fixes pre-existing Block 3a drift).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:13:27 +02:00
f93830ce74 feat(campaign): durable machine-wide campaign-ledger core (v5.7 Fase 2 Block 3a THIN)
Add the durable ledger that sits ABOVE individual config-audit sessions for a
machine-wide audit campaign: repo list + per-repo lifecycle (pending → audited →
planned → implemented) + a machine-wide roll-up by status and severity.

scanners/lib/campaign-ledger.mjs — same hybrid split as knowledge-refresh:
- PURE transforms (createLedger / addRepo / setRepoStatus / rollUp) with `now`
  injected (YYYY-MM-DD, never the clock) → deterministic + unit-testable.
- soft validateLedger (returns {valid,errors}, never throws) for loaded data;
  transforms throw on programmer error (invalid status, unknown path).
- thin IO shell (defaultLedgerPath / loadLedger→null-on-ENOENT / saveLedger).
- persists to ~/.claude/config-audit/campaign-ledger.json — OUTSIDE the plugin
  dir (next to sessions/) so it survives uninstall/reinstall/upgrade.
- schemaVersion stamped from the start → cheap Block 4 migration.

THIN scope (Block 3a, operator-approved): ledger + roll-up + persistence only —
no CLI/command/execution (Blocks 3b/3c/4). Internal plumbing, byte-stable until
consumed: no `export async function scan` + lives in lib/ → scanner count stays
15, no orchestrator wiring, SC-5 unchanged.

tests/lib/campaign-ledger.test.mjs — 28 tests (TDD, red→green). Full hermetic
suite 1091→1119 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:30:44 +02:00
0f9c091a14 feat(knowledge): knowledge-refresh — living register, deterministic stale core + web poll (v5.7 Chunk 3)
The 'living' half of the v5.7 living knowledge base. Same hybrid split as the
optimization lens (Chunk 2b): a deterministic, byte-stable, unit-tested core +
a web/judgment command shell.

- scanners/lib/knowledge-refresh.mjs: pure assessFreshness(register,
  {referenceDate, staleAfterDays=90}) — age-based fresh/stale classification of
  source.verified; referenceDate injected (never reads the clock) → fully
  deterministic. 15 tests.
- scanners/knowledge-refresh-cli.mjs: -cli (NOT an orchestrated scanner →
  scanner count stays 15, suite byte-stable). Read-only — never writes the
  register, never hits the network. --reference-date/--stale-after/--dry-run,
  exit 0/1/3. 8 tests.
- commands/knowledge-refresh.md (opus): CLI stale-report → re-verify each stale
  entry by re-reading its source.url → poll CC changelog + Anthropic blog →
  apply ONLY human-approved writes, then re-validate the register. No unverified
  claim is ever auto-written (Verifiseringsplikt). Web-driven → not byte-stable.

No new agent, no new orchestrated scanner. Docs/badges: commands 19→20,
tests 1068→1091 (20 lib + 32 scanner test files). self-audit A/A, readmeCheck passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:22:57 +02:00
7b3b487d26 feat(opt): optimization lens Chunk 2b — opus prose-judgment analyzer + /config-audit optimize
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) <noreply@anthropic.com>
2026-06-21 14:01:18 +02:00
c1409ae9b9 test(tok): hermetic HOME in runScanner — deterministic on populated ~/.claude
token-hotspots.test.mjs ran scan() under the developer's real HOME, so
readActiveConfig leaked ~/.claude (user CLAUDE.md cascade + ambient MCP/
plugins) into every fixture result. This made assertions machine-dependent:
green on a clean HOME, red on a populated one (small-cascade tipped past the
10k-token threshold; sonnet-era gained ambient MCP findings).

Wrap the HOME-dependent scan() call in the shared withHermeticHome helper —
same isolation the OST test and the snapshot/byte-stability suite already use.
Full suite (1055) now green on BOTH real and clean HOME.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:45:53 +02:00
e7833b65fc feat(opt): optimization lens CA-OPT-001 (procedure→skill) — v5.7 Fase 1 Chunk 2a
First detector of the 'is the config OPTIMAL?' axis (vs the existing 'correct?' scanners). New orchestrated scanner family CA-OPT (count 14->15), the deterministic half of the hybrid optimization lens.

CA-OPT-001 (low, Missed opportunity): a multi-step procedure in CLAUDE.md (>=6 consecutive numbered steps) that belongs in a skill. Reads recommendation + provenance from the best-practices register (BP-MECH-003). Conservative by design; the negative corpus proves null false-positives. 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 (OST,OPT), SC-5 regenerated under hermetic HOME (additive OPT entry only). 10 new tests; suite 1045->1055, self-audit A/A, readmeCheck passed (all verified with a clean HOME).

Note: the pre-existing TOK test reads the real ~/.claude (non-hermetic) -> run the suite with a clean HOME for deterministic results. Tracked as a separate follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 23:10:03 +02:00
55f83a3c99 feat(knowledge): best-practices register foundation — v5.7 Fase 1 Chunk 1
Add knowledge/best-practices.json: a machine-readable, provenance-stamped, schema-validated best-practices register — the source of truth for the upcoming v5.7 optimization lens (CA-OPT). 13 seed entries migrated from the v5.5 V-rows (loading-model + compaction facts) and the Anthropic 'Steering Claude Code' blog (mechanism-fit rules); each entry carries source.url + verified date + confidence. Only confirmed claims are user-facing (Verifiseringsplikt).

scanners/lib/best-practices-register.mjs: zero-dependency loader + validator (loadRegister/validateRegister/getEntry, native JSON.parse — not YAML, since the repo is zero-dep and yaml-parser.mjs can't parse arrays-of-objects). tests/lib/best-practices-register.test.mjs: 22 tests (schema, provenance integrity, negative cases, lookup).

Byte-stable: no scanner consumes the register yet (Chunk 2), so all scanner output is unchanged. Suite 1023->1045, self-audit A/A, readmeCheck passed. Full design: docs/v5.7-optimization-lens-plan.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:37:47 +02:00