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.3",
"version": "0.79.4",
"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
12 changes: 12 additions & 0 deletions plugins/claude-ops/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
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.4] - 2026-10-01

### Fixed

- **The inventory reads the Explore and Plan agents' full disallowed-tools list on Claude Code
2.1.286.** A `...spread` inside `tools` or `disallowedTools` now resolves to the binding its own
module and scope see, by the same rule name and field resolution use. Before, it took the
nearest same-name binding in the bundle, an unrelated call on 2.1.286, so the shared entries
(the Artifact tools among them) were dropped and the field read `partial`. A spread whose
binding is not an array literal, or is assigned anywhere else (a conditional write in the same
block, a nested block, another function, or an expression-bodied arrow), still reads `partial`.

## [0.79.3] - 2026-10-01

### Fixed
Expand Down
7 changes: 6 additions & 1 deletion plugins/claude-ops/skills/inventory/reference/extraction.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,12 @@ An agent in the initializer is `default`, one pushed is `conditional`, one never
`export{X as NAME}` for a three-character-or-longer binding is read, since the chunk's own closing
export can name it plainly (`export{qHe}`) while a later statement renames it. `tools` and
`disallowedTools` resolve element by element (literals, tool-name constants, one level of
`...spread`); `get tools(){...}` is `getter` and `tools:uw.tools` is `reference`, each null.
`...spread`); `get tools(){...}` is `getter` and `tools:uw.tools` is `reference`, each null. A
spread reads the binding its own module and scope see, not the nearest same-name binding in the
bundle. When that binding is not an array literal, is itself a bare or conditional assignment
rather than a declaration, or any other code assigns it (a conditional write in the same block such
as `if(c)pY=["B"]`, a nested block, a function such as `function init(){pY=["B"]}`, or an
expression-bodied arrow such as `()=>pY=["B"]`), the list is `partial`.

### 10. Find built-in tools by shape, not by builder

