Skip to content

feat: add claude-ops plugin - #66

Merged
kyle-sexton merged 8 commits into
mainfrom
feat/publish-claude-ops
Jul 11, 2026
Merged

feat: add claude-ops plugin#66
kyle-sexton merged 8 commits into
mainfrom
feat/publish-claude-ops

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Closes the publish gate for melodic-software/medley#1288 (EPIC melodic-software/medley#1273).

What

New claude-ops plugin bundling three Claude Code operations skills migrated from medley:

Skill Purpose
/claude-ops:claude-observability Read locally captured telemetry (OTEL DuckDB store, collector, optional Aspire dashboard, hook-event JSONL, ccusage) with cross-session trend reports and an explicit clean prune action
/claude-ops:claude-troubleshooting Search known Claude product GitHub bugs, check service health/model quality, maintain a persistent tracked-issue registry
/claude-ops:claude-code-changelog Ingest CC changelog entries and integrate them into the current repo (fetch/diff/status read-only; apply gated on explicit user intent)

Marketplace entry: category: monitoring, tags operations, observability, otel, telemetry, troubleshooting, changelog, claude-code, skill. Explicit version: 0.1.0 in plugin.json only.

De-coupling (repo-agnostic)

  • Project root resolves via CLAUDE_PROJECT_DIR, falling back to git rev-parse --show-toplevel from CWD (never the script's own location — plugin cache is not the project).
  • Persistent state moved out of the plugin/source tree to ${CLAUDE_PLUGIN_DATA}: troubleshooting registry.json, check-all output, --write observability reports. Medley's registry data does NOT ship — consumers start empty.
  • Bundled assets referenced via ${CLAUDE_PLUGIN_ROOT}; no ../ reach-outs; sibling-skill refs stay bare; the one cross-plugin ref (/bug-report:bug-report) is qualified with graceful degradation.
  • Medley-specific rules/docs/skill references (work-slice paths, /issues, /onboard, rule-file cites, repo-grep tooling, release-tracking issue markers) generalized to consumer-context seams; integrations skip silently when the consumer lacks them.
  • Tests rewritten self-contained (no external test lib), matching this repo's convention. DuckDB format='nd' alias replaced with canonical 'newline_delimited' (verified equivalent against live duckdb).
  • No userConfig: variability is covered by existing env vars (CC_OTEL_STORE, retention windows, CHECK_ALL_OUTPUT_DIR) and conventional project-relative defaults — no speculative knobs.

Gate evidence

  • claude plugin validate --strict plugins/claude-ops → PASS; claude plugin validate --strict . (catalog manifest) → PASS
  • claude plugin details token cost: always-on ~454 tok (per-component ~150–160 always-on; on-invoke ~3.3k / ~2.4k / ~1.9k)
  • --plugin-dir smoke test in a clean non-medley repo: /claude-ops:claude-troubleshooting status loaded under the claude-ops namespace, ran registry stats against empty state, fetched status.claude.com, and rendered the health snapshot (graceful empty-state degradation confirmed)
  • All 8 bundled test suites pass locally (219 cases); shellcheck --rcfile=.shellcheckrc clean; typos clean; markdownlint-cli2 clean; editorconfig-checker clean; exec bits set on shebang scripts

Security review (per MIGRATION-PLAYBOOK acceptance)

  • Hooks/MCP/LSP: none shipped. Scripts spawn only local tooling (git, jq, gh, python3, optional duckdb/otelcol-contrib/Docker); no eval, no curl | sh.
  • Network egress: gh reads of GitHub issues and curl reads of status.claude.com/Marginlab pages (troubleshooting quality/status), WebFetch of the official changelog — read-only; issue creation is draft-first with explicit confirmation.
  • No secrets/PII; no telemetry emission (this plugin READS local telemetry).

Note

Medium Risk
Large new plugin with many bash scripts that stop/restart otelcol-contrib, prune local telemetry (including prompts/API bodies when captured), and a repo-mutating changelog apply path; network reads via gh/status/changelog are read-only per the PR description.

Overview
Adds the claude-ops plugin (0.1.0) to the marketplace catalog and root README — three repo-agnostic Claude Code operations skills under /claude-ops:*.

Observability is the largest addition: skill docs plus a local OTEL stack (collector config, DuckDB cc-otel.sql, Aspire dashboard starters, JSONL/ccusage query catalogs) and retention via clean / prune-otel-store.sh (hot NDJSON trim, cold Parquet compaction, collector stop/restart with locking). Bundled bash regression tests cover jq pipelines, port probing, prune, and collector startup contracts.

Changelog integration adds /claude-ops:claude-code-changelog with gated actions (fetch/diff/status read-only; apply only on explicit intent) — explore → research → interview → implement, P1/P2/P3 rubric, git-log–based applied-version tracking.

Troubleshooting (per plugin manifest/README; not fully shown in the excerpted diff) covers GitHub bug search, health checks, and a persistent issue registry under ${CLAUDE_PLUGIN_DATA}. Medley-specific paths and shipped registry data are not included; project root resolves via CLAUDE_PROJECT_DIR / git toplevel.

Reviewed by Cursor Bugbot for commit a418c23. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_bf2d092f-d387-4792-8a2f-d0bc41cc5c31)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Cursor Bugbot skipped (usage limit reached), so automated review did not complete; not approving. Human review is needed before merge.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4496529bda

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/claude-ops/skills/claude-observability/scripts/clean.sh Outdated
Comment thread plugins/claude-ops/skills/claude-observability/otel/prune-collector-lifecycle.sh Outdated
Comment thread plugins/claude-ops/skills/claude-troubleshooting/scripts/registry_manager.py Outdated
@kyle-sexton
kyle-sexton force-pushed the feat/publish-claude-ops branch from 4496529 to 1e2e91c Compare July 11, 2026 13:06
@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ccb7f795-d3d6-4ef6-abd4-20e5f77521d8)

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 15s —— View job


PR Review: claude-ops plugin

  • Gather context and understand request
  • Read key plugin files (plugin.json, SKILL.md files, scripts)
  • Review claude-observability skill (clean.sh, prune scripts, OTEL scripts)
  • Review claude-troubleshooting skill (registry_manager.py, scripts)
  • Review claude-code-changelog skill
  • Check marketplace.json and plugin.json structure
  • Verify CLAUDE.md / repo-agnostic compliance
  • Post comprehensive review findings

Overall assessment

Solid architecture: plugin-form isolation is correct (${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}, ${CLAUDE_PROJECT_DIR} fallbacks), no ../ reach-outs, no secrets, marketplace entry looks clean. The observability prune pipeline is unusually careful (dry-check, sentinel locking, compact-before-trim, verify-before-replace). One blocker before merge, two confirmed Codex P2s, and a few smaller issues below.


Critical — Blocker

1. Python 3 SyntaxError in registry_manager.py (×3 locations)

except ValueError, TypeError: is Python 2 syntax. In Python 3 it is a SyntaxError raised at parse time, so the entire module fails to import and every invocation of /claude-ops:claude-troubleshooting that touches the registry exits with a traceback before running any logic.

Three occurrences:

Line Context
185 validate_issue — date field validation
268 action_list — stale-date parsing
449 action_stats — stale-date parsing

Fix: except (ValueError, TypeError): at all three sites. The bundled tests are shell-only (.test.sh), so this escaped the test suite — a Python smoke test (python3 -c "import registry_manager") would catch it.

Fix this →


High — Codex-confirmed P2s

2. Registry keyed by number only, not (repo, number)

