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.80.3",
"version": "0.81.0",
"description": "Claude Code operations toolkit. Fifteen 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), machine-profile (discover this machine's facts and per-tree identity domains, store them as a re-runnable profile with the observation behind every value, and diff the stored profile against the host now; read-only unless the operator confirms a write, never installs and never reapplies a stored value on its own), 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
23 changes: 23 additions & 0 deletions plugins/claude-ops/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,29 @@
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.81.0] - 2026-10-01

### Added

- **`probe-observability-state.sh --otel-store` reports the scheduled prune task.** A sixth line,
`prune-task:<state>`, reads the Windows `ClaudeCodeOtelPrune` task from `schtasks /query /xml`:
`provisioned` only when pwsh runs the full provisioning launcher signature, `missing`, `disabled`,
`stale path` when a hand-registered action names a prune script that no longer exists,
`hand-registered` while it still does, or `unrecognized action`. Off Windows it reads
`n/a (not Windows)`. The line carries no text from the task, because it reaches model context,
and a UNC path is never tested. The observability skill flags every state but `provisioned` and
`n/a`.

### Changed

- **The OTEL prune's Windows setup points at the provisioned task.** `operator-setup-retention.md`
drops the hand `schtasks /create` recipe, which named the versioned plugin path and broke after
every plugin update. It now describes `ClaudeCodeOtelPrune` as machine provisioning registers it
(melodic-software/provisioning#669): a daily launcher that reads the user-scope claude-ops
`installPath` from `installed_plugins.json` on every run, so plugin updates need no
re-registration. The doc records the three names the two repositories share: the plugin, the
in-plugin script path and `CC_OTEL_STORE`.

## [0.80.3] - 2026-10-01

### Changed
Expand Down
8 changes: 5 additions & 3 deletions plugins/claude-ops/skills/observability/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ OTEL collector :4318: !`bash -c 'source "${CLAUDE_PLUGIN_ROOT}/skills/observabil
OTEL store: !`bash "${CLAUDE_PLUGIN_ROOT}/skills/observability/scripts/probe-observability-state.sh" --otel-store 2>/dev/null || echo "unknown"`

The OTEL store lines are the three hot files (`<name>:<bytes>B` or `absent`), then `cold:<bytes>B (<n> files)`
or `cold:absent`, then `last-prune:<UTC time> (<age>)` or `last-prune:never`. A report states hot size (sum of
the three files), cold size and last-prune age, and flags `last-prune:never` or an age over about 2 days
(`2d` or more): the scheduled prune is not firing (see [context/operator-setup-retention.md](context/operator-setup-retention.md)).
or `cold:absent`, then `last-prune:<UTC time> (<age>)` or `last-prune:never`, then `prune-task:<state>` for the
Windows `ClaudeCodeOtelPrune` task. A report states hot size (sum of the three files), cold size and last-prune
age, and flags `last-prune:never` or an age over about 2 days (`2d` or more): the scheduled prune is not firing.
It also flags any `prune-task:` state other than `provisioned` or `n/a (not Windows)`. The states are listed
in [context/operator-setup-retention.md](context/operator-setup-retention.md).

## Purpose

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,35 +109,59 @@ the JSONL hook-events layer (`hook-events.jsonl` and its rotated `.1`, which the
layer its own 365-day `--keep-skill-usage-days` window, longer because a starvation report wants
long history and those rows carry names and branches only, no content).

### Windows: per-user Scheduled Task (no admin)

The limited runtime user must first have the scoped `SERVICE_STOP | SERVICE_START` grant
converged by machine provisioning.
The grant intentionally includes no service-configuration or ACL-writing rights.

The registration carries machine-specific absolute paths, so it is **generated from your
machine's paths** and never committed. Unlike the boot-time Collector service, the prune is a
bash script, so the task must invoke `bash.exe` by full path. The script ships inside the
installed plugin and sources sibling helpers, so the task must point at the plugin's own directory: resolve
`<skill-dir>/otel/prune-otel-store.sh` from a Claude Code
session and substitute that absolute path below (`<plugin-prune-script>`). The plugin cache path
changes on plugin updates, so re-register the task after updating the plugin. Daily, off-peak
(minimizes overlap with the brief stop window):

```text
schtasks /create /tn "ClaudeCodeOtelPrune" /sc daily /st 04:00 /rl limited /f /tr ^
"\"C:\Program Files\Git\bin\bash.exe\" \"<plugin-prune-script>\""
```

With `CC_OTEL_STORE` set (the prerequisite above) the working directory is irrelevant. Every
resolved path is absolute. To override the retention windows for this task, use the `setx`
recipe above (user env vars are the only surface the task sees).
**Verify:** `bash <skill-dir>/scripts/probe-observability-state.sh --otel-store`
prints five lines: the three hot files, `cold:<bytes>B (<n> files)`, and `last-prune:<UTC time> (<age>)`.
Every successful non-dry prune writes `<store>/.last-prune`; `last-prune:never` or an age of `2d` or
more means the task is not firing. Also `schtasks /query /tn "ClaudeCodeOtelPrune"`;
`schtasks /run /tn "ClaudeCodeOtelPrune"` then re-run a `--dry-run` to confirm the window held.
**Reversal:** `schtasks /delete /tn "ClaudeCodeOtelPrune" /f`.
### Windows: the provisioned daily Scheduled Task (no admin)

Machine provisioning registers the prune on each fleet host as `ClaudeCodeOtelPrune`: daily at
04:00 (off-peak, so the brief Collector stop rarely overlaps a session), as the console account,
run level `Limited`. The same apply converges the scoped `SERVICE_STOP | SERVICE_START` grant the
prune needs; the grant includes no service-configuration or ACL-writing rights.

The task runs a launcher, not a fixed path. On every run it reads the user-scope entry for this
plugin (key `claude-ops@<marketplace>`) in `installed_plugins.json` (under `CLAUDE_CONFIG_DIR`,
else `~/.claude/plugins/`) and runs `skills/observability/otel/prune-otel-store.sh` from that
`installPath` through Git Bash (a machine or a user-scope Git for Windows install). A plugin
update therefore needs no re-registration, and the 14-day orphan sweep of an old version directory
cannot strand the task. The log is `%LOCALAPPDATA%\provisioning\logs\ClaudeCodeOtelPrune.log`: one
UTC timestamp line per run, the prune's output, then `done: <plugin key> <version>` or
`failed: <reason>`. A missing plugin, a missing script and a failed prune each exit 1.

The contract between the two repositories is three names: the plugin key, the in-plugin path
`skills/observability/otel/prune-otel-store.sh`, and `CC_OTEL_STORE`. Renaming or moving the
script, or publishing the plugin from another marketplace, breaks the task. Basis:
`Get-OtelStorePruneArgument` in provisioning's `common/Provisioning.psm1` and the
`ClaudeCodeOtelPrune` rows of `hosts/*/Set-MachineConfiguration.ps1`
(melodic-software/provisioning#669, merged as `16aefbe`), read 2026-10-01; recheck when either
repository changes one of the three names.

A task registered by hand from an earlier version of this page names a versioned cache path or a
copied script. The next provisioning apply replaces it in place under the same name. On a machine
without that provisioning, register an equivalent daily task whose action finds the install path
at run time; one that names the versioned path breaks after the next plugin update.

The task sees only OS user environment variables, so use the `setx` recipe above to override its
retention windows.
**Verify:** `bash <skill-dir>/scripts/probe-observability-state.sh --otel-store` prints six lines:
the three hot files, `cold:<bytes>B (<n> files)`, `last-prune:<UTC time> (<age>)`, and
`prune-task:<state>`. Every successful non-dry prune writes `<store>/.last-prune`;
`last-prune:never` or an age of `2d` or more means the task is not firing or is failing (read its
log). The `prune-task:` states, none of which prints text from the task, since the line reaches
model context (read the action with `schtasks /query /tn "ClaudeCodeOtelPrune" /xml`):

| State | Meaning |
|---|---|
| `provisioned` | enabled, and the action is pwsh running the provisioning launcher: it names `installed_plugins.json`, the `claude-ops@` plugin key and the in-plugin prune path |
| `missing` | no `ClaudeCodeOtelPrune` task: run the provisioning apply |
| `disabled` | the task or its trigger is disabled |
| `stale path` | a hand-registered task names a prune script that no longer exists, typically a version directory the orphan sweep removed |
| `hand-registered` | a hand-registered task whose script still exists: a versioned path breaks at the next plugin update, and a copied script never gets fixes |
| `unrecognized action` | the task runs neither the launcher nor a prune script; a UNC path is never tested |
| `n/a (not Windows)`, `unknown (schtasks not found)` | not checked |

Provisioning's `-Test` also reports the task, and reports drift when the hot files pass 1 GiB
together or `cold/` passes 2 GiB. To run the task now, `schtasks /run /tn "ClaudeCodeOtelPrune"`
from cmd.exe, then re-run a `--dry-run` to confirm the window held.
**Reversal:** provisioning owns the task; remove it there (its host runbook), not by hand, or the
next apply registers it again.

### macOS / Linux: lifecycle integration required

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,21 @@
# EMPTY (no hook-event emitter wired, or no hooks fired yet)
# INVALID root (<value>): the hooks write nothing
#
# --otel-store output (stdout, five lines — one per store file, in order
# cc-logs.json, cc-metrics.json, cc-traces.json, then the cold tier and the
# last prune):
# --otel-store output (stdout, six lines: one per store file, in order
# cc-logs.json, cc-metrics.json, cc-traces.json, then the cold tier, the last
# prune, and the scheduled prune task):
# <name>:<bytes>B
# <name>:absent
# cold:<bytes>B (<n> files) | cold:absent
# last-prune:<ISO-8601 UTC> (<age, e.g. 3h or 2d>) | last-prune:never
# prune-task:provisioned | missing | disabled | stale path | hand-registered
# | unrecognized action | n/a (not Windows) | unknown (schtasks not found)
# The stamp is <store>/.last-prune, written by prune-otel-store.sh at the end of
# every successful non-dry run.
# every successful non-dry run. The task is the Windows ClaudeCodeOtelPrune task:
# `provisioned` is machine provisioning's pwsh launcher, which finds this plugin at
# run time; a hand-registered action names a prune script path, which is stale once
# a plugin update's orphan sweep removes it. No text from the task is printed: the
# line lands in model context.
#
# --pipeline output (stdout, six lines, fixed order and labels; read-only, it
# never heals the guard):
Expand Down Expand Up @@ -194,6 +200,52 @@ hook_files() {
return 0
}

# The ClaudeCodeOtelPrune task's state, read from its exported XML (locale-free,
# unlike the labels of the verbose list output). A disabled task or trigger carries
# <Enabled>false</Enabled>; an enabled one omits the element.
prune_task_state() {
case "${OSTYPE:-}" in
msys* | cygwin* | win*) ;;
*)
printf 'n/a (not Windows)'
return 0
;;
esac
if ! command -v schtasks >/dev/null 2>&1; then
printf 'unknown (schtasks not found)'
return 0
fi
local xml command arguments path
# MSYS would rewrite the /-style options into paths.
if ! xml="$(MSYS2_ARG_CONV_EXCL='*' schtasks /query /tn ClaudeCodeOtelPrune /xml 2>/dev/null | tr -d '\r')"; then
printf 'missing'
return 0
fi
if [[ "$xml" == *'<Enabled>false</Enabled>'* ]]; then
printf 'disabled'
return 0
fi
command="$(sed -n 's:.*<Command>\(.*\)</Command>.*:\1:p' <<<"$xml" | head -n 1)"
arguments="$(sed -n 's:.*<Arguments>\(.*\)</Arguments>.*:\1:p' <<<"$xml" | head -n 1)"
# The provisioning launcher: pwsh reading the plugin index for this plugin's key
# and running the in-plugin prune script from the install it records.
if [[ "$command" == *pwsh.exe && "$arguments" == *installed_plugins.json* &&
"$arguments" == *claude-ops@* && "$arguments" == *skills/observability/otel/prune-otel-store.sh* ]]; then
printf 'provisioned'
return 0
fi
path="$(grep -o '[^";]*prune-otel-store\.sh' <<<"$arguments" | head -n 1)"
path="${path//\\//}"
# A UNC path is never tested: [[ -f ]] on one contacts a remote host.
if [[ -z "$path" || "$path" == //* ]]; then
printf 'unrecognized action'
elif [[ -f "$path" ]]; then
printf 'hand-registered'
else
printf 'stale path'
fi
}

case "$MODE" in
--hook-events)
if ((!ROOT_VALID)); then
Expand Down Expand Up @@ -246,6 +298,7 @@ case "$MODE" in
else
printf 'last-prune:never\n'
fi
printf 'prune-task:%s\n' "$(prune_task_state)"
;;
--pipeline)
PROJECT="$(repo_root)"
Expand Down
Loading
Loading