Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.79.1",
"version": "0.79.2",
"description": "Claude Code operations toolkit. Fourteen 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), check (read-only check that node and jq resolve for the claude-ops hooks; 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
11 changes: 11 additions & 0 deletions plugins/claude-ops/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
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.79.2] - 2026-10-01

### Fixed

- **The inventory no longer reports a phantom built-in command `string` on Claude Code 2.1.286.**
A command, bundled-skill, subagent or tool name held in an identifier now resolves only when the
binding that read sees, by the module and scope rule, is that string constant. A name bound to a
conditional or a call, or a constant in another module, stays unresolved; before, the nearest
string constant anywhere ahead won, so the skill loader's `Vt=$t?smt(e):e` read an unrelated
`Vt="string"`. `VALIDATED_AGAINST` is `2.1.286`.

## [0.79.1] - 2026-10-01

### Fixed
Expand Down
7 changes: 7 additions & 0 deletions plugins/claude-ops/skills/inventory/reference/extraction.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ everywhere, so it is trusted only when that nearest binding lies within
`eo({name:r,...})`) resolves, while a loop variable whose only binding is megabytes away does not.
No preceding binding is unresolved, never guessed.

The index holds only string bindings, so its nearest entry can sit behind a nearer binding it
cannot see. Every candidate, for commands, bundled skills, subagents and tools alike, must also be
the string constant the read sees under the module and scope rule that field resolution uses
(`_scoped_constant`); otherwise the name stays unresolved. In 2.1.286 the generic skill loader
builds `{type:"prompt",name:Vt,...}` with `Vt=$t?smt(e):e` in its own scope, and an unrelated
`Vt="string"` megabytes ahead used to surface as a built-in command `string`.
Comment thread
kyle-sexton marked this conversation as resolved.

Two further shapes, both first seen in 2.1.284:

- **Descriptor member.** `let t=c;ps({name:t.name,description:t.description,...})` with
Expand Down
76 changes: 58 additions & 18 deletions plugins/claude-ops/skills/inventory/scripts/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
# the skill's evals. Drift from it is not an error - the extraction is designed
# to survive ordinary releases - but it downgrades every count from "verified"
# to "believed", which the report has to say out loud.
VALIDATED_AGAINST = "2.1.284"
VALIDATED_AGAINST = "2.1.286"

# Commands that have shipped in every build observed. Their absence means the
# extraction broke, not that Anthropic deleted /help. This is the cheapest
Expand Down Expand Up @@ -1874,12 +1874,18 @@ def _binding_pattern(ident: str) -> re.Pattern[str]:


def _binding_value(
src: str, braces: BraceMap, ident: str, at: int, *, deferred: bool = False
src: str,
braces: BraceMap,
ident: str,
at: int,
*,
deferred: bool = False,
window: int = SHORT_VALUE_LOCALITY_BYTES,
) -> int | None:
"""Offset of the `ident=` value `at` reads.

A single-character name is function-local: the nearest binding before
`at` in `at`'s own module, within `SHORT_VALUE_LOCALITY_BYTES`. A longer
`at` in `at`'s own module, within `window` bytes. A longer
one follows the module rule in `_declaration`. A binding after `at` is
taken only when the read is `deferred`, reached through a getter, a
method, an arrow, or a function-valued field, which run after the module
Expand All @@ -1888,7 +1894,7 @@ def _binding_value(
initializer.
"""
if len(ident) == 1:
lo = max(_chunk_span(src, at)[0], at - SHORT_VALUE_LOCALITY_BYTES)
lo = max(_chunk_span(src, at)[0], at - window)
found = None
for m in _binding_pattern(ident).finditer(src, lo, at):
if _visible(braces, m.start(), at, src):
Expand Down Expand Up @@ -2314,7 +2320,7 @@ def extract_builtin_commands(src: str, braces: BraceMap) -> dict[str, dict[str,
c = _NAME_IDENT_RE.search(body)
if not c or c.group(1) not in idents:
continue
name = resolve_name_ident(c.group(1), open_i, index)
name = resolve_name_ident(src, braces, c.group(1), open_i, index)
if not name or not _NAME_OK.fullmatch(name):
continue
aliases = _read_aliases(body)
Expand Down Expand Up @@ -2479,7 +2485,11 @@ def build_const_index(


def resolve_name_ident(
ident: str, at: int, index: dict[str, list[tuple[int, str]]]
src: str,
braces: BraceMap,
ident: str,
at: int,
index: dict[str, list[tuple[int, str]]],
) -> str | None:
"""Resolve a registration's name identifier by its nearest preceding binding.

Expand All @@ -2490,7 +2500,7 @@ def resolve_name_ident(
is trusted only when that nearest binding lies within
`SHORT_IDENT_LOCALITY_BYTES`, because such names are function-local and a
far binding belongs to some other function. No preceding binding at all is
unresolved, never guessed.
unresolved, never guessed. The candidate must also pass `_scoped_constant`.
"""
bindings = index.get(ident)
if not bindings:
Expand All @@ -2501,7 +2511,31 @@ def resolve_name_ident(
pos, value = bindings[k]
if len(ident) == 1 and at - pos > SHORT_IDENT_LOCALITY_BYTES:
return None
return value
return _scoped_constant(src, braces, ident, at, value)


# A binding's value is a constant only when one string literal is the whole
# expression: `x="a",` or `x="a";`, not `x="a"+y` or `x="a"?b:c`.
_CONST_VALUE_RE = re.compile(_STR + r"(?=[ \t]*(?:[,;)}\n]|\Z))")


def _scoped_constant(
src: str, braces: BraceMap, ident: str, at: int, candidate: str
) -> str | None:
"""`candidate` when the binding the read of `ident` at `at` sees, under
the module and scope rule of `_binding_value`, is that string constant.

A constant index holds only string bindings, so its nearest entry can be
an unrelated one far behind a nearer binding to a conditional or a call
(`Vt=$t?smt(e):e`), or one in another module. Either way the read's
value is not that constant, and the name stays unresolved.
"""
try:
v = _binding_value(src, braces, ident, at, window=SHORT_IDENT_LOCALITY_BYTES)
except (ValueError, IndexError, RecursionError):
return None
m = _CONST_VALUE_RE.match(src, v) if v is not None else None
return candidate if m and _unescape(m.group(1)) == candidate else None


def _nearest_binding(src: str, ident: str, at: int) -> int | None:
Expand Down Expand Up @@ -2780,7 +2814,7 @@ def add(rec: dict[str, Any]) -> None:
continue
name, descriptor = found
else:
resolved = resolve_name_ident(nm.group(2), call_start, index)
resolved = resolve_name_ident(src, braces, nm.group(2), call_start, index)
if resolved is None:
unresolved.append(nm.group(2))
continue
Expand Down Expand Up @@ -3121,7 +3155,11 @@ def _workflow_invocation(


def resolve_tool_ident(
ident: str, at: int, index: dict[str, list[tuple[int, str]]]
src: str,
braces: BraceMap,
ident: str,
at: int,
index: dict[str, list[tuple[int, str]]],
) -> str | None:
"""Resolve a tool-name identifier by its nearest preceding PascalCase binding.

Expand All @@ -3130,18 +3168,20 @@ def resolve_tool_ident(
identifier in between (`no="SendMessage"`, then `no="column"`). The index
holds only tool-shaped values; among them the nearest PascalCase binding
wins, and a snake_case one is taken only when no PascalCase binding
precedes. A single-character identifier keeps the usual locality limit.
precedes. A single-character identifier keeps the usual locality limit,
and the chosen value must pass `_scoped_constant`.
"""
bindings = index.get(ident)
if not bindings:
return None
before = bindings[: bisect.bisect_left(bindings, (at, ""))]
if len(ident) == 1:
before = [b for b in before if at - b[0] <= SHORT_IDENT_LOCALITY_BYTES]
for _, value in reversed(before):
if _PASCAL_RE.fullmatch(value):
return value
return before[-1][1] if before else None
value = next(
(v for _, v in reversed(before) if _PASCAL_RE.fullmatch(v)),
before[-1][1] if before else None,
)
return None if value is None else _scoped_constant(src, braces, ident, at, value)


def _name_expr(
Expand Down Expand Up @@ -3205,7 +3245,7 @@ def _array_names(
else:
complete = False
elif ident and not spread:
value = resolve_tool_ident(ident.group(0), at, index)
value = resolve_tool_ident(src, braces, ident.group(0), at, index)
if value is None:
complete = False
else:
Expand Down Expand Up @@ -3362,7 +3402,7 @@ def extract_builtin_agents(
if kind == "literal":
name = text
elif kind == "ident" and text:
name = resolve_name_ident(text, open_i, name_index)
name = resolve_name_ident(src, braces, text, open_i, name_index)
else:
name = None
if not name or not re.fullmatch(AGENT_NAME_RE, name):
Expand Down Expand Up @@ -3470,7 +3510,7 @@ def extract_builtin_tools(
if kind == "literal":
name = text
elif kind == "ident" and text:
name = resolve_tool_ident(text, open_i, index)
name = resolve_tool_ident(src, braces, text, open_i, index)
if name is None and (
kind == "member" or (kind == "ident" and len(text or "") == 1)
):
Expand Down
Loading
Loading