find_by_number at line 125 compares i.get("number") == number without comparing repo. The skill tracks six different repos (anthropics/claude-code, anthropics/anthropic-sdk-python, anthropics/anthropic-sdk-csharp, modelcontextprotocol/python-sdk, microsoft/mcp, …). Issue numbers are only unique within a repo, so anthropics/claude-code#1 and anthropics/anthropic-sdk-python#1 collide in every lookup:

  • action_add (line 332): rejects the second as a duplicate
  • action_update / action_remove (lines 364, 395): target whichever one appears first in the list
  • action_validate (line 417): numbers_seen deduplicates globally, flagging valid cross-repo pairs as duplicates

Fix: change the key to (number, repo) everywhere, and update the CLI get subcommand to accept an optional --repo filter.

Fix this →

3. jq exit 5 accepted as success in clean.sh — can silently truncate JSONL

Lines 150–154 treat rc=5 as success with the comment "valid lines still made it to $tmp." This is jq-version-dependent. In jq 1.6 (still common on older Ubuntu/macOS setups), a malformed intermediate line causes jq to stop processing and exit non-zero — events after the malformed record never reach $tmp. Then mv -f "$tmp" "$file" overwrites the original with a truncated file, silently losing all events that followed the bad line.

The flock guard prevents concurrent malformed writes but not lines already in the file from prior aborted writers.

Safer approach: on exit 5, emit a warning and leave the original untouched (return non-zero so the skip path at line 178 fires). Pruning incomplete data is worse than pruning nothing.

Fix this →

4. pkill -f otelcol-contrib kills all matching processes on the machine

default_stop at line 27:

pkill -f otelcol-contrib >/dev/null 2>&1 || true

On a machine where another application (monitoring agent, sidecar, app-level collector) also runs otelcol-contrib, this kills all of them. Only the plugin's own collector is restarted via cleanup. Any other collectors remain dead until manually restarted.

The collector is spawned without tracking a PID file, so there's no clean way to scope the kill. Minimum viable fix: write the spawned PID to $store_dir/collector.pid in start-collector.sh, then read it here and kill only that PID (falling back to pkill if the file is absent or the process is gone).

Fix this →


Medium

5. save_registry has no file-level locking

save_registry at line 90 uses an atomic temp+os.replace which is safe for the final write, but two concurrent add invocations could both read the same registry (both see 5 issues), both add their entry to their in-memory copy, and the last os.replace wins — one addition is silently lost. An fcntl.flock on the registry file during load-through-save would close the window.

6. No Bash 5+ or Python 3 runtime requirements in plugin.json

prune-otel-store.sh uses EPOCHSECONDS (a Bash 5.0 variable). macOS ships Bash 3.2 as the system shell; consumers without Homebrew bash will get EPOCHSECONDS= (empty string), producing a silent arithmetic error that passes validation but computes cutoff_seconds = -N*86400 — an epoch cutoff in 1969 that keeps everything (prune becomes a no-op). The plugin.json has no runtimeRequirements field advertising the Bash 5 + Python 3.6+ constraints, and the README doesn't call them out as prerequisites.


Low / Informational

7. check-all.test.sh mocks gh but has no test for the happy path where snapshot is created by the skill

check-all.sh exits with ERROR: snapshot not found if the TSV doesn't exist; the snapshot creation responsibility sits in the model-invoked context/action-check-all.md prose. This is documented-by-convention but fragile — if the model skips the snapshot step, the shell script fails with a user-unfriendly error. A guard that creates an empty snapshot (or a distinct exit code) would improve the DX.

8. validate_issue URL check too narrow

Line 177: url.startswith("https://github.com/") is correct for all six listed repos (all on GitHub). Minor concern: if a future tracked repo is on https://github.com/ this is fine, but the error message says "url must start with https://github.com/" which implicitly locks the skill to GitHub-hosted issues. Fine today; worth a comment for future extension.


What looks good

  • Plugin isolation is correct: ${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}, ${CLAUDE_PROJECT_DIR} used consistently; no ../ reach-outs
  • OTEL prune lifecycle is carefully designed: sentinel locking, dry-check short-circuit, compact-before-trim, verify-before-replace — the failure modes have been thought through
  • action_add duplicate detection, save_registry atomic write (os.replace), flock in clean.sh — all correct for the single-process case
  • start-collector.sh sentinel double-check before spawn minimizes TOCTOU window
  • Marketplace entry category/tags are appropriate; plugin.json version explicit at 0.1.0
  • clean action requires explicit user confirmation before running (SKILL.md gating)
  • Network egress is read-only; no eval, no curl | sh
  • --dry-run flags throughout are correct and tested

Recommendation: request changes — the Python 3 SyntaxError (finding 1) makes /claude-ops:claude-troubleshooting DOA on first run, and the repo-collision bug (finding 2) will silently corrupt the registry once any two tracked repos share an issue number. Both are small, targeted fixes.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Cursor Bugbot skipped (usage limit reached), so automated review did not complete; not approving. Human review is needed before merge.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e2e91c21d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/claude-ops/skills/claude-troubleshooting/scripts/registry_manager.py Outdated
Comment thread plugins/claude-ops/skills/claude-observability/context/data-sources.md Outdated
@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_e7d92f2d-9c70-471c-b9ab-b9810305ed73)

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 29s —— View job


PR Review: claude-ops plugin

  • Gather context and understand the PR
  • Read key plugin files (plugin.json, SKILL.md files)
  • Review registry_manager.py for Python 3 syntax issues and registry key bugs
  • Review clean.sh for jq exit-5 handling
  • Review prune-collector-lifecycle.sh for pkill scoping
  • Review prune-otel-store.sh for Bash 5+ EPOCHSECONDS usage
  • Check data-sources.md for jq query issue
  • Check start-collector.sh for PID tracking
  • Check if new commit bc90e4a addresses any prior findings
  • Post comprehensive review findings

The new commit (bc90e4a) only adds a # gitleaks:allow annotation to a test fixture. None of the previously flagged issues have been addressed. All findings below are confirmed against the current HEAD.


Critical — Blocker

1. Python 3 SyntaxError in registry_manager.py (×3 locations)

except ValueError, TypeError: is Python 2 syntax. In Python 3, it is a SyntaxError raised at parse time — the entire module fails to import before any logic runs. Every invocation of /claude-ops:claude-troubleshooting that touches the registry exits with a traceback.

Line Function Commit link
185 validate_issue — date field validation L185
268 action_list — stale-date parsing L268
449 action_stats — stale-date parsing L449

Fix: except (ValueError, TypeError): at all three sites. The test suite is shell-only (.test.sh), so this escaped the suite — a one-liner Python smoke test (python3 -c "import registry_manager") would catch it.

Fix this →


High — Confirmed P2s (carry-over from prior reviews, still unaddressed)

2. Registry keyed by number only, not (repo, number)

find_by_number at L125 compares i.get("number") == number without comparing repo. The skill tracks issues across six distinct repos. GitHub issue numbers are only unique within a repo, so anthropics/claude-code#1 and anthropics/anthropic-sdk-python#1 collide in every lookup:

Note also that action_remove L399 filters by i.get("number") != number — this removes all issues with that number across repos.

Fix: change the lookup key to (number, repo) everywhere; update the CLI get subcommand to accept --repo.

Fix this →

3. jq exit 5 accepted as success in clean.sh — can silently truncate JSONL

run_jq at L150–154:

run_jq() {
  jq -c ... "$file" >"$tmp" 2>/dev/null
  local rc=$?
  [[ "$rc" -eq 0 || "$rc" -eq 5 ]]
}

