Skip to content
Merged
2 changes: 1 addition & 1 deletion plugins/claude-ops/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "claude-ops",
"version": "0.77.3",
"version": "0.77.4",
"description": "Claude Code operations toolkit. Thirteen skills: audit-skill-visibility (audit whether each installed skill is actually VISIBLE to the model, and diagnose why most of a fleet never gets used: a skill is invisible when its description is dropped by Claude Code's skill-listing context budget, which sheds descriptions lowest-score-first so an unused skill loses the keywords that would let it be matched, from skills genuinely not wanted, from skills the run cannot observe at all; computes whether the listing overflows from documented settings, and withholds every cold verdict the data cannot support rather than reporting absence of data as absence of use), inventory (read-only enumeration of the complete invocable surface: every built-in CLI command with aliases and hidden/gated status, every bundled skill, every built-in subagent and tool, and every component of every installed plugin across all marketplaces; reads the shipped binary because upstream publishes no built-in command list, and carries an integrity verdict so a drifted build reports counts as floors rather than silently short totals), audit-install-state (read-only audit of the machine-scope ~/.claude installation directory and ~/.claude.json: full inventory split into an authored surface and rolled-up bulk trees, product-managed retention vs genuinely unmanaged state, filename-scheme resolution before any process-liveness check, and deliberate/mid-experiment detection; reports, never deletes), audit-performance (read-only slowness-diagnostic capture run at the moment the machine or a session feels slow: CLI version, retention-sweep health including the unparsable-settings pause, which warns in /status, a timed census walk of the install tree as a sweep-cost proxy, active-session and plugin-fleet counts, a process census, and the fan-out layer, which covers a load-labeled no-op spawn baseline, every hook that will fire bucketed per-tool-call versus per-turn with its invocation shape, the configured statusline, subagent concurrency and spawn-depth ceilings against documented defaults, whether running sessions predate the settings file they are judged by, and orphan attribution by parent liveness rather than age, plus on Windows a kernel-object census (Token objects against uptime, paged pool) that names a host-level leak beneath all four suspects; read against a bundled known-performance-issues reference that also records the causes tested and cleared; separates the four documented suspects of accumulated state, version regression, component bloat, and per-spawn fan-out cost, and routes remediation out; reports, never mutates, and never executes a discovered hook or statusline command), audit-native-overlap (map native Claude Code surfaces, namely built-in CLI commands, bundled skills, plugin-backed built-ins, and session-provided skills, against the current repo's plugin skills and agents, so a custom component never silently duplicates what Claude Code itself ships; bare invocation is a read-only overlap report carrying the extraction's integrity floors and a shared-listing-budget exposure section, verdicts are human-gated in a committed store rendered into a generated registry whose every row carries an observable recheck trigger, and only an explicit apply step bakes presence-gated native references into descriptions and Boundary sections), observability (read locally captured telemetry from the OTEL store, the collector, the per-session hook event log and hook-event JSONL, and ccusage, with trend reports, a per-session report of what fired, what was blocked and the event timeline, and store pruning), known-issues (search known Claude product GitHub bugs, check service health, maintain a persistent tracked-issue registry), changelog (ingest Claude Code changelog entries and turn them into decisions: apply executes those in scope one PR per owner plugin and hands larger ones off as work items, then re-extract the native surface and file its drift as work items), prerequisites (read-only table of external binaries declared by enabled plugins; never installs), plugins (bring a machine's plugin fleet current on demand: marketplace refresh, effective-scope updates including in-repo project/local installs, new-plugin install per policy, scope-divergence detection and explicit convergence), morning-brief (read-only gh-based operator morning view: queue-label counts, merge-ready PRs, parked decisions with their RECOMMENDED lines, and loop-lane telemetry freshness), lanes (start/restart/stop/status loop lanes as named background Claude Code sessions seeded from canonical prompt files, with per-lane model/effort, a repo-pull + marketplace-refresh launch step, and a consume-restarts action, an OS-schedulable reader that relaunches stopped lanes whose telemetry carries a restart_request), and a re-runnable setup action that settles where the known-issues registry, the skill-usage log and the hook log root live, places the root's self-ignoring guard, and detects retired conventions. Plus an opt-in, default-off per-session hook event log (one JSON line per hook event on every event the generated registry marks observable, written to <root>/sessions/<session_id>.jsonl, with SessionEnd retention by session count or age and an optional detached pre-prune command), a family of eight advisory *-audit hooks (API errors, config changes, instruction loads, permission denials, pre-compaction, skill usage, tool failures, and unsurfaced hook failures. The last also warns the user via systemMessage, since a hook that fails to launch enforces nothing and Claude Code surfaces the failure to nobody) that emit the shared hook-telemetry envelope, and a reference sink that routes envelopes under the same root: per session when the envelope carries a session id, else into the shared hook-events.jsonl the observability skill reads.",
"author": {
"name": "Melodic Software",
Expand Down
6 changes: 6 additions & 0 deletions plugins/claude-ops/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
All notable changes to the `claude-ops` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.77.4] - 2026-09-30

