• v5.13.0 c60f849d2e

    v5.13.0 Stable

    ktg released this 2026-07-31 15:34:09 +00:00 | 40 commits to main since this release

    Summary

    "Pipeline hardening" — the batch release of everything found by dogfooding the plugin against the
    maintainer's real machine and by walking the analyze → plan → implement → rollback pipeline
    end-to-end on a throwaway repo copy: one new lens mode (optimize --subtract) and 14 bugs
    (M-BUG-11M-BUG-20, M-BUG-22M-BUG-25), every one of them a real defect a user could hit.

    The minor bump is carried by --subtract alone; the other 14 are fixes. Two themes run through them:
    agent-facing commands were scanning config the user cannot act on (plugin-bundled and vendored
    copies masking real findings), and new finding types kept shipping without their matching humanizer
    entry
    , so plain-language output contradicted the finding's own evidence. The rollback chunk found the
    worst class in the repo: a restoreBackup that returned {restored: [], failed: []} — a success-shaped
    no-op
    — because nothing agreed on where a backup lives or what its manifest looks like.

    No count change (scanners 16, agents 7, commands 21, hooks 4). Frozen v5.0.0 snapshots
    untouched throughout; the SC-5 default-output snapshot was regenerated once, for two humanized titles
    only (M-BUG-15). 1398 tests (+54).

    Known and deliberately not fixed in this release: rollback still cannot delete files that
    implement created — a backup cannot hold a file that never existed. It no longer fails silently
    (manifests carry a created: list, restoreBackup returns createdNotRemoved, and rollback.md
    requires the report), but automatic deletion of user files is destructive and gets its own design.
    drift-cli.mjs still lacks --output-file (M-BUG-21).

    Added

    • optimize --subtract — the subtraction axis (BP-SUB-001). Every command so far asked an
      addition question: what to add, what to move, what it costs. Nothing asked what no longer earns its
      always-loaded rent. --subtract adds that as a fourth lensCheck on the existing hybrid motor — a
      mode, not a 22nd command or a 17th scanner, because the measured payoff (~18% of one file) justifies
      a mode and no more. It is opt-in and proposes only.
      It is also 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 wrongly deleted one costs a broken script or a wrong remote — so the
      floor is decided in code (scanners/lib/floor-exclusion.mjs) before the opus judge sees anything,
      never in prose. That ordering is an invariant, not an implementation detail.
      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.
      Verified against a hand-built ground truth written 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
      gate is re-runnable via scripts/dogfood-subtraction-gate.local.mjs. Three bugs the dogfood run
      exposed are now covered by fixtures: JS \b is ASCII-only so /\bunngå\b/ never matched (every
      Norwegian keyword ending in æ/ø/å was silently dead); a bare word/word is not a path
      ("pros/cons" vetoed the largest deletable block); "mid-sentence" must key on a preceding lowercase
      letter, or **bold labels:** read as entities and cost 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.

    Fixed

    • rollback — the backup path contract the engine and the commands disagreed on
      (M-BUG-22/M-BUG-23/M-BUG-24/M-BUG-25).
      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 sitting right there. Four defects, one root — nothing agreed on where a backup lives or what
      its manifest looks like.
      M-BUG-22 (high): 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 — so listBackups() returned
      9 phantom backups from the test suite and 0 of the 4 real ones. The canonical root is now
      ~/.claude/config-audit/backups, with the legacy root kept readable (legacy: true) so older
      backups stay listable and restorable.
      M-BUG-25 (high, the worst failure mode in the file): 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: []} — success-shaped, and silent. 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 — "check
      for active sessions" had never fired once. It fires now.
      M-BUG-24: the suite called createBackup() against the developer's real home, leaving nine stray
      backups there while cleanupOldBackups() deletes past ten. The root is overridable via
      CONFIG_AUDIT_BACKUP_ROOT / CONFIG_AUDIT_LEGACY_BACKUP_ROOT, and both test files use it — any new
      test touching createBackup() must too.
      Verified against backup 20260717_032636 on a throwaway copy, through the previously broken engine
      path: 3/3 files restored byte-exact (sha256 match), zero writes outside the copy, backup dir
      unmodified.
    • rules-validatorglobToRegex corrupted 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, so 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. Fixture outcomes byte-identical.
    • analyze persists the 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, so
      analysis-report.md never landed on disk and the plan/interview/status phases found nothing to read.
      New orchestrator-writes contract: the agent returns the complete report as its final message and the
      analyze command saves it verbatim before presenting the summary. The harness note is file-type
      specific
      plan was dogfooded afterwards and writes action-plan.md without friction, so the same
      fix is not needed there.
    • implement pins >> append discipline on the shared 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. Both contracts now pin the mechanism: append with a Bash >> heredoc, never the Write/Edit
      tool on a shared log.
    • optimize lens scopes out plugin-bundled CLAUDE.md + keys candidates by absolute path
      (M-BUG-11).
      The lens CLI fed its precision-gate agent every CLAUDE.md discovery returned, including
      the 256 files under ~/.claude/plugins/ — vendored copies across every cached version plus their
      fixtures and examples. optimize --global produced 454 candidates across 92 "files", ~250 of them from
      plugin-internal files a user cannot act on (the plugin overwrites them on update). Second defect:
      candidates were keyed by relPath || absPath, and relPath collides across scopes — a repo-root
      CLAUDE.md and ~/.claude/CLAUDE.md both key to CLAUDE.md, so the two files that actually matter
      merged into one indistinguishable bucket and the agent's Read(file) would resolve the wrong one.
      Dogfood: candidates 454→45, distinct files 92→11, repo vs user-global now distinct.
    • feature-gap scopes presence checks to authored config and reads the 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 was invisible to the settings-key checks (the includeGlobal gotcha plus the
      maxFiles cap), which would have flipped statusLine/autoMode into false positives the moment the
      maskers were removed. Both halves are fixed together: isAuthoredConfig excludes plugin-bundled and
      nested examples//tests/fixtures/ config, and readSettingsCascade reads user→project→local
      directly. Empty target: ~0 (masked) → 18 humanized opportunities.
    • posture --output-file humanizes findings in default mode (M-BUG-12). feature-gap.md and
      posture.md both read findings from posture.mjs --output-file and group on the humanizer fields,
      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 and 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. The payload is now humanized in default mode (applied to
      result.scannerEnvelope, which is where posture nests it); --json/--raw stay raw.
    • AGT findings humanize to "Wasted tokens", not "Other" (M-BUG-17). The agent-listing scanner emits
      an always-loaded per-turn token cost — "the dominant single always-loaded source" — but
      SCANNER_TO_CATEGORY had no AGT entry, so its findings fell through to the Other fallback, a bucket
      that isn't even in the analyzer-agent's category list. All 16 orchestrator scanner prefixes are now
      covered by the category map, closing the class.
    • On-demand copy for the oversized skill-body finding (M-BUG-16). The v5.11 B7 finding measures a
      skill body, which loads only when the skill is invoked — but with no SKL.static entry it fell
      through to SKL._default ("using more of the listing budget than it should"), so the humanized title
      claimed a listing-budget cost and directly contradicted its own humanized evidence ("loads on demand
      only … NOT every turn").
    • Honest absence-state copy for two GAP enhancement findings (M-BUG-15). The "No path-scoped rules"
      and "No subagent isolation" checks fire on an empty collection too, but the humanized titles
      presupposed the feature exists — "Your subagents share Claude's main work folder" appeared in the same
      report as "You haven't set up any specialized helper agents yet". A user cannot simultaneously have no
      subagents and have subagents that lack isolation. Both titles now use the house "You haven't set up X
      yet" framing, which is honest for the zero-state and the has-but-unconfigured state. Fixed in the
      humanizer, not by gating the scanner — a presence gate would have moved the frozen v5.0.0
      marketplace-medium baseline.
    • Size-neutral copy for "CLAUDE.md not modular" (M-BUG-14). The check is a pure presence check with
      no length gate, but the copy claimed the file is "one big block" and that splitting makes it "easier on
      the loading time" — an unconditional size overclaim that simply lies for a ~625-token CLAUDE.md. Copy
      softened to the honest structural framing; no length gate added, which would have made it the only
      size-gated check among its siblings.
    Downloads