In jq 1.6 (common on older Ubuntu/macOS), a malformed line causes jq to stop processing entirely and exit non-zero — events after the bad line never reach $tmp. Then mv -f "$tmp" "$file" at L164 silently overwrites the original with truncated content, permanently losing all events that followed the malformed record.

Fix: on exit 5, emit a warning and leave the original file untouched (return non-zero so the skip path at L176 fires).

Fix this →

4. pkill -f otelcol-contrib kills all matching processes on the machine

default_stop at L27:

pkill -f otelcol-contrib >/dev/null 2>&1 || true

On a machine where another app (monitoring sidecar, CI agent) also runs otelcol-contrib, pruning kills all collectors, and only the plugin's own one is restarted. Meanwhile, start-collector.sh spawns the collector via a subshell (L170–173) and does not capture or persist the PID — so there is currently no way to scope the kill to the plugin-owned process.

Fix: write the spawned PID to $store_dir/collector.pid after the nohup spawn (echo $! > "$store_dir/collector.pid"; note $! refers to the subshell here so the collector itself should be spawned without the outer subshell, or the inner nohup PID captured via nohup ... & echo $! > pid_file). Then in prune-collector-lifecycle.sh, read that PID and kill only that PID, falling back to pkill if the file is absent.

Fix this →

5. closedAt datetime rejected by date.fromisoformat at runtime

validate_issue L180–186 runs date.fromisoformat(val) on the closedAt field. GitHub's closedAt value (from gh issue view --json closedAt) is YYYY-MM-DDTHH:MM:SSZ — an ISO 8601 datetime with a Z suffix. datetime.date.fromisoformat only accepts YYYY-MM-DD in Python ≤ 3.10 and still rejects the Z suffix in 3.11+, raising ValueError. This means a registry updated with --closedAt "$closed" immediately fails validate.

Note: this is also affected by finding #1 — the except ValueError, TypeError: on L185 is Python 2 syntax and causes a SyntaxError at import time regardless.

Fix: normalize to YYYY-MM-DD (e.g., val[:10]) before parsing, or use datetime.fromisoformat(val.replace('Z', '+00:00')) and .date().

Fix this →


Medium

6. save_registry has no file-level locking

L90: The atomic temp+os.replace protects the final write, but two concurrent add invocations could both call load_registry, both see the same 5-issue list, both add their entry in memory, and the last os.replace wins — one addition is silently lost. An fcntl.flock over the registry file during load-through-save would close the window.

7. EPOCHSECONDS requires Bash 5.0+ — undeclared prerequisite

prune-otel-store.sh L203–204:

cutoff_seconds=$((EPOCHSECONDS - retention_days * SECONDS_PER_DAY))
body_cutoff_seconds=$((EPOCHSECONDS - body_retention_days * SECONDS_PER_DAY))

EPOCHSECONDS is a Bash 5.0 built-in. macOS ships Bash 3.2 as /bin/bash. Consumers without Homebrew bash will get an empty EPOCHSECONDS, producing a silent arithmetic evaluation of (0 - N*86400) — a cutoff in 1969 that keeps all records, making prune a no-op. No error is emitted. The plugin.json has no runtimeRequirements field declaring Bash 5+ or Python 3.6+, and the README doesn't list them as prerequisites.

Fix: add a runtimeRequirements section to plugin.json and guard EPOCHSECONDS usage with a version check (((BASH_VERSINFO[0] >= 5))) or use $(date +%s) as a portable fallback.

8. jq indexing bug in data-sources.md "failed-then-fixed" example

data-sources.md L132–138:

jq -s '
  sort_by(.ts)
  | [range(1; length)
     | select(.[.-1].hook == .[.]hook and .[.-1].exit_code != 0 and .[.].exit_code == 0)
     | .[.-1].hook] ...
' "$HOOK_LOG"

Inside range(1; length), . is the loop integer, not the sorted array. .[.-1] therefore evaluates to integer[integer-1] = null, so null.hook == null.hooktrue and null.exit_code != 0true for every record. The select() passes all entries unconditionally, producing garbage output rather than detecting retried hooks.

Fix: bind the sorted array to a named variable before the range:

sort_by(.ts) as $events
| [ range(1; $events | length)
    | . as $i
    | select($events[$i-1].hook == $events[$i].hook
             and $events[$i-1].exit_code != 0
             and $events[$i].exit_code == 0)
    | $events[$i-1].hook ]
| group_by(.) | map({hook: .[0], retries: length})
| sort_by(-.retries)

Fix this →


Low / Informational

9. check-all.test.sh mocks gh but has no happy-path snapshot test

check-all.sh errors with ERROR: snapshot not found if the TSV doesn't exist; snapshot creation is delegated to the model via context/action-check-all.md prose. If the model skips the snapshot step, the shell script fails with a user-unfriendly error. A distinct exit code or a graceful empty-state would improve DX.

10. validate_issue URL check implicitly locks the skill to GitHub

L177: url.startswith("https://github.com/") is correct today. Worth a comment noting this is intentional so the constraint is visible if a non-GitHub repo is added later.


What looks good

  • Plugin isolation is correct: ${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}, ${CLAUDE_PROJECT_DIR} used consistently; no ../ reach-outs
  • OTEL prune lifecycle is well-designed: mkdir-atomic sentinel lock, dry-check short-circuit, compact-before-trim, verify-before-replace
  • action_add duplicate detection and save_registry atomic write (os.replace + fsync) are correct for the single-process case
  • start-collector.sh double-checks the prune sentinel closest to spawn to minimize the TOCTOU window
  • clean action requires explicit user confirmation before running (SKILL.md gating)
  • Network egress is read-only; no eval, no curl | sh; --dry-run flags throughout are correct and tested
  • Marketplace entry category/tags are appropriate; plugin.json has explicit version: 0.1.0

Recommendation: request changes. Findings 1–5 must be addressed before merge. Finding 1 (Python 3 SyntaxError) makes the troubleshooting skill DOA on first invoke. Finding 2 (repo-key collision) silently corrupts the registry once any two tracked repos share an issue number. Findings 3–5 carry confirmed data-loss or machine-scope risk.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Not approving because Cursor Bugbot skipped (usage limit reached), so the required automated review did not complete successfully. Human review is needed; no reviewers were assigned because no eligible non-author maintainers are available in this repository.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc90e4a1e3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/claude-ops/skills/claude-observability/context/otel-queries.md Outdated
Comment thread plugins/claude-ops/skills/claude-troubleshooting/context/action-check-all.md Outdated
Comment thread plugins/claude-ops/skills/claude-observability/otel/start-dashboard.sh Outdated
Comment thread plugins/claude-ops/skills/claude-observability/otel/net-probe.sh Outdated
@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_d30ce0fc-5725-42d7-b1eb-222d33b26b21)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Not approving because Cursor Bugbot skipped (usage limit reached), so the required automated review did not complete successfully. Human review is needed; no reviewers were assigned because no eligible non-author maintainers are available in this repository.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d52abfa33

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ec2040c4-1962-4ed9-84fd-3f0b924b2a1b)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Not approving because Cursor Bugbot skipped (usage limit reached), so the required automated review did not complete successfully. Human review is needed; no reviewers were assigned because no eligible non-author maintainers are available in this repository.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@kyle-sexton
kyle-sexton force-pushed the feat/publish-claude-ops branch from 972d655 to 4992676 Compare July 11, 2026 13:45
@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_b569742a-5718-4e1b-b290-6429a1d9757b)

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 7s —— View job