### Changed

- **`audit-install-state` reads unreferenced plugin-cache versions through a shared `lib/plugin_cache_versions.py`.** The module is byte-identical with the copy in `disk-hygiene`, and `scripts/check-cross-plugin-source-drift.sh` fails if the copies diverge. The report is unchanged.

## [0.77.3] - 2026-09-30

### Changed
Expand Down
110 changes: 110 additions & 0 deletions plugins/claude-ops/lib/plugin_cache_versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Which plugin cache version directories does no installPath reference?

Claude Code records every installed plugin in ``plugins/installed_plugins.json``
and keeps each version under ``plugins/cache/<marketplace>/<plugin>/<version>``.
A version no ``installPath`` names is orphaned: Claude Code stamps it with a
``.orphaned_at`` marker (epoch milliseconds) and removes it after a window.

This module reads the registry and the markers and answers that question. It is
pure and read-only: every read goes through a caller-supplied ``read`` function
taking a path relative to the Claude directory, so each caller keeps its own
read guard. Its callers shape the answer into their own rows.

Python 3.11+, standard library only. This file is carried byte-identical by
every plugin that uses it (scripts/cross-plugin-source-registry.txt).
"""

from __future__ import annotations

import datetime as dt
import json
from collections.abc import Callable
from pathlib import Path
from typing import Any, NamedTuple

INSTALLED_PLUGINS = "plugins/installed_plugins.json"

# Days after an update or uninstall that Claude Code removes an orphaned plugin
# version, counted from its `.orphaned_at` marker. Basis:
# https://code.claude.com/docs/en/plugins/loading.md ("Cleanup of previous versions"),
# verified 2026-09-30; recheck when that section or a Claude Code changelog entry
# changes the window.
ORPHAN_SWEEP_DAYS = 14

_DAY = 86400.0

Reader = Callable[[str], str]


class Registry(NamedTuple):
"""What the registry says about this cache.

``doubt`` is empty when the registry can vouch for the cache. Otherwise it
says why not, and no version may be called unreferenced: a missing registry
is not evidence that every directory is orphaned. ``foreign`` marks the case
where the registry parsed but none of its paths lies under this cache.
"""

referenced: frozenset[Path]
installs: bool
doubt: str
foreign: bool


def install_paths(data: object) -> list[str]:
"""Every `installPath` in the parsed registry, whatever scope or project entry holds it."""
if isinstance(data, dict):
own = data.get("installPath")
found = [own] if isinstance(own, str) else []
return found + [p for v in data.values() for p in install_paths(v)]
if isinstance(data, list):
return [p for v in data for p in install_paths(v)]
return []


def resolve(path: str | Path) -> Path | None:
try:
return Path(path).expanduser().resolve()
except (OSError, RuntimeError):
return None


def load_registry(read: Reader, root: Path, label: str = INSTALLED_PLUGINS) -> Registry:
"""Read the registry under ``root`` and resolve the paths it references.