Expand Down
93 changes: 90 additions & 3 deletions plugins/claude-ops/skills/inventory/scripts/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -1921,6 +1921,91 @@ def _binding_value(
return found.end()


def _spread_array(src: str, braces: BraceMap, ident: str, at: int) -> int | None:
"""The `[` of the array literal `...ident` at `at` spreads, or None.

The binding is the one `at`'s module and scope see (`_binding_value`),
and another function must not write it: `var x=[a];function f(){x=[b]}`
reads b once f has run, so the list is not static.
"""
try:
v = _binding_value(src, braces, ident, at, window=SHORT_IDENT_LOCALITY_BYTES)
except (ValueError, IndexError, RecursionError):
return None
if v is None or not src.startswith("[", v):
return None
head = re.search(
r"(?<![\w$.])" + re.escape(ident) + r"\s*=\s*$", src[max(0, v - 256) : v]
)
if head is None:
return None
if _written_elsewhere(src, braces, ident, max(0, v - 256) + head.start()):
return None
return v


def _written_elsewhere(src: str, braces: BraceMap, ident: str, pos: int) -> bool:
"""Whether the binding at `pos` may not hold its initializer when read:
it is a bare assignment rather than a declaration (`if(c)x=2`,
`c&&(x=2)`), it sits in an expression-bodied arrow (`()=>x=2`), or any
other code writes it, in the same block (`if(c)x=2;`), a nested block,
or a function. A write that a nearer declaration of `ident` shadows is
to that local instead.
"""
ident_re = r"[A-Za-z_$][\w$]*"
simple = r"(?:" + _STR + r"|[\w$.]+|\[(?:" + _STR + r'|[^\[\]"])*\])'
chain = re.compile(
r"(?<![\w$.])(?:var|let|const)\s+(?:"
+ ident_re
+ r"\s*=\s*"
+ simple
+ r"\s*,\s*)*$"
)
# `_declares` misses a declarator whose statement follows a function
# declaration's `}`; a `var` reached back through simple declarators is
# one too.
if not (_declares(src, pos) or chain.search(src, max(0, pos - 4096), pos)):
return True
lo, hi = _chunk_span(src, pos)

def in_arrow(at: int) -> bool:
head = _statement_start(src, at)
head = max(lo, at - 4096) if head is None else head
return "=>" in _mask_strings(src[head:at])
Comment thread
kyle-sexton marked this conversation as resolved.

if in_arrow(pos):
return True
home_fn = _function_block(src, braces, pos)
home_block = braces.enclosing(pos)

def separate(d: int) -> bool:
"""Whether a declaration at `d` introduces its own binding: a `var`
in another function, or a `let`/`const` in another block. A `var`
in the same function is this binding again, and its initializer a
write."""
if d == pos or not _declares(src, d):
return False
if _is_var(src, d) or re.search(r"\bvar\s+$", src[max(0, d - 8) : d]):
return _function_block(src, braces, d) != home_fn
return braces.enclosing(d) != home_block

name = re.compile(r"(?<![\w$.])" + re.escape(ident) + r"(?![\w$])")
for m in _write_pattern(ident).finditer(src, lo, hi):
w = name.search(src, m.start(), m.end())
if w is None or w.start() == pos or separate(w.start()):
continue
w = w.start()
if not _visible(braces, pos, w, src):
continue
scope = _function_block(src, braces, w) or braces.enclosing(w)
if scope is None or not any(
separate(d.start()) and _visible(braces, d.start(), w, src)
for d in name.finditer(src, scope[0], scope[1])
Comment thread
kyle-sexton marked this conversation as resolved.
):
return True
return False


def _function_pattern(ident: str) -> re.Pattern[str]:
return re.compile(r"function\s+" + re.escape(ident) + r"\s*\(([^()]*)\)\s*\{")

Expand Down Expand Up @@ -3224,7 +3309,9 @@ def _array_names(
"""Tool names in the array literal at `open_i`, and whether all resolved.

Elements are string literals, tool-name constants, or a `...spread` of
another array constant, which is followed `hops` deep.
another array constant, which is followed `hops` deep. The spread reads
the binding its own module and scope see (`_binding_value`), not the
nearest same-name binding in the bundle.
"""
names: list[str] = []
complete = True
Expand All @@ -3237,8 +3324,8 @@ def _array_names(
if lit:
names.append(_unescape(lit.group(1)))
elif spread and hops > 0:
v = _nearest_binding(src, spread.group(1), at)
if v is not None and src.startswith("[", v):
v = _spread_array(src, braces, spread.group(1), start)
if v is not None:
more, ok = _array_names(src, braces, v, index, v, hops - 1)
names.extend(more)
complete = complete and ok
Expand Down
75 changes: 75 additions & 0 deletions plugins/claude-ops/skills/inventory/scripts/test_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,81 @@ def test_an_unresolved_type_is_counted(self) -> None:
self.assertEqual(notes["unresolved_names"], ["qq9"])
self.assertEqual(notes["resolved"], notes["definitions_seen"] - 1)

def test_a_spread_reads_its_own_scope_not_the_nearest_binding(self) -> None:
src = AGENT_SRC + (
'var pY=[xt,"Artifact"];function g(){let pY=p(1);return pY}'
'var SP={agentType:"spread-probe",whenToUse:"s",source:"built-in",'
'disallowedTools:[yt,...pY],getSystemPrompt:()=>""};'
)
rec = self._extract(src)[0]["spread-probe"]
self.assertEqual(rec["disallowed_tools"], ["Agent", "Edit", "Artifact"])
self.assertEqual(rec["disallowed_tools_source"], "literal")

def test_a_spread_of_a_non_constant_binding_stays_partial(self) -> None:
src = AGENT_SRC + (
'var pY=[xt,"Artifact"];var pY=c?[xt]:[yt];'
'var SP={agentType:"spread-probe",whenToUse:"s",source:"built-in",'
'disallowedTools:[yt,...pY],getSystemPrompt:()=>""};'
)
rec = self._extract(src)[0]["spread-probe"]
self.assertEqual(rec["disallowed_tools"], ["Agent"])
self.assertEqual(rec["disallowed_tools_source"], "partial")

def test_a_spread_another_function_reassigns_stays_partial(self) -> None:
src = AGENT_SRC + (
'var pY=[xt,"Artifact"];function init(){pY=["Other"]}init();'
'var SP={agentType:"spread-probe",whenToUse:"s",source:"built-in",'
'disallowedTools:[yt,...pY],getSystemPrompt:()=>""};'
)
rec = self._extract(src)[0]["spread-probe"]
self.assertEqual(rec["disallowed_tools"], ["Agent"])
self.assertEqual(rec["disallowed_tools_source"], "partial")

def test_a_spread_written_off_the_straight_line_stays_partial(self) -> None:
for prelude in (
'var pY=[xt,"Artifact"];if(c){pY=["Other"]}',
'var pY=[xt,"Artifact"];for(;;){pY=["Other"]}',
'var pY=[xt,"Artifact"];var f=()=>pY=["Other"];f();',
'var f=()=>pY=["Other"];var pY=[xt,"Artifact"];f();',
'var pY=[xt,"Artifact"];if(c)pY=["Other"];',
'var pY=[xt,"Artifact"];pY=c?["Other"]:pY;',
'var pY=[xt,"Artifact"];c&&(pY=["Other"]);',
'var pY=[xt,"Artifact"];for(;;)pY=["Other"];',
'var pY=[xt,"Artifact"];if(c){var pY=f()}',
):
src = AGENT_SRC + (
prelude + 'var SP={agentType:"spread-probe",'
'whenToUse:"s",source:"built-in",disallowedTools:[yt,...pY],'
'getSystemPrompt:()=>""};'
)
rec = self._extract(src)[0]["spread-probe"]
self.assertEqual(rec["disallowed_tools_source"], "partial", prelude)

def test_a_spread_declared_after_a_function_declaration_resolves(self) -> None:
src = AGENT_SRC + (
'function h(){return 1}var a="x",b=["y","z"],pY=[xt,"Artifact"],q=1;'
'var SP={agentType:"spread-probe",whenToUse:"s",source:"built-in",'
'disallowedTools:[yt,...pY],getSystemPrompt:()=>""};'
)
rec = self._extract(src)[0]["spread-probe"]
self.assertEqual(rec["disallowed_tools"], ["Agent", "Edit", "Artifact"])
self.assertEqual(rec["disallowed_tools_source"], "literal")

def test_a_spread_whose_writer_shadows_the_name_still_resolves(self) -> None:
for writer in (
'function g(){let pY=[];pY=["Other"]}',
'if(c){let pY=f();pY=["Other"]}',
):
src = AGENT_SRC + (
'var pY=[xt,"Artifact"];' + writer + 'var SP={agentType:"spread-probe",'
'whenToUse:"s",source:"built-in",disallowedTools:[yt,...pY],'
'getSystemPrompt:()=>""};'
)
rec = self._extract(src)[0]["spread-probe"]
self.assertEqual(
rec["disallowed_tools"], ["Agent", "Edit", "Artifact"], writer
)

def test_no_roster_leaves_every_agent_absent(self) -> None:
agents, notes = self._extract(AGENT_SRC.split("function R()")[0])
self.assertFalse(notes["roster_found"])
Expand Down
Loading