PR Review: claude-ops plugin

  • Gather context and understand the request
  • Read git log to understand commit history and what's been fixed
  • Review registry_manager.py — Python 3 syntax, repo-key collision, closedAt handling, locking
  • Review clean.sh — jq exit-5 handling
  • Review prune-collector-lifecycle.sh — pkill scoping fix
  • Review prune-otel-store.sh — EPOCHSECONDS / Bash 5+ guard
  • Review prune-compact.sh — cold parquet filename collision
  • Review start-collector.sh — PID tracking and gRPC port probing
  • Review start-dashboard.sh — OTLP port probing
  • Review net-probe.sh — protocol error classification
  • Review data-sources.md and otel-queries.md — jq query fixes
  • Review action-check-all.md — resolved issue status fix
  • Check plugin structure and CLAUDE.md compliance
  • Post comprehensive findings

Summary

All blockers and high-priority findings from the two prior Claude reviews (Python 3 SyntaxError, repo/number composite key, closedAt datetime, jq exit-5 data loss, pkill blast radius, jq indexing in data-sources.md, union aggregate in otel-queries.md, resolved-issue status in action-check-all.md, OTLP port probing in start-dashboard.sh, net-probe TCP classification) are confirmed fixed in the current HEAD. Three items from the Codex P2 round remain open.


Confirmed Fixed

Finding Location Status
Python 3 SyntaxError except ValueError, TypeError: registry_manager.py:193 ✅ Fixed: except (ValueError, TypeError):
Registry identity (repo, number) registry_manager.py:120–133 ✅ Fixed: find_by_key(issues, number, repo) with explicit ambiguity errors
closedAt rejects GitHub timestamps registry_manager.py:198–205 ✅ Fixed: datetime.fromisoformat(str(closed).replace("Z", "+00:00"))
jq exit-5 silently truncates JSONL clean.sh:149–175 ✅ Fixed: any nonzero jq rc skips the file intact
pkill -f otelcol-contrib kills unrelated collectors prune-collector-lifecycle.sh:37 ✅ Fixed: scoped to otelcol-contrib.*otel-collector.yaml
Failed-then-fixed jq indexing data-sources.md:131–139 ✅ Fixed: sort_by(.ts) as $e with index variable
Union aggregate binder error otel-queries.md:26–32 ✅ Fixed: wrapped in subquery with GROUP BY outside
Resolved issues used invalid status fixed action-check-all.md:13–14 ✅ Fixed: --status closed + --category fixed
start-dashboard.sh only probed UI port start-dashboard.sh:166–180 ✅ Fixed: probes both host_ui_port and host_otlp_port
net-probe.sh classified connected-but-non-HTTP as free net-probe.sh:32–34 ✅ Fixed: exit codes 8/52/56 → listening
action_remove removed all issues matching number registry_manager.py:440 ✅ Fixed: if i is not issue (identity comparison)

Still Open

1. EPOCHSECONDS used without Bash 5+ guard — silent no-op prune on macOS

prune-otel-store.sh:203–204:

cutoff_seconds=$((EPOCHSECONDS - retention_days * SECONDS_PER_DAY))
body_cutoff_seconds=$((EPOCHSECONDS - body_retention_days * SECONDS_PER_DAY))

EPOCHSECONDS is Bash 5.0+. macOS ships Bash 3.2 as /bin/bash; consumers without Homebrew bash will get EPOCHSECONDS expanding to empty string. Arithmetic on empty string evaluates as 0, so cutoff_seconds = 0 - N*86400, which is a large negative epoch — all records are newer than the cutoff and prune silently becomes a no-op. No error is emitted. The script's own shebang comment says "bash 5+" but this is the only runtime check that's missing.

The plugin.json has no runtimeRequirements field to advertise the constraint, and README.md doesn't list it as a prerequisite.

Fix: add a guard at the top of the main function:

if ((BASH_VERSINFO[0] < 5)); then
  EPOCHSECONDS="$(date +%s)"
fi

Or replace with a portable fallback: $(date +%s) works on both GNU date and macOS BSD date.

Fix this →


2. start-collector.sh only probes HTTP port 4318, not gRPC port 4317

start-collector.sh:118:

port_state="$(port_status "$OTLP_HTTP_PORT")"  # only 4318

otel-collector.yaml binds both 127.0.0.1:4317 (gRPC) and 127.0.0.1:4318 (HTTP). When port 4317 is already occupied by another process, port_state reads free, action becomes would-spawn, and the nohup launch proceeds — but the Collector fails its own gRPC bind at startup and silently exits. The script returns 0 and emits spawned Collector, but telemetry capture is off.

start-dashboard.sh was correctly fixed to probe both its ports; the same pattern applies here.

Fix:

readonly OTLP_HTTP_PORT=4318
readonly OTLP_GRPC_PORT=4317
# ...
local http_port_state grpc_port_state
http_port_state="$(port_status "$OTLP_HTTP_PORT")"
grpc_port_state="$(port_status "$OTLP_GRPC_PORT")"
# ...
if [[ "$http_port_state" == "listening" || "$grpc_port_state" == "listening" ]]; then
    action="noop-already-running"
fi

Fix this →


Medium (carry-over, low severity)

3. save_registry has no file-level locking

save_registry at line 90: os.replace makes the final write atomic, but two concurrent add invocations both calling load_registry see the same 5-issue list, each appending their entry to an in-memory copy. The last os.replace wins — one entry is silently lost. An fcntl.flock on the registry file for the load-through-save window closes the race. Low occurrence risk in practice (the skill is invoked interactively), but the failure mode is silent.


What looks good

  • All six previously flagged correctness/data-loss blockers are confirmed fixed with clean implementations
  • Plugin isolation is correct throughout: ${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}, ${CLAUDE_PROJECT_DIR} used consistently; no ../ reach-outs; no hardcoded project paths
  • prune-collector-lifecycle.sh config-file pattern matching is a principled fix — significantly better than bare binary name; the residual documented with escape hatch env vars (CC_OTEL_STOP_CMD / CC_OTEL_RUNNING_CMD) is a reasonable trade-off
  • Cold compaction is protected by the prune sentinel, so the Codex concern about same-second filename collisions is effectively mitigated — a second concurrent prune cannot start while the first holds the sentinel
  • action_add duplicate detection and action_remove identity-comparison removal are correct
  • --dry-run flags throughout are consistent and tested
  • Network egress is read-only; no eval, no curl | sh
  • Marketplace category/tags are appropriate; plugin.json version is explicit at 0.1.0

Recommendation: Finding 1 (EPOCHSECONDS / silent no-op prune) is a medium-severity correctness issue on a commonly-used macOS baseline; finding 2 (gRPC port not probed) is a small gap in the same probing logic just fixed in start-dashboard.sh. Both are straightforward one-liners. Finding 3 (registry locking) is low-risk in practice. The PR is otherwise in good shape.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Not approving because Cursor Bugbot skipped (usage limit reached), so the required automated review did not complete successfully. Human review is needed; no reviewers were assigned because no eligible non-author maintainers are available in this repository.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4992676673

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/claude-ops/skills/claude-observability/otel/net-probe.sh Outdated
@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_e223abcb-3486-47a0-bce8-886259d5005d)

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 57s —— View job


PR Review: claude-ops plugin

  • Gather context and understand the request
  • Check git log for latest commit (b37abde — "fix: address third Codex review batch")
  • Verify previously-open items are fixed (EPOCHSECONDS, gRPC port probe, registry locking)
  • Review new Codex P2s: daemon-path in operator docs, Docker reachability, curl rc=1
  • Post comprehensive review findings