``label`` names the registry in ``doubt``.
"""
cache = root / "plugins" / "cache"
try:
data: Any = json.loads(read(INSTALLED_PLUGINS))
except (OSError, ValueError) as exc:
return Registry(
frozenset(), False, f"{label} unreadable ({type(exc).__name__})", False
)
if not isinstance(data, dict) or not isinstance(data.get("plugins"), dict):
return Registry(frozenset(), False, f"{label} has no `plugins` object", False)
referenced = frozenset(
p for p in map(resolve, install_paths(data)) if p is not None
)
installs = bool(data["plugins"])
cache_resolved = resolve(cache)
if installs and not any(cache_resolved in p.parents for p in referenced):
doubt = f"no installPath in {label} lies under {cache}"
return Registry(referenced, installs, doubt, True)
return Registry(referenced, installs, "", False)


def orphan_marker(read: Reader, version_rel: str, now: float) -> dict[str, Any] | None:
"""The age of a version's ``.orphaned_at`` marker, or None when it is missing or unparsable."""
try:
epoch = int(read(f"{version_rel}/.orphaned_at").strip()) / 1000
age = (now - epoch) / _DAY
return {
"orphaned_at": dt.datetime.fromtimestamp(epoch, dt.timezone.utc).isoformat(
timespec="seconds"
),
"marker_age_days": round(age, 1),
"past_sweep_window": age >= ORPHAN_SWEEP_DAYS,
}
except (OSError, ValueError, OverflowError):
return None
3 changes: 2 additions & 1 deletion plugins/claude-ops/skills/audit-install-state/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ section of <https://code.claude.com/docs/en/plugins/loading> ("removes that dire
background cleanup 14 days later, so a session that already loaded the old version keeps running").
Verified 2026-09-29 against Claude Code 2.1.285 and that page as fetched that day. Recheck when the
page changes the window, the marker name, or the sweep condition, or a release note names plugin
cache cleanup; then update `ORPHAN_SWEEP_DAYS` in `scripts/install_state.py`.
cache cleanup; then update `ORPHAN_SWEEP_DAYS` in `lib/plugin_cache_versions.py` and its byte-identical copy in
`disk-hygiene`.

## Phase 4. Numeric names and liveness

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@
from pathlib import Path, PurePosixPath
from typing import Callable

_LIB_DIR = Path(__file__).resolve().parents[3] / "lib"
if str(_LIB_DIR) not in sys.path:
sys.path.insert(0, str(_LIB_DIR))

from plugin_cache_versions import ( # noqa: E402 (path set above; plugin-bundled module)
INSTALLED_PLUGINS,
ORPHAN_SWEEP_DAYS,
Comment thread
kyle-sexton marked this conversation as resolved.
load_registry,
orphan_marker,
resolve,
)

MIN_PYTHON = (3, 11)

# --------------------------------------------------------------------------
Expand Down Expand Up @@ -166,9 +178,6 @@ def read_text_guarded(root: Path, relpath: str, limit: int = 2_000_000) -> str:
# them"), read as raw markdown, verified 2026-08-11. See reference/surfaces.md.
# --------------------------------------------------------------------------

# Days after update or uninstall that Claude Code removes an orphaned plugin version directory.
ORPHAN_SWEEP_DAYS = 14

SWEPT = "product-managed-swept" # deleted at startup once older than cleanupPeriodDays
KEPT = "product-managed-kept" # documented as never age-swept
SESSION_SCOPED = (
Expand Down Expand Up @@ -1630,27 +1639,6 @@ def node_modules_bucket(rows: list[FileRow]) -> dict:
}


INSTALLED_PLUGINS = "plugins/installed_plugins.json"


def _install_paths(data: object) -> list[str]:
"""Every `installPath` in the parsed registry, whatever scope or project entry holds it."""
if isinstance(data, dict):
own = data.get("installPath")
found = [own] if isinstance(own, str) else []
return found + [p for v in data.values() for p in _install_paths(v)]
if isinstance(data, list):
return [p for v in data for p in _install_paths(v)]
return []


