Commit graph

104 commits

Author SHA1 Message Date
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
e3b044a476 feat(ost): v5.6 C — output-style scanner (CA-OST, count 13→14)
New orchestrated scanner output-style-scanner.mjs — first new family since
SKL. Three findings, each pinned to a CONFIRMED V-row of the steering-model
plan + re-verified against code.claude.com/docs/en/output-styles:

- CA-OST-001 (medium, V10): user/project custom style missing
  keep-coding-instructions:true (default false) → silently strips built-in
  software-engineering instructions when active. Scoped to user/project.
- CA-OST-002 (low, V11): plugin style with force-for-plugin:true overrides the
  user's selected outputStyle. Verifiseringsplikt correction — the plan bullet
  said "project/user style," but force-for-plugin is plugin-styles-only per the
  docs, so the check keys on source==='plugin'.
- CA-OST-003 (medium): settings outputStyle matching no built-in
  (Default/Explanatory/Learning/Proactive, case-insensitive) nor discovered
  custom style → dead config.

Byte-stability — a scanner addition, not a field addition. Growing the
scanners array + scanners_ok cannot be hidden by a field strip, but re-seeding
frozen v5.0.0 (the SKL precedent) would now bake in B2's hotspot triple +
claudeMd drift. So, per the B2 lesson, frozen v5.0.0 snapshots are PRESERVED
and the OST entry is stripped at compare time via new
tests/helpers/strip-added-scanner.mjs (wired into json/raw-backcompat + the
Step 5/6 humanizer tests); only SC-5 default-output is regenerated (additive
OST entry, diff reviewed). OST is fixture-gated (no output styles on
marketplace-medium / hermetic HOME → silent).

Wiring: orchestrator; humanizer (OST→Configuration mistake) + humanizer-data
OST family (title-coupled); scoring (OST→Settings, keeps 10 areas). Suite
1012→1023 (+11). Badges: scanners 14, tests 1023, TRANSLATIONS families 15.
Lore swept: README, CLAUDE.md, scanner-internals, humanizer.md. self-audit A/A.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:02:44 +02:00
778b517e6f feat(tok,acr): v5.6 B2 — load-pattern column in token-hotspots
Annotate every ranked TOK hotspot with the load-pattern triple
(loadPattern/survivesCompaction/derivationConfidence):

- hotspotLoadPattern() maps each discovery `type` → a deriveLoadPattern kind.
  Rules reuse activeConfig.rules for precise `scoped` handling; claude-md maps
  by scope. Two new deriveLoadPattern kinds back the rest: `command`
  (on-demand — body loads on /invoke) and `harness-config` (external —
  settings/keybindings/.mcp.json/hooks.json/plugin.json configure the CLI, not
  the model context, so they cost no per-turn context tokens). Honest split:
  the .mcp.json FILE is external; the MCP server's tool schemas are a separate
  `always` hotspot.

Byte-stability — the opposite of B1's manifest. token-hotspots IS a byte-equal
SC-6/SC-7 CLI, and its hotspots ride inside scan-orchestrator + posture, so the
change touched SIX frozen-v5.0.0 comparisons across five test files. Resolved by
preserving the frozen baselines: a shared tests/helpers/strip-hotspot-load-pattern.mjs
strips the additive triple before each byte-equal compare (proves the original
schema is byte-identical). SC-5 default-output snapshots (scan-orchestrator +
token-hotspots) regenerated — diff reviewed as additive-only.

Tests 1008→1012. Self-audit A/A, scanner count unchanged at 13 (C bumps to 14).
Completes v5.6 B (B1 manifest + B2 token-hotspots).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:21:01 +02:00
bb647ce35f feat(mft): v5.6 B1 — load-pattern accounting in manifest
Consume the v5.6 Foundation enumeration in buildManifest:

- Component-level sources: drop the coarse `kind:'plugin'` roll-up (it
  double-counted skills/rules/agents already enumerated once). Kinds are now
  claude-md/skill/rule/agent/output-style/mcp-server/hook.
- Every source carries loadPattern/survivesCompaction/derivationConfidence.
  Rules/agents/output-styles propagate the foundation-derived values; CLAUDE.md
  maps scope→kind (all cascade files always-loaded); skills are tagged on-demand
  (skill-body) so the body cost does not inflate the always-loaded subtotal.
- New `summary` (always/onDemand/external/unknown {tokens,count}); the
  always-loaded subtotal — "tokens that enter context every turn" — is the headline.

manifest is an environment-aware CLI → SC-6/SC-7 verify it by mode-equivalence,
not byte-equal, and it is not in SC-5. Adding fields in place keeps all snapshots
green with no regen (verified). `total` changes (de-duped) — intended correctness fix.
TOK's load-pattern column (byte-equal SC-6) is deferred to the next chunk (B2).