Summary

The third Codex batch (b37abde) closes two remaining findings from the prior round — the gRPC port probe and cold parquet filename collision. Three medium items and one low item remain open.


Confirmed Fixed in b37abde

Finding File Status
start-collector.sh only probed HTTP port 4318 start-collector.sh:113-147 ✅ Fixed — probes both :4318 and :4317; new skip-grpc-port-in-use action with correct advisory message
Cold parquet filename collision within same UTC second prune-compact.sh:59-74 ✅ Fixed — serial existence check appends -N suffix; sentinel prevents concurrent prune so serial check is sufficient

Still Open — Medium

1. EPOCHSECONDS used without Bash 5+ guard — silent no-op prune on macOS

prune-otel-store.sh:203–204:

cutoff_seconds=$((EPOCHSECONDS - retention_days * SECONDS_PER_DAY))
body_cutoff_seconds=$((EPOCHSECONDS - body_retention_days * SECONDS_PER_DAY))

EPOCHSECONDS is Bash 5.0+. macOS ships Bash 3.2 as /bin/bash; consumers without Homebrew bash get EPOCHSECONDS expanding to the empty string, so cutoff_seconds = 0 - N*86400 — a negative epoch in 1969 that's older than every record. Prune becomes a silent no-op: nothing is trimmed, no error is emitted, printf 'cutoff_epoch_seconds=%s\n' outputs a large negative number that looks plausibly wrong but is easy to miss.

This has been flagged in every Claude review round and is unaddressed. One-liner fix in the pruning body before line 203:

local epoch_now="${EPOCHSECONDS:-$(date +%s)}"
cutoff_seconds=$((epoch_now - retention_days * SECONDS_PER_DAY))
body_cutoff_seconds=$((epoch_now - body_retention_days * SECONDS_PER_DAY))

date +%s is portable on GNU date (Linux) and BSD date (macOS). Also worth adding a runtimeRequirements note in plugin.json / README listing Python 3.6+ as a prerequisite regardless.

Fix this →


2. operator-setup-collector-daemon.md uses the old medley path for the scheduled-task config

operator-setup-collector-daemon.md:61:

"<repo-root>\.claude\skills\claude-observability\otel\otel-collector.yaml"

After migration to a plugin, otel-collector.yaml lives in the plugin cache under ${CLAUDE_PLUGIN_ROOT}/skills/claude-observability/otel/otel-collector.yamlnot under the consumer project's .claude\skills\. A consumer following this template will register a scheduled task pointing at a file that doesn't exist; the Collector silently never starts.

The macOS recipe at line 88 has the same problem: <repo-root>/${CLAUDE_PLUGIN_ROOT}/skills/... is a mixed-path nonsense string.

The doc already says the command "carries machine-specific absolute paths" and is "generated from your machine's paths" — it just needs to tell consumers how to find the plugin root (e.g. claude plugin details claude-ops exposes the cache location, or the skill can echo "${CLAUDE_PLUGIN_ROOT}" during setup). The template placeholder should be <plugin-root>, not <repo-root>\.claude\skills\.

Fix this →


3. start-dashboard.sh does not detect an unreachable Docker daemon — violates advisory contract

container_state() at line 78:

if docker inspect "$container_name" >/dev/null 2>&1; then
    ...
fi
...
printf '%s\n' "absent"

When the Docker CLI is installed but the daemon is not running (Docker Desktop paused, socket not owned by the user), docker inspect exits non-zero — indistinguishable from "container does not exist." The function returns "absent", which in main() leads to action="would-spawn" (when ports are free), and run_dashboard_create calls docker run. Under set -euo pipefail, docker run exits non-zero → the whole script exits non-zero, which the header explicitly says it won't: "NON-BLOCKING / advisory: skip paths… exit 0."

The docker-absent path at line 74 handles "no docker binary" but not "binary present, daemon unreachable." Adding a docker info >/dev/null 2>&1 || { printf '%s\n' "docker-daemon-absent"; return 0; } guard before the inspect calls closes the gap.

Fix this →


4. net-probe.sh treats curl rc=1 as "free" — SSH/non-HTTP listeners on OTLP ports appear unoccupied

net-probe.sh:33:

case "$rc" in
  0 | 8 | 52 | 56) printf 'listening\n' ;;
  *)               printf 'free\n' ;;
esac

A prior fix (2nd batch) correctly added rc 8/52/56 for connected-but-non-HTTP cases. The Codex P2 identifies one more: when a TCP peer accepts and sends an SSH banner (or HTTP/0.9 response), curl exits 1 (CURLE_UNSUPPORTED_PROTOCOL). This falls to * and is reported as free. With an SSH daemon on port 4318 (or 18888), start-collector.sh/start-dashboard.sh would attempt a bind and fail rather than reporting the conflict.

Fix: add 1 to the "listening" set:

case "$rc" in
  0 | 1 | 8 | 52 | 56) printf 'listening\n' ;;
  *)                    printf 'free\n' ;;
esac

The existing comment block explains the reasoning; extend it to cover rc=1 (CURLE_UNSUPPORTED_PROTOCOL / protocol mismatch after TCP accept).

Fix this →


Still Open — Low

5. save_registry has no file-level locking (carry-over)

registry_manager.py:90: Two concurrent add invocations both read the same 5-issue list, both append in memory, last os.replace wins — one entry silently lost. fcntl.flock during load-through-save closes the race. Low occurrence risk in the interactive skill use case; documented here for completeness.


What looks good

  • gRPC port probe fix in start-collector.sh is clean — the order of checks (http-listening → sentinel → binary-absent → grpc-listening → would-spawn) is correct
  • Cold parquet filename uniquification in prune-compact.sh is correct; sentinel prevents concurrency, serial loop suffices
  • All prior critical/high fixes (Python 3 SyntaxError, (repo, number) composite key, closedAt datetime, jq exit-5, pkill scoping, jq indexing, DuckDB aggregate, action-check-all status, start-dashboard OTLP port probe) confirmed present
  • Plugin isolation (${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}, ${CLAUDE_PROJECT_DIR}) correct throughout; no ../ reach-outs
  • Marketplace entry and plugin.json version (0.1.0) look clean

Recommendation: request changes. Findings 1 and 2 are the priority — EPOCHSECONDS makes prune silently a no-op on the macOS baseline most plugin consumers run, and the wrong scheduled-task path in the operator docs would leave the always-on daemon unconfigured. Findings 3 and 4 are quick one-liner additions to the advisory scripts. All four are targeted fixes with no architectural impact.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Not approving because Cursor Bugbot skipped (usage limit reached), so the required automated review did not complete successfully. Human review is needed; no reviewers were assigned because no eligible non-author maintainers are available in this repository.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b37abdeb14

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/claude-ops/skills/claude-observability/otel/start-dashboard.sh Outdated
Comment thread plugins/claude-ops/skills/claude-observability/otel/start-dashboard.sh Outdated
@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_f60d8bf2-65b4-45ff-afba-dc8bbf0e52e9)

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 28s —— View job


PR Review: claude-ops plugin

  • Gather context and understand the request
  • Check git log to identify what the fourth Codex batch commit fixed
  • Verify Codex P1 findings: OTEL store .gitignore gap and dashboard port binding
  • Verify remaining P2s: EPOCHSECONDS portability, operator-setup daemon path, Docker daemon detection, curl rc=1
  • Verify stopped-container port-conflict check (new Codex P2)
  • Post comprehensive review findings