def _resolved(path: str | Path) -> Path | None:
try:
return Path(path).expanduser().resolve()
except (OSError, RuntimeError):
return None


def unreferenced_versions(
root: Path,
rows: list[FileRow],
Expand All @@ -1675,48 +1663,40 @@ def unreferenced_versions(
sizes[key] = sizes.get(key, 0) + row.bytes
if not sizes:
return [], None
try:
text = read_text_guarded(root, INSTALLED_PLUGINS)
opened.add(INSTALLED_PLUGINS)
data = json.loads(text)
except (OSError, ValueError) as exc:

def read(relpath: str) -> str:
text = read_text_guarded(root, relpath)
opened.add(relpath)
return text

registry = load_registry(read, root)
if registry.foreign:
return (
[],
f"{INSTALLED_PLUGINS} unreadable ({type(exc).__name__}); nothing reported",
f"{registry.doubt}; the registry may belong to another root, so nothing is reported",
)
if not isinstance(data, dict) or not isinstance(data.get("plugins"), dict):
return [], f"{INSTALLED_PLUGINS} has no `plugins` object; nothing reported"
if registry.doubt:
return [], f"{registry.doubt}; nothing reported"
cache = root / "plugins" / "cache"
cache_resolved = _resolved(cache)
referenced = {p for p in map(_resolved, _install_paths(data)) if p is not None}
if data["plugins"] and not any(cache_resolved in p.parents for p in referenced):
return [], (
f"no installPath in {INSTALLED_PLUGINS} lies under {cache}; the registry may belong "
"to another root, so nothing is reported"
)
now = time.time() if now is None else now
found: list[dict] = []
for (marketplace, plugin, version), size in sizes.items():
if _resolved(cache / marketplace / plugin / version) in referenced:
if resolve(cache / marketplace / plugin / version) in registry.referenced:
continue
rel = f"plugins/cache/{marketplace}/{plugin}/{version}"
try:
marker = read_text_guarded(root, f"{rel}/.orphaned_at")
opened.add(f"{rel}/.orphaned_at")
epoch_ms = int(marker.strip())
orphaned_at, age = iso(epoch_ms / 1000), (now - epoch_ms / 1000) / 86400
except (OSError, ValueError, OverflowError):
orphaned_at, age = None, None
marker = orphan_marker(read, rel, now) or {
"orphaned_at": None,
"marker_age_days": None,
"past_sweep_window": False,
}
found.append(
{
"marketplace": marketplace,
"plugin": plugin,
"version": version,
"path": rel,
"bytes": size,
"orphaned_at": orphaned_at,
"marker_age_days": None if age is None else round(age, 1),
"past_sweep_window": age is not None and age >= ORPHAN_SWEEP_DAYS,
**marker,
"evidence": MEASURED,
}
)
Expand Down
2 changes: 1 addition & 1 deletion plugins/disk-hygiene/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "disk-hygiene",
"version": "0.41.1",
"version": "0.41.2",
"description": "Context-aware disk hygiene for arbitrary directory trees: inventories orphaned and temporary artifacts, classifies evidence into review tiers, and offers exact-path cleanup only after a fresh safety preview and explicit per-tier approval. The target is read-only by default; OS-managed paths, links and mount points, VCS-tracked content without the complete checkout evidence bundle, changed entries, and live-handle uncertainty fail closed.",
"author": {
"name": "Melodic Software",
Expand Down
6 changes: 6 additions & 0 deletions plugins/disk-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
All notable changes to the `disk-hygiene` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.41.2] - 2026-09-30

### Changed

- **The deep inventory's `plugin-cache-version` rows come from a shared `lib/plugin_cache_versions.py`.** The module is byte-identical with the copy in `claude-ops`, so the install-state audit and the deep inventory apply one rule for which cache versions are unreferenced, including the guarded read of `installed_plugins.json`. `scripts/check-cross-plugin-source-drift.sh` fails if the copies diverge.

## [0.41.1] - 2026-09-30

### Changed
Expand Down
Loading
Loading