Tests 996→1008 (deterministic buildManifest unit test + CLI presence checks).
Self-audit A/A, scanner count unchanged at 13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 17:48:55 +02:00
62d910ed6d feat(acr,yaml): v5.6 Foundation — load-pattern enumeration + block-seq parser
Foundation chunk of v5.6 "steering-model II" (internal plumbing for B/C;
no command-output change, so --json/--raw/SC-5/6/7 stay byte-stable, count
stays 13).

active-config-reader.mjs:
- deriveLoadPattern(kind,{scoped}) — pure helper mapping each source kind to
  loadPattern {always,on-demand,external} + survivesCompaction {yes,no,n/a}
  + derivationConfidence {confirmed,inferred}, traced to the published
  loading model (V-rows in docs/v5.5-steering-model-plan.md).
- enumerateRules / enumerateAgents / enumerateOutputStyles — the three
  source kinds previously unenumerated (mirror enumerateSkills). Output-style
  discovery is direct (not a new file-discovery type) to keep the discovery
  surface stable.
- readActiveConfig now exposes rules/agents/outputStyles arrays + totals
  counts/subtotals (folded into grandTotal).

yaml-parser.mjs:
- parseSimpleYaml now reads YAML block sequences (paths:\n  - a), not just
  inline paths:. An empty-valued key with no `- ` items stays null
  (backcompat). Resolves a pre-existing RUL false-positive (a block-seq-scoped
  rule was misread as unscoped) — fix flows through unchanged RUL code.

Tests +35 (961 -> 996): block-seq parser cases, RUL block-seq regression
(no-misflag + durability-fires), deriveLoadPattern table, three enumerators
(positive+negative). Amended two existing ACR asserts (top-level key shape +
grandTotal sum). self-audit A/A, readmeCheck passed, mismatches []. tests
badge 961+->996+; README testing prose de-staled (635/36 -> 996/56);
CLAUDE.md Foundation note.

B (manifest/tokens render + snapshot regen) and C (CA-OST, count->14)
deferred to their own sessions/GO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 16:57:52 +02:00
f3aadb5183 feat(rul,cml): durability findings — config lost after compaction (v5.5.0 / A)
Per the official "what survives compaction" model (context-window.md):
only the project-root CLAUDE.md (+ unscoped rules) is re-injected after a
context compaction. Two additive, structural findings (low severity):

- RUL: a large (>50-line) PATH-SCOPED rule — reloads only on a matching
  file read, and is not re-injected after compaction, so must-hold rules
  can silently drop mid-session.
- CML: a NESTED (subdirectory) CLAUDE.md — not re-injected after
  compaction (only the project root is).

Additive (no new scanner, count stays 13); one humanizer entry each;
hermetic temp-fixture tests (positive + negative). Suite 957 -> 961,
SC-5 byte-stable, self-audit A/A.

Known limitation (pre-existing, broader than A): the lightweight
frontmatter parser reads inline `paths:` but not YAML block sequences,
so block-sequence-scoped rules are still seen as unscoped. Deferred.

Part of v5.5.0 "steering-model I". Foundation (active-config-reader
enumeration) deferred to v5.6 with B.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 11:20:04 +02:00
f75ed5655c feat(plh): flag plugin-agent frontmatter Claude Code ignores (v5.5.0 / E)
Plugin subagents silently ignore `hooks`/`mcpServers`/`permissionMode`
frontmatter — these are honored only for user/project agents in
.claude/agents/ (code.claude.com/docs sub-agents). Setting them in a
plugin agent is dead config; `permissionMode` is MEDIUM because it
implies a restriction Claude Code does not apply (false security).
hooks/mcpServers are LOW.

Additive to PLH's agent-frontmatter loop (no new scanner, count stays
13). One humanizer pattern covers the three field titles. Hermetic
temp-fixture test (positive + negative). Suite 954 -> 957, byte-stable.

Part of v5.5.0 "steering-model I". Foundation (active-config-reader
enumeration) deferred to v5.6 with B — A/E are additive and don't
consume it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 11:11:12 +02:00
5ac6c87053 fix(hkv): remove post-session — it is a runner hook, not a settings event
Verified 2026-06-20 against hooks.md + the 2.1.169 changelog: the
`post-session` hook in that changelog is a SELF-HOSTED-RUNNER
workspace-lifecycle hook (runs after the session, before the workspace
is deleted), NOT a settings.json hook event. It is absent from
hooks.md's 30-event list (all PascalCase), so config-audit was wrongly
treating a bogus `post-session` settings hook as valid — it now flags it.

Restructured the event test suite accordingly and added a negative test.
Resolved U1/U2 in the v5.5 plan (U1 refuted → removed; U2 `outputStyles`
plugin.json key confirmed → PLH unchanged). Suite stays 954, self-audit A/A.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 10:12:52 +02:00
b6a62d7699 fix(hkv,rul): add 3 verified hook events; correct globs-rule wording
HKV: add Setup, UserPromptExpansion, PostToolBatch to VALID_EVENTS,
verified live against code.claude.com/docs/en/hooks.md (2026-06-19). A
valid hook using one of these was wrongly flagged "will never fire" — a
user could delete a working hook. Made the "(N total)" hint dynamic so
it can't drift again. Flagged the unverified kebab 'post-session' in a
comment (an existing test depends on it; follow-up check needed).

RUL: reword the globs finding. Only `paths:` is documented; whether CC
ever read `globs` is unverified, so the old "deprecated/legacy" framing
overclaimed (Verifiseringsplikt). New wording steers to the documented
`paths:` field. Updated the coupled fix-engine title match and the
humanizer entry (which also carried the "field was renamed" overclaim).

Suite 950 -> 954 (badge bumped). self-audit A/A, scanner count 13. No
version bump — these land in the pending v5.4.1 patch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 09:56:00 +02:00
292352eff8 fix(plh): require CLAUDE.md commands/agents/hooks section only for shipped components
plugin-health flagged "CLAUDE.md missing <commands|agents|hooks> section" regardless of
whether the plugin actually had that component — e.g. graceful-handoff (no commands/ or
agents/ dir) got two spurious medium findings. Same over-report class as the model-field fix.

Now gated on component presence (pluginShipsComponent): a section is required only if the
plugin ships that component (commands/ or agents/ with .md, or hooks/hooks.json). Across the 5
stable plugins this drops 12 spurious findings to 3 legitimate ones (graceful-handoff hooks,
ai-psychosis commands+hooks). New fixture plugin-section-coverage proves both directions.
Found via the marketplace-wide review. Suite 950/950, self-audit A/A, scanner count 13.
tests badge 949 -> 950.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 09:06:20 +02:00
a5cfc331fd fix(plh): align required-frontmatter with CC docs (drop optional model/tools/name)
plugin-health-scanner required `model`+`allowed-tools` on commands and `model`+`tools`
on agents, plus `name` on commands. Per primary docs these are OPTIONAL:
- Commands/skills (code.claude.com/docs slash-commands): "All fields are optional. Only
  `description` is recommended." `name` defaults to the directory name.
- Subagents (code.claude.com/docs sub-agents): "Only `name` and `description` are required";
  `model` defaults to `inherit`, `tools` inherits all.

REQUIRED_COMMAND_FRONTMATTER -> [description]; REQUIRED_AGENT_FRONTMATTER -> [name, description].
This was over-reporting: every command without an explicit `model` got a spurious medium
finding (10 on the okr plugin alone). Found via the okr pilot review. Suite 949/949, self-audit
A/A, scanner count 13 (no new scanner).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 07:27:44 +02:00
3633571c7e feat(set): validate autoMode structure + flag it in shared settings (CA-SET)
settings-validator now validates the autoMode block (auto-mode classifier
config). Structure (medium): autoMode must be an object whose only keys are
environment/allow/soft_deny/hard_deny, each a string array ("$defaults" is a
valid entry); flags not-an-object, unknown-subkey, not-string-array. Dead-config
(low): Claude Code does not read autoMode from shared project settings
(.claude/settings.json), so an autoMode block committed there has no effect —
keyed on file.scope === 'project'.

Both premises primary-source-verified (code.claude.com/docs/en/auto-mode-config).
The plan's "test per-file scope first" gate passed: ConfigFile already carries
scope. SET is in the orchestrator; SC-5 re-checked, byte-equal (snapshot fixture
has no autoMode). Fixtures force-added (.claude/ is gitignored).

Tests +5 (944->949). Scanner count unchanged (13). --json/--raw byte-stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-19 22:14:26 +02:00
76d5eda101 feat(plh): validate plugin.json skills:-array entries (CA-PLH-016)
PLH now validates each entry of a plugin.json `skills` field (string|array):
every entry must resolve to an existing directory inside the plugin root.
One finding per bad entry (medium, plugin-hygiene), problem ∈ {non-string,
escapes-root, not-found, not-a-directory}. Mirrors `claude plugin validate`.
String|array normalized so a non-string top-level value is caught too.

Verifiseringsplikt: the plan's "CC suggests the parent directory" error text
is NOT in the primary docs — dropped. Only the four primary-source-verified
conditions are asserted (escape backed by the path-traversal rule).

Tests +3 (941->944). Scanner count unchanged (13). --json/--raw byte-stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-19 21:56:57 +02:00
7abc5a1dcb feat(plh): flag plugin.json paths that shadow default folders (CA-PLH-015)
PLH now flags a plugin.json component-path key (commands/agents/outputStyles)
that replaces a default folder still present on disk — Claude Code stops
scanning that folder, so its contents are silently ignored (dead config).
Mirrors CC's /doctor & `claude plugin list` warning (v2.1.140+).

Field set pinned to the docs' "replaces" category only (Verifiseringsplikt,
code.claude.com/docs path-behavior-rules): skills is excluded (adds to the
default skills/ scan — both load) as are hooks/mcpServers/lspServers (own
merge rules); a custom path that addresses the default folder is not flagged.

Tests +5 (936->941). Scanner count unchanged (13). --json/--raw byte-stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-19 21:46:22 +02:00
0874188fe4 fix(plh): downgrade cross-plugin command-name overlap to low ambiguity
Commands are namespaced (/name:command), so a command name shared by two
differently-named plugins keeps both reachable — it is ambiguity, not a hard
conflict. The check now mirrors COL's plugin-vs-plugin skill finding: severity
LOW (was HIGH), category plugin-hygiene, COL-shaped details.namespaces, and a
group-first shape (one finding per command name listing every namespace, not
pairwise). It keys on the declared namespace (was folder basename) and fires
only across 2+ distinct namespaces — when plugins share a declared name, the
namespace-collision finding (medium) is the right signal, so this stays silent.

Removes the inaccurate "only one wins" humanizer entry. Adds fixtures
(duplicate-command-name; a shared command in duplicate-plugin-name's colliding
namespace) and 4 tests. Suite 932->936. self-audit A 97 / A 100, scanners 13.
2026-06-19 15:30:35 +02:00
c6c5f17752 feat(plh): flag plugin namespace collisions (same declared name)
Two plugins that declare the same `name` in plugin.json collapse into one
component namespace (/name:command, name:skill, agent "name"). Resolution
between two installed same-name plugins is undocumented, so one plugin's
commands/skills/agents are silently shadowed and unreachable. PLH now flags
this at medium severity, keying on the declared `name` (not folder basename,
via new declaredName on scanSinglePlugin) with a COL-shaped details.namespaces
payload. Name-less plugins are excluded from the collision map.

Search-first (code.claude.com/docs/en/plugins): plugin components are
namespaced by the declared name, so a plugin component can never shadow a
user/project one — only a same-name collision loses components. This refutes
the original "plugin vs user vs project shadowing" framing in the backlog.

Adds humanizer pattern, fixture (duplicate-plugin-name: 2 colliding + 2
name-less), and 3 tests. Suite 929->932. self-audit A 97 / A 100, scanners 13.
2026-06-19 15:13:19 +02:00
d678765fad feat(dis): flag forbidden-param permission rules CC silently ignores
Extends the DIS scanner and its shared permission-rules lib with a third
documented Claude Code permission footgun. Verified verbatim against
code.claude.com/docs/en/permissions (fetched 2026-06-19).

CC's Tool(param:value) matching (2.1.178) is off-limits for a tool's own
canonicalizing field — CC ignores such a rule and emits a startup warning,
because e.g. Bash(command:rm *) is bypassable by a compound command. The
forbidden fields: command (Bash/PowerShell), file_path (Read/Edit/Write),
path (Grep/Glob), notebook_path (NotebookEdit), url (WebFetch).

- lib/permission-rules.mjs: new forbiddenParamRule(entry) returning
  { tool, key, hint } or null. Only the param:value form (colon present)
  whose key equals the tool's forbidden field is flagged; Bash(npm:*),
  WebFetch(domain:host), Agent(model:opus), and Bash(command) (no colon)
  are left valid. FORBIDDEN_PARAMS map is the single source of truth.
- DIS: scans allow + deny + ask and splits severity by intent — deny/ask
  hits are false security (medium: the block never applies), allow hits are
  dead config (low: param:value matching is deny/ask-only). Two findings,
  permissions-hygiene, CA-DIS-NNN.
- 11 new tests (7 lib, 4 DIS) + 1 fixture forbidden-param-permissions
  (force-added past .gitignore .claude/). Suite 918 -> 929. Snapshot
  unchanged (SC-5 byte-equal), contamination grep clean, gitleaks clean.
  README/CLAUDE document the broadened DIS mandate; test badge synced.
  self-audit: PASS, configGrade A 97, pluginGrade A 100, scanners 13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-19 14:04:25 +02:00
b0bf8c5817 feat(cml): context-window-scaled CLAUDE.md char budget (mirrors CC 40.0k warning)
Add a char-based CML finding that mirrors Claude Code's own startup warning
("Large CLAUDE.md will impact performance (X chars > 40.0k)"). CC 2.1.169 scales
that threshold with the model's context window, so the finding anchors on the
conservative 200k window (we cannot observe the user's window; the anchor fires
earliest) and discloses the relaxed ~200,000-char figure at 1M context. MEDIUM
severity (token cost, not an adherence cliff — consistent with the v5.2.0 reframe).

Keyed on chars, not lines, so it is complementary to the existing 200/500-line
checks (which stay untouched): a file can be long by lines yet under budget (short
lines, e.g. large-cascade at 37k chars / 1024 lines), or short by lines yet over it.

Extract the shared 200k/1M context-window constants to scanners/lib/context-window.mjs
(single source of truth; skill-listing-budget.mjs now imports + re-exports them).

40.0k figure and context-window scaling verified against the CC changelog (2.1.169,
2026-06-08) and the live startup-warning text. +6 tests, new fixture large-claude-chars
(48,531 chars / 100 lines). Suite 918/918, self-audit PASS configGrade A 97.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-19 13:34:40 +02:00
03949c6c98 feat(dis): flag ineffective allow wildcards; treat Tool(*) as deny-all
Extends the DIS scanner and its shared permission-rules lib with two
documented Claude Code permission footguns. Verified verbatim against
code.claude.com/docs/en/permissions (fetched 2026-06-19).

- lib/permission-rules.mjs: new isIneffectiveAllowGlob(entry) — unanchored
  tool-name globs in permissions.allow (`*`, `B*`, `mcp__*`) that CC silently
  skips ("does not auto-approve anything"); valid only as a glob-free
  `mcp__<server>__*`. Shared with CNF.
- lib/permission-rules.mjs: dominates() now treats the `Tool(*)` deny-all glob
  as equivalent to a bare deny (covers a bare allow) — CC: "Bash(*) is
  equivalent to Bash ... both forms remove the tool from Claude's context".
- DIS: new finding "Ineffective allow wildcard — Claude Code ignores this rule"
  (low, permissions-hygiene, CA-DIS-NNN); the existing dead-allow finding now
  also catches a bare allow killed by a Tool(*) deny.
- 9 new tests (5 lib, 4 DIS) + 2 fixtures (force-added past .gitignore .claude/).
  Suite 903 -> 912. Snapshot unchanged, contamination grep clean. README/CLAUDE/
  scanner-internals document the broadened DIS mandate; test badge synced.
  self-audit: PASS, configGrade A 96, pluginGrade A 100, readme gate passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-19 06:31:18 +02:00
dfe9049b55 feat(feature-gap): recommend disableBundledSkills under skill-listing pressure
Chunk 2 of the disableBundledSkills GAP feature. Adds a conditional GAP check
that prescribes the `disableBundledSkills` lever — but only when the active
skill listing is measurably over budget (SKL's CA-SKL-002 overflow signal) and
the lever is un-pulled. It stays an opportunity, not noise.

Bundled skills (/code-review, /batch, /debug, /loop, /claude-api, …) live in the
CC binary, not on disk, so their exact cost is unmeasurable here — the finding
says so plainly, and frames the lever as zero-cost budget reclaim that leaves
the user's own skills untouched. CC 2.1.169+.

- Pure, exported bundledSkillsLeverFinding({leverPulled, aggregate}) → finding|null
  (severity low, category token-efficiency, CA-GAP-NNN), wired into scan() via the
  shared measureActiveSkillListing().
- Lever resolved via new isBundledSkillsDisabled(): env var + settings cascade
  read directly, because discovery does NOT tag ~/.claude/settings.json (its
  relPath lacks ".claude" when walked from the .claude root) — the dominant
  user-scope location for this global preference would otherwise be missed.
- GAP scan() now reads HOME → existing GAP tests retrofitted to withHermeticHome
  per the hermetic rule. Snapshots unchanged, contamination grep clean.
- 16 new tests (9 GAP, 7 lib). Suite 887 -> 903. README/CLAUDE.md document the
  cross-scanner remediation; test counts synced. self-audit: PASS, configGrade
  A 96, pluginGrade A 100, readme gate passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-18 21:38:19 +02:00
0a631e3061 refactor(skl): extract skill-listing budget to shared lib (single source of truth)
Chunk 1 of the disableBundledSkills GAP feature. Moves the per-description cap,
aggregate budget constants, calibration note, and the enumerate-and-measure step
out of skill-listing-scanner into scanners/lib/skill-listing-budget.mjs — so SKL
(diagnoses overflow) and the upcoming GAP check (prescribes disableBundledSkills)
consume one budget definition instead of two divergent copies.

- New lib: assessSkillListingBudget (pure aggregate math) + measureActiveSkillListing
  (HOME-scoped enumerate-and-measure wrapper).
- SKL delegates measurement; all finding strings kept byte-identical. 18/18 SKL
  tests pass unchanged → behavior-neutral refactor.
- 12 new lib unit tests pin the budget contract. Suite 875 -> 887.
- README badge + CLAUDE.md test counts synced (self-audit --check-readme: passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-18 21:23:01 +02:00
5b28e84966 test(fix-cli): close missed HOME-leak — isolate spawns + lock SKL/COL out
Devil's advocate gap-verification (read-only Workflow, 9 skeptics) refuted the
blanket "all closed" claim by finding fix-cli.test.mjs was the one CLI-spawning
test still reading the real ~/.claude. fix-cli runs the SKL skill-listing
scanner (HOME-scoped) even with includeGlobal:false, so its manual findings
include CA-SKL-001 on a dev machine but not in clean CI.

This directly corrects 325182d, which listed fix-cli.test.mjs as "Proven safe,
left as-is (output byte-identical real vs empty HOME — fixable findings are
project-local HKV/RUL/SET, never SKL/COL)". That reasoning predated SKL being
wired into scan-orchestrator (7bb2547/66433fe) and was false: real HOME yields
manual=6 (incl. CA-SKL-001), hermetic manual=5 (5230 vs 4798 bytes).

- wrap all 5 fix-cli spawns in hermeticEnv() (matches the other 11 CLI tests)
- add a regression lock: a project-scoped run must surface no CA-SKL/CA-COL
- redirect the --apply backup check to HERMETIC_HOME — the test was also
  writing backups into the real ~/.config-audit/backups on every run
- docs: stale "26 hook events" -> 28 (README:528, scanner-internals:73);
  hook-validator.mjs comment April -> June 2026 (functional count already 28)

Re-audited all 12 CLI-spawning test files: 11 hermetic-helper, manifest custom
HOME-env, post-edit-verify safe-by-construction (early-exit only, never reaches
scanners). HOME-leak class now actually closed. Suite 875/875, no snapshot drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-18 18:50:20 +02:00
325182ddc9 test: isolate HOME in all CLI-spawning tests (close leak class)
Follow-up to the posture-grade-stability fix in 66433fe. Audited every
test that spawns a CLI and found more of the same class: tests running
HOME-scoped scanners (SKL/COL) or the CLAUDE.md cascade against the
developer's real ~/.claude instead of an isolated HOME.

Fixed (env: hermeticEnv()):
- posture.test.mjs        — runs full posture (SKL/COL/cascade); twin of
                            the posture-grade-stability leak, masked only
                            because its asserts are structural/relative
- drift-cli.test.mjs      — ACTIVE bug: the CLI wrote baselines into the
                            real ~/.claude during the run (pollution); now
                            isolated, and afterEach cleanup wrapped in
                            withHermeticHome so it looks in the same HOME
- token-hotspots-cli.test.mjs — scan-orchestrator run executes SKL/COL on
                            real HOME; TOK reads the HOME cascade
- accurate-tokens.test.mjs — TOK reads the HOME cascade (kept the
                            ANTHROPIC_API_KEY deletion)

Proven safe, left as-is (no HOME-scoped scan affecting assertions, no
HOME writes): post-edit-verify.test.mjs (fast-path early-returns only),
fix-cli.test.mjs (output byte-identical real vs empty HOME — fixable
findings are project-local HKV/RUL/SET, never SKL/COL),
lint-default-output (caller already uses withHermeticHome).

Suite 875/875, no snapshot drift. No test regressed under isolation,
confirming none had a hidden real-HOME dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-18 18:17:09 +02:00
66433fee48 feat(skill-listing): add CA-SKL-002 aggregate listing-budget check
Syklus 2 of Fase 4 Items 2+3. Flags when the sum of active skill
descriptions exceeds the listing budget (~2% of context, CC 2.1.32).

Design (operator-confirmed "fact-first, 200k anchor"):
- low severity (estimate) vs medium for the verified 1,536-char cap
- each description counted up to the 1,536 cap (what actually loads in
  the listing) — avoids double-counting the tail CA-SKL-001 flags
- fires when sum > 2% x 200k = 4000 tok; evidence leads with the measured
  sum + a calibration note that the budget scales 5x on 1M-context models
- aggregate emitted after the per-skill loop so the common case reads
  001=cap, 002=aggregate (finding IDs are a sequential counter, not stable
  semantic IDs — tests match on title, never NNN)

Also:
- tailored humanizer static entry for the aggregate title
- fix latent HOME leak in posture-grade-stability.test.mjs: it spawned
  posture.mjs without hermeticEnv(), so a real ~/.claude leaked HOME-scoped
  SKL/COL findings into the baseline grade (Token Efficiency A->B). Now
  isolated like the 8 other CLI-spawning tests.
- docs sync: test count 868->875, scanner-internals, gap-matrix, plan status

Suite 875/875, no snapshot drift, self-audit clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-18 18:06:17 +02:00
7bb254780a feat(skill-listing): add SKL scanner for the skill-listing token budget
Fase 4 Items 2+3 (CC 2.1.114→181 gap-review). New orchestrated scanner
`skill-listing-scanner.mjs` (prefix SKL) flags every active skill whose
description exceeds the verified 1,536-char listing cap (CC 2.1.105, changelog
L1502). Past the cap, Claude Code silently truncates the description the model
reads to route skill invocation — dropping the trigger phrases at the tail.
HOME-scoped over all user + plugin skills via enumerateSkills (COL is the model).

- CA-SKL-001 (medium): description > 1,536 chars. Remediation folds in Item
  2(b) — recommends disableBundledSkills + skillOverrides + trimming
  (designvalg A: no standalone GAP-check, which would fire for nearly everyone).
- Designvalg B: v1 ships the verified cap ONLY. The aggregate 2%-of-context
  listing budget is deferred — it needs a context-window assumption that would
  turn a verified fact into a guess (would carry a CALIBRATION_NOTE if added).
- Choice C: recognize the skillOverrides settings key (CC 2.1.129) in
  KNOWN_KEYS. Left OUT of TYPE_CHECKS — the value is a per-skill object
  (off/user-invocable-only/name-only), not a string; a 'string' check (as the
  plan sketched) would create a NEW false positive. Verify-first deviation.

Registration: scan-orchestrator (13th scanner), humanizer (SKL → 'Wasted
tokens' + static/_default translations), scoring SCANNER_AREA_MAP (→ Token
Efficiency; no 11th area), README badge 12→13, CLAUDE.md (finding-id +
test-count), docs/scanner-internals.md, gap-matrix + plan status notes.

Snapshots reseeded hermetically (SEED_SNAPSHOT/UPDATE_SNAPSHOT): SKL entry with
0 findings in empty HOME, scanners_ok 11→12, claudeMdEstimatedTokens bump from
the CLAUDE.md edits flowing through the cascade. Contamination grep clean.

Suite 868/868 (856 baseline + 11 SKL + 1 skillOverrides). RED→GREEN logged
per cycle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-18 17:36:28 +02:00
8376dab83f fix(tokens): refresh stale "Opus 4.7" framing to model-neutral + Opus 4.8 anchor
Fase 4 token-opt, Item 1 of gap-review NEXT STEP #2. The prompt-cache pattern corpus + TOK scanner were frozen at an "Opus 4.7" framing after CC shipped Opus 4.8 (default, 2.1.154) and Fable 5 (2.1.170). Model-era facts re-verified against the official changelog cache before editing.

The patterns are properties of prompt-caching, not of any model, so mechanic text is now model-neutral with a single "current default: Opus 4.8" anchor — preventing a re-freeze at the next model bump.

- rename knowledge/opus-4.7-patterns.md -> prompt-cache-patterns.md (git mv, history preserved); 6 reference sites updated
- TOK scanner: line-318 finding text (human-facing) made model-neutral; header + cache-prefix-scanner + CLI comments refreshed
- configuration-best-practices.md body + footnote 4.7 -> 4.8
- human-facing docs: commands/{tokens,help,manifest}.md, project CLAUDE.md, README, docs/scanner-internals.md
- gap-matrix row marked DONE; future Items 2/3 retargeted to new filename

Failing-test-first (Iron Law): +2 knowledge staleness guards (era-anchor + no-refreeze) +1 scanner assertion (no stale model anchor in finding text). Suite 853 -> 856 green; zero snapshot drift; self-audit A(97) PASS. CHANGELOG / v5 plan / ratified gap-plan keep historical opus-4.7-patterns refs (correct record of past state).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-18 15:27:36 +02:00
b3c572ad46 fix(mcp-config-validator): remove invented trust field (verify-first)
`.mcp.json` has no per-server `trust` key — verified 2026-06-18 against
code.claude.com/docs/en/mcp + /settings. MCP server approval is
dialog/settings-based (enableAllProjectMcpServers / enabledMcpjsonServers /
disabledMcpjsonServers), never a JSON field. The scanner's "Missing trust
level" (CA-MCP-001, medium) and "Invalid trust level" (high) were false
positives flagging a field that does not exist.

- scanner: delete both trust checks + VALID_TRUST_LEVELS; drop `trust` from
  VALID_SERVER_FIELDS so a stray `trust` is now flagged as an unknown field
- humanizer: remove the two trust-level entries
- knowledge (5 files): point to the real approval mechanism, not a trust field
- fixtures: scrub `trust` (incl. the invalid "local" in optimal-setup)
- tests: flip assertions (no trust-level finding; stray trust -> unknown
  field) + add knowledge-staleness re-freeze guards
- snapshots: reseed (marketplace-medium .mcp.json -8 tokens, hermetic)
- gap-matrix: mark the trust verify-first item DONE

Suite: 853/853 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-18 14:22:56 +02:00
624f5edabc docs(knowledge): refresh corpus to Opus 4.8 era (CC 2.1.114->181 Batch 3)
The knowledge base was frozen at ~v2.1.111, describing an Opus-4.7 world
after CC shipped Opus 4.8 and Fable 5. Refreshed three agent-facing
knowledge files; every fact re-verified against the official changelog
(~/.claude/cache/changelog.md, CC 2.1.181) on 2026-06-18.

- feature-evolution.md: new Opus-4.8-era rows above the v2.1.111 freeze --
  Opus 4.8 default + /effort xhigh (2.1.154), Fable 5 Mythos-class
  (2.1.170), post-session hook (2.1.169), MessageDisplay (2.1.152),
  /simplify -> /code-review (2.1.147), /config key=value (2.1.181).
- hook-events-reference.md: 26 -> 28 events (+MessageDisplay, +post-session);
  documented Stop/SubagentStop additionalContext output field.
- claude-code-capabilities.md: 2026-06 model/effort lineup table;
  bundled skills /simplify -> /code-review; documented /config key=value.
- cc-2.1.x-changelog-delta.md: marked SUPERSEDED by the gap-matrix.

Verification caught two version errors in STATE/matrix, corrected to the
changelog:
- Stop/SubagentStop additionalContext is 2.1.163, not 2.1.165 (2.1.165 was
  "Bug fixes" only; matrix row 109 already said 2.1.163).
- settings `agent` field introduced 2.0.59; 2.1.157 = honored for
  dispatched `claude agents` sessions, not the introduction.

New tests/knowledge/knowledge-staleness.test.mjs (8 tests) encodes the
verified facts as a re-freeze guard (RED before edits, GREEN after).

Full suite: 850/850 green (+8). self-audit PASS, A(100)/A(97).
2026-06-18 13:35:00 +02:00
feaa7ed2e4 fix(claude-md-linter): reframe CLAUDE.md length from HIGH adherence cliff to MEDIUM token cost
The >500-line check emitted HIGH severity with "Files over 500 lines
significantly reduce Claude's adherence to instructions." CC 2.1.169
scaled the "too long" warning by context window, and the plugin's own
configuration-best-practices.md:97 footnote already says raw line count
is a Sonnet-era heuristic superseded by cache-prefix stability — so the
absolute HIGH + universal adherence claim is now-wrong.

- >500 lines: HIGH -> MEDIUM, reworded to token-cost-every-turn +
  smaller-context-model caveat + cache-prefix pointer; notes CC 2.1.169
  scales the threshold by context window.
- >200 lines: stays MEDIUM, dropped the absolute "optimal adherence"
  framing for the same token/context-window framing.

Aligns the scanner with anti-patterns.md:7 (CA-CML-001 = medium) and
configuration-best-practices.md:97. No snapshot impact (byte snapshots
use a 24-line fixture CLAUDE.md).

Full suite: 842/842 green (+5). self-audit PASS, A(100)/A(97).
2026-06-18 13:15:38 +02:00
bec3f45329 fix(permissions): param-aware DIS dead-allow + CNF conflict matching
The DIS scanner collapsed Tool(param) rules to the bare tool name, so
Agent(model:opus) deny + Agent(model:sonnet) allow (and the same for
WebFetch(domain:...)) were flagged as dead config — a false positive now
that CC 2.1.178 matches Tool(param:value) and 2.1.172 adds domain rules.
The conflict-detector shared the blind spot from the other side: a
wildcard deny like WebFetch(domain:*) did not cover a
WebFetch(domain:good.com) allow, so a genuine cross-scope conflict was
missed (false negative).

New shared scanners/lib/permission-rules.mjs:
- parseRule / paramMatches (glob)
- dominates(deny, allow) -> DIS dead-allow (deny fully covers allow)
- rulesIntersect(a, b)   -> CNF cross-scope conflict (match sets intersect)

DIS now delegates to dominates; conflict-detector :156 delegates to
rulesIntersect. A bare deny still covers all params, so true positives
are preserved (Bash deny + Bash(npm:*) allow still flagged).

Re-seeded the marketplace-medium snapshots: the false-positive CA-DIS
finding (Read(src/**) allow + Read(./.env) deny) is correctly gone. This
changes snapshot CONTENT only — envelope schema is unchanged, so --json
and --raw stay byte-stable.

Full suite: 837/837 green (+25). self-audit PASS, A(100)/A(97).
2026-06-18 13:06:20 +02:00