The fourth Codex batch commit (5589e12) closes three items: net-probe.sh curl rc=1 classification, start-dashboard.sh Docker daemon reachability detection, and operator-setup-collector-daemon.md stable-copy config path. Five items remain open.


Confirmed Fixed in 5589e12

Finding File Status
net-probe.sh: curl rc=1 treated as free — SSH/non-HTTP listeners appeared unoccupied net-probe.sh:35 ✅ Fixed — 0 | 1 | 8 | 52 | 56 all route to listening
start-dashboard.sh: CLI-present-but-daemon-down killed the script under set -e start-dashboard.sh:81–84 ✅ Fixed — docker info probe produces skip-docker-unreachable advisory exit
operator-setup-collector-daemon.md: Windows/macOS recipes pointed at plugin cache path (changes on update) operator-setup-collector-daemon.md:58–65,91 ✅ Fixed — recipes now say to copy yaml to a stable local path (%USERPROFILE%\.otelcol\) and point the daemon at the copy

Still Open — High

1. EPOCHSECONDS used without Bash 5+ guard — silent no-op prune on macOS (flagged 3× prior rounds, still unaddressed)

prune-otel-store.sh:203–204:

cutoff_seconds=$((EPOCHSECONDS - retention_days * SECONDS_PER_DAY))
body_cutoff_seconds=$((EPOCHSECONDS - body_retention_days * SECONDS_PER_DAY))

EPOCHSECONDS is a Bash 5.0 built-in. macOS ships Bash 3.2 as /bin/bash; consumers without Homebrew bash get it expanding to an empty string. $(( - N*86400)) evaluates as 0 - N*86400 = a large negative epoch — older than every stored record — so prune becomes a silent no-op (nothing is trimmed, no error is emitted). The cutoff_epoch_seconds=<large-negative> line in the output makes it visible only to someone looking closely.

One-liner fix before line 203:

local epoch_now="${EPOCHSECONDS:-$(date +%s)}"
cutoff_seconds=$((epoch_now - retention_days * SECONDS_PER_DAY))
body_cutoff_seconds=$((epoch_now - body_retention_days * SECONDS_PER_DAY))

date +%s is portable on GNU date (Linux) and BSD date (macOS).

Fix this →


2. Dashboard container ports bind to all interfaces — exposes Aspire UI and OTLP ingestion on LAN/VPN (Codex P1, still unaddressed)

start-dashboard.sh:112–113:

-p "${host_ui_port}:${CONTAINER_UI_PORT}" \
-p "${host_otlp_port}:${CONTAINER_OTLP_PORT}" \

Docker's -p host:container without a host IP defaults to 0.0.0.0 — all network interfaces. The Aspire dashboard is spawned with ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true. On any developer machine reachable from a LAN or VPN, this exposes the live telemetry UI (sessions, tool I/O, traces) and the OTLP ingestion port to anyone on the network.

Fix: bind to loopback explicitly:

-p "127.0.0.1:${host_ui_port}:${CONTAINER_UI_PORT}" \
-p "127.0.0.1:${host_otlp_port}:${CONTAINER_OTLP_PORT}" \

Fix this →


3. Default OTEL store path in consumer repo unprotected by .gitignore — raw telemetry can be accidentally committed (Codex P1, still unaddressed)

otel-collector.yaml:101,105,109:

path: ${env:CC_OTEL_STORE:-.claude/observability/otel}/cc-logs.json

When CC_OTEL_STORE is unset in a session, the Collector writes session telemetry (prompts, tool I/O, API request/response bodies) to .claude/observability/otel/ relative to the Collector's working directory — typically the consumer repo root for a SessionStart-hook-spawned Collector. The marketplace repo's .gitignore only excludes .claude/settings.local.json. A consumer repo without an explicit .claude/observability/ ignore will see the JSONL files as untracked and can accidentally stage/commit them.

Mitigations to consider (in priority order):

  1. Default to ${CLAUDE_PLUGIN_DATA} — moves the store fully out of the consumer tree when CC_OTEL_STORE is unset; safest for new consumers
  2. Have setup scripts add the .gitignore entrystart-collector.sh or operator-setup.md could echo '.claude/observability/' >> .gitignore as part of setup
  3. At minimum, document prominentlyoperator-setup.md and SKILL.md should call out the accidental-commit risk in the setup checklist, not just as a buried prerequisite note

Fix this →


Still Open — Medium

4. Stopped dashboard container: no port-conflict guard before docker start (Codex P2, still unaddressed)

start-dashboard.sh:178 and line 236:

stopped) action="would-start" ;;   # no port check — absent path has one, stopped does not
...
would-start)
    docker start "$container_name" >/dev/null   # under set -euo pipefail

The absent branch correctly gates on ui_port_state/otlp_port_state (line 184), but the stopped branch routes directly to would-start without the same check. When the container was stopped externally and another process has claimed its published port, docker start fails with a port-already-allocated error and exits non-zero under set -e. The script's own header says "NON-BLOCKING / advisory: skip paths exit 0" — this violates that contract.

Fix: apply the same port guard to the stopped case:

stopped)
  if [[ "$ui_port_state" == "listening" || "$otlp_port_state" == "listening" ]]; then
    action="skip-port-in-use"
  else
    action="would-start"
  fi
  ;;

Fix this →


Still Open — Low

5. save_registry has no file-level locking (carry-over, low severity)

registry_manager.py:90: Two concurrent add invocations both calling load_registry see the same 5-issue list, both append in memory, last os.replace wins — one entry silently lost. The atomic write is correct for the final write; the race is in the load-through-save window. An fcntl.flock over the registry file closes it. Low occurrence risk in the interactive use case.


What looks good

  • All four blockers and confirmed data-loss issues from prior rounds are fixed and verified present
  • net-probe.sh curl exit-code mapping is now comprehensive and well-documented (rc 0/1/8/52/56 = listening, bias toward free on timeout = correct for advisory tool)
  • start-dashboard.sh docker daemon detection is clean: docker info before docker inspect, with correct advisory skip path
  • operator-setup-collector-daemon.md stable-copy rationale is well-explained (cache path changes on update)
  • Plugin isolation (${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}, ${CLAUDE_PROJECT_DIR}) remains correct throughout; no ../ reach-outs
  • OTEL prune lifecycle (sentinel lock, dry-check, compact-before-trim, verify-before-replace) is well-designed
  • No eval, no curl | sh, no secrets; network egress remains read-only

Recommendation: request changes. Findings 2 and 3 are the priority — exposing the anonymous Aspire dashboard on all interfaces is a clear local-network security issue for any developer on a LAN or VPN, and the unguarded default store path is a data-exfiltration risk (prompts, API bodies in git history). Finding 1 (EPOCHSECONDS) makes prune a silent no-op on the macOS baseline and has been flagged in every review round. Finding 4 is a one-line fix to extend the existing port guard to the stopped-container path.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Not approving because Cursor Bugbot skipped (usage limit reached), so the required automated review did not complete successfully. Human review is needed; no reviewers were assigned because no eligible non-author maintainers are available in this repository.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5589e125a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/claude-ops/skills/claude-observability/otel/start-collector.sh Outdated
@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_347c7030-a835-45f7-ba0d-8c91d90c8371)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Not approving because Cursor Bugbot skipped (usage limit reached), so the required automated review did not complete successfully. Human review is needed; no reviewers were assigned because no eligible non-author maintainers are available in this repository.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b828b1d7f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kyle-sexton
kyle-sexton force-pushed the feat/publish-claude-ops branch from 0b828b1 to 4742897 Compare July 11, 2026 15:37
@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_0286cbf6-f571-462d-939f-5abef9059f2f)

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 12s —— View job


PR Review: claude-ops plugin

  • Gather context and understand the PR state
  • Check latest commit (4742897) against prior open findings
  • Verify EPOCHSECONDS — still unaddressed (flagged 5× rounds)
  • Verify operator-setup-retention.md prune path fix
  • Verify start-collector.sh noop-already-running scope fix
  • Verify prune-compact.sh SQL path escaping fix
  • Check for any new issues introduced by latest batch
  • Post comprehensive review findings

Summary

The sixth Codex batch (4742897) closes three items from the prior round. One medium finding has been open across all six review rounds and remains unaddressed. The rest of the PR is in good shape.


Confirmed Fixed in 4742897

Finding File Status
noop-already-running reported success for foreign :4318 listener, silently losing CC telemetry start-collector.sh:74–86, 149–157 ✅ Fixed — collector_process_running() mirrors prune lifecycle scoping; foreign listener reports skip-port-conflict with explicit "telemetry NOT captured" advisory
Single quotes in store path broke DuckDB SQL literals (/tmp/O'Neil/...) prune-compact.sh:15–23 ✅ Fixed — sql_path() doubles single quotes via ${p//\'/\'\'} before any SQL interpolation site
Scheduled prune task pointed at defunct in-repo .claude/skills/ path operator-setup-retention.md:89–96 ✅ Fixed — recipes resolve via ${CLAUDE_PLUGIN_ROOT}/skills/claude-observability/otel/prune-otel-store.sh with re-register note

Still Open — Medium (flagged in every review round)

EPOCHSECONDS used without Bash 5+ guard — silent no-op prune on macOS

prune-otel-store.sh:203–204:

cutoff_seconds=$((EPOCHSECONDS - retention_days * SECONDS_PER_DAY))
body_cutoff_seconds=$((EPOCHSECONDS - body_retention_days * SECONDS_PER_DAY))

EPOCHSECONDS is Bash 5.0+. macOS ships Bash 3.2 as /bin/bash; consumers without Homebrew bash get it expanding to the empty string. $(( - N*86400)) evaluates as 0 - N*86400 — a large negative epoch that is older than every record. Prune silently becomes a no-op: nothing is trimmed, no error is emitted, and cutoff_epoch_seconds=<large-negative> in the output is easy to miss. The script's own header comment says "bash 5+" but there is no runtime guard.

One-liner fix before line 203:

local epoch_now="${EPOCHSECONDS:-$(date +%s)}"
cutoff_seconds=$((epoch_now - retention_days * SECONDS_PER_DAY))
body_cutoff_seconds=$((epoch_now - body_retention_days * SECONDS_PER_DAY))

date +%s is portable on GNU date (Linux) and BSD date (macOS). This has been flagged in every review round — it is the single remaining open finding.

Fix this →


Still Open — Low (carry-over)

save_registry has no file-level locking

registry_manager.py:90: The os.replace atomic write is correct for the final write, but two concurrent add invocations both calling load_registry first see the same in-memory state. Last writer wins — one entry is silently lost. An fcntl.flock over the registry file for the load-through-save window closes the race. Low occurrence risk in the interactive skill use case.


What looks good

  • All prior critical/high fixes confirmed present: Python 3 SyntaxError, (repo, number) composite key, closedAt ISO datetime, jq exit-5 data loss, pkill blast radius, jq indexing, DuckDB aggregate binder error, action-check-all status enum, dashboard OTLP port probe, net-probe curl exit codes, start-dashboard.sh loopback binding, store .gitignore protection, Docker daemon reachability, stopped-container port guard, gRPC port probe, parquet filename uniquification, operator-setup-collector-daemon path
  • collector_process_running() in start-collector.sh correctly mirrors the prune lifecycle scoping (config-filename shape match, POSIX + Windows both covered)
  • sql_path() in prune-compact.sh handles both path conversion (cygpath) and SQL-literal safety (single-quote doubling) in a single well-named helper
  • Test enum in start-collector.test.sh updated to include skip-port-conflict
  • Plugin isolation (${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}, ${CLAUDE_PROJECT_DIR}) consistent throughout; no ../ reach-outs
  • No eval, no curl | sh; network egress remains read-only; --dry-run flags throughout

Recommendation: the EPOCHSECONDS one-liner is the only thing between this and a clean bill. It is a two-line change that has been called out every round — fixing it removes the last medium finding and gets the PR to merge-ready.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Not approving because Cursor Bugbot skipped (usage limit reached), so the required automated review did not complete successfully. Human review is needed; no reviewers were assigned because no eligible non-author maintainers are available in this repository.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

Bundles three Claude Code operations skills migrated from
melodic-software/medley (melodic-software/medley#1288):

- claude-observability: read locally captured telemetry (OTEL DuckDB
  store, collector, optional Aspire dashboard, hook-event JSONL,
  ccusage) with cross-session trend reports and store pruning
- claude-troubleshooting: search known Claude product GitHub bugs,
  check service health, maintain a persistent tracked-issue registry
- claude-code-changelog: ingest Claude Code changelog entries and
  integrate them into the current repo (fetch/diff/status/apply)

Repo-agnostic: project root resolves via CLAUDE_PROJECT_DIR with git
fallback; persistent state (issue registry, check-all output, written
reports) lives under CLAUDE_PLUGIN_DATA; bundled assets referenced via
CLAUDE_PLUGIN_ROOT; work-item and quirks-doc integrations degrade
gracefully per consumer context. Tests are self-contained (no external
test lib).
- clean.sh: never promote a partial jq temp — a malformed JSONL line
  makes jq stop mid-stream, so accepting exit 5 could silently drop
  every valid event after the bad line; any nonzero rc now skips the
  file intact
- prune-collector-lifecycle.sh: scope collector stop/running checks to
  processes whose command line references this plugin's config file
  instead of every otelcol-contrib on the machine (Windows path now
  matches via CIM CommandLine; posix via pgrep/pkill -f)
- registry_manager.py: issue identity is (repo, number) — duplicate
  detection, get/update/remove disambiguation via --repo; closedAt
  accepts GitHub's datetime timestamps; PEP 758 except clauses
  parenthesized for Python 3.10 compatibility
- data-sources.md: fix invalid jq in the failed-then-fixed query
  (bind sorted array, index by range variable) + add a regression test
  mirroring it; drop two leftover relative reach-outs into medley
  internals (rules schema link, cc-telemetry-ensure hook links)
- net-probe.sh: classify connected-but-non-HTTP curl exits (8/52/56) as
  listening — a non-HTTP process on the port must read occupied or
  callers publish a doomed Docker bind onto it; residual ambiguity
  (timeout) still leans free per the advisory rationale
- start-dashboard.sh: probe the role's OTLP host port too — docker run
  publishes both ports, so either being bound now reports
  skip-port-in-use instead of failing the spawn under set -e
- otel-queries.md: aggregate the hot+cold union in a subquery (the
  bare UNION ALL form is a DuckDB binder error, verified live)
- action-check-all.md: resolved issues record status=closed +
  category=fixed ('fixed' is a category, not a valid status)
- start-collector.sh: probe the gRPC receiver port (4317) too — the
  config binds both receivers, so 4318-free-but-4317-taken now reports
  skip-grpc-port-in-use instead of spawning a collector doomed to die
  on its duplicate bind while claiming success
- prune-compact.sh: uniquify cold parquet filenames when a second prune
  of the same store file lands in the same UTC second — mv -f no longer
  overwrites already-compacted history (append-only contract)
- operator-setup-collector-daemon.md: daemon recipes now point at a
  stable local copy of the plugin's otel-collector.yaml (the in-repo
  .claude/skills path no longer exists under plugin install, and the
  plugin cache path changes on update)
- start-dashboard.sh: probe docker daemon reachability — CLI-present-
  but-daemon-down now reports skip-docker-unreachable instead of dying
  on docker run under set -e
- net-probe.sh: curl rc 1 (protocol mismatch after an accepted
  connection, e.g. a non-HTTP banner) now classifies as listening
- start-dashboard.sh: publish both dashboard ports on 127.0.0.1 only —
  the container runs anonymous-auth and its telemetry can carry
  sensitive data, so it must never be reachable beyond localhost
- start-collector.sh: drop an ignore-all .gitignore into the OTEL store
  dir on spawn (raw prompts/API bodies must not land as committable
  untracked files when the store lives inside a repo); daemon-setup doc
  carries the same advice for the manual path
- start-dashboard.sh: the stopped-container path is port-aware — docker
  start rebinds published ports, so a foreign listener now reports
  skip-port-in-use instead of failing under set -e
- start-collector.sh: a :4318 listener only reports noop-already-running
  when an otelcol-contrib process running this config shape exists —
  a stale/third-party listener now reports skip-port-conflict (telemetry
  NOT captured) instead of silent false success
- prune-compact.sh: sql_path doubles single quotes so paths like
  /tmp/O'Neil survive DuckDB SQL literal interpolation (verified live)
- operator-setup-retention.md: scheduled prune recipes point at the
  installed plugin's script (resolve via CLAUDE_PLUGIN_ROOT; re-register
  after plugin updates) instead of the defunct in-repo path
@kyle-sexton
kyle-sexton force-pushed the feat/publish-claude-ops branch from 4742897 to a418c23 Compare July 11, 2026 15:41
@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_66856be7-c506-4a48-9b7b-5dc213009cfb)

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 54s —— View job


PR Review: claude-ops plugin

  • Gather context and understand the PR state
  • Check latest commit (a418c23) — sixth Codex batch fixes
  • Verify EPOCHSECONDS finding is addressed (flagged every prior round)
  • Scan for any new issues introduced by latest batch
  • Post comprehensive review findings

Confirmed Fixed in a418c23

Finding File Status
noop-already-running on foreign :4318 listener — telemetry silently lost start-collector.sh:68–90, 147–160 ✅ Fixed — collector_process_running() uses config-shape match (pgrep/CIM otel-collector.yaml); foreign listener now reports skip-port-conflict with explicit "telemetry NOT captured" advisory
Single quotes in store path broke DuckDB SQL literals prune-compact.sh:15–23 ✅ Fixed — sql_path() doubles single quotes via ${p//\'/\'\'} before any SQL interpolation
Scheduled prune task pointed at defunct in-repo path operator-setup-retention.md:82–112 ✅ Fixed — Windows and macOS/Linux recipes now reference <plugin-prune-script> resolved via ${CLAUDE_PLUGIN_ROOT}, with a re-register-after-update note

Still Open — Medium (flagged in every review round, remains unaddressed)

EPOCHSECONDS used without Bash 5+ guard — silent no-op prune on macOS

prune-otel-store.sh:203–204:

cutoff_seconds=$((EPOCHSECONDS - retention_days * SECONDS_PER_DAY))
body_cutoff_seconds=$((EPOCHSECONDS - body_retention_days * SECONDS_PER_DAY))

EPOCHSECONDS is a Bash 5.0 built-in. macOS ships Bash 3.2 as /bin/bash; consumers without Homebrew bash get EPOCHSECONDS expanding to the empty string. $(( - N*86400)) evaluates as 0 - N*86400 — a large negative epoch older than every stored record — so prune silently becomes a no-op: nothing is trimmed, no error is emitted, and cutoff_epoch_seconds=<large-negative> in the printed output is easy to overlook. The script header comments "bash 5+" but there is no runtime guard.

One-liner fix before line 203:

local epoch_now="${EPOCHSECONDS:-$(date +%s)}"
cutoff_seconds=$((epoch_now - retention_days * SECONDS_PER_DAY))
body_cutoff_seconds=$((epoch_now - body_retention_days * SECONDS_PER_DAY))

date +%s is portable on GNU date (Linux) and BSD date (macOS). This has been flagged in every review round — it is the single remaining open finding.

Fix this →


Still Open — Low (carry-over)

save_registry has no file-level locking

registry_manager.py:90: The os.replace atomic write is correct for the final write, but two concurrent add invocations both calling load_registry first see the same in-memory state — last writer wins, one entry is silently lost. An fcntl.flock over the registry file for the load-through-save window closes the race. Low occurrence risk given interactive-only skill use.


What looks good

  • All prior critical/high fixes confirmed present: Python 3 SyntaxError, (repo, number) composite key, closedAt ISO datetime, jq exit-5 data loss, pkill blast radius, jq indexing, DuckDB aggregate binder error, action-check-all status enum, dashboard OTLP port probe, net-probe curl exit codes, start-dashboard loopback binding, store .gitignore protection, Docker daemon reachability, stopped-container port guard, gRPC port probe, parquet filename uniquification, operator-setup-collector-daemon path
  • collector_process_running() in start-collector.sh correctly mirrors the prune lifecycle scoping — config-filename shape match with POSIX + Windows both handled; enum updated in test suite
  • sql_path() in prune-compact.sh cleanly handles both MSYS→Windows path conversion and SQL-literal escaping in one helper; all DuckDB interpolation sites flow through it
  • Plugin isolation (${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}, ${CLAUDE_PROJECT_DIR}) consistent throughout; no ../ reach-outs; no hardcoded paths
  • No eval, no curl | sh; network egress read-only; --dry-run flags throughout

Recommendation: EPOCHSECONDS is the only thing between this PR and a clean bill. It is a two-line, zero-risk change that makes prune work correctly on the macOS baseline most plugin consumers run. Fixing it closes the last open finding and the PR is ready to merge.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: medium. Not approving because Cursor Bugbot skipped (usage limit reached), so the required automated review did not complete successfully. Human review is needed; no reviewers were assigned because no eligible non-author maintainers are available in this repository.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@kyle-sexton
kyle-sexton merged commit dde738e into main Jul 11, 2026
22 checks passed
@kyle-sexton
kyle-sexton deleted the feat/publish-claude-ops branch July 11, 2026 15:44

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a418c23a4c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

curl --noproxy '*' -sS --max-time 2 -o /dev/null "http://127.0.0.1:${port}" 2>/dev/null || rc=$?
case "$rc" in
0 | 1 | 8 | 52 | 56) printf 'listening\n' ;;
*) printf 'free\n' ;;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify accepted-connection timeouts as occupied

When a process on one of the probed ports accepts the TCP connection but does not send an HTTP response, curl exits 28 (curl --manual: “Operation timeout”), and this wildcard reports the port as free. I verified the dashboard path with a local listener on 18888 that accepts and sleeps: start-dashboard.sh --dry-run reported port_18888=free and action=would-spawn, so the non-dry run would reach docker run -p 127.0.0.1:18888:... under set -e and exit nonzero instead of taking the advisory skip-port-in-use path.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant