diff --git a/plugins/claude-ops/.claude-plugin/plugin.json b/plugins/claude-ops/.claude-plugin/plugin.json index 0c8acd458e..7d65ea6d94 100644 --- a/plugins/claude-ops/.claude-plugin/plugin.json +++ b/plugins/claude-ops/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "claude-ops", - "version": "0.31.11", + "version": "0.31.12", "description": "Claude Code operations toolkit. Ten skills: inventory (read-only enumeration of the complete invocable surface \u2014 every built-in CLI command with aliases and hidden/gated status, every bundled skill, 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 \u2014 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 \u2014 CLI version, retention-sweep health including the silent unparsable-settings pause, a timed census walk of the install tree as a sweep-cost proxy, active-session and plugin-fleet counts, a process census, and a bundled known-performance-issues reference; separates the three documented suspects \u2014 accumulated state, version regression, component bloat \u2014 and routes remediation out; reports, never mutates), observability (read locally captured telemetry \u2014 OTEL store, collector, hook-event JSONL, ccusage \u2014 with trend reports 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 integrate them into the current repo), plugins (bring a machine's plugin fleet current on demand \u2014 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 \u2014 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 \u2014 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 lives. Plus a family of seven advisory *-audit telemetry-emitter hooks (API errors, config changes, instruction loads, permission denials, pre-compaction, skill usage, tool failures) that emit the shared hook-telemetry envelope, and a reference sink that maps envelopes into the hook-events.jsonl the observability skill reads.", "author": { "name": "Melodic Software", diff --git a/plugins/claude-ops/CHANGELOG.md b/plugins/claude-ops/CHANGELOG.md index 1a6abc11af..2097a561d2 100644 --- a/plugins/claude-ops/CHANGELOG.md +++ b/plugins/claude-ops/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to the `claude-ops` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.31.12] + +### Fixed + +- **`morning-brief` Queues section no longer hardcodes melodic-software queue labels + on live runs (#610).** Default labels remain the melodic-software taxonomy, but + live queries now filter to labels that exist in the target repo (like the + telemetry-issue path, degrading to "no queue labels found" when none match). + Pass `--queue-labels` to pin a custom comma-separated set and `--decision-label` + to pin the parked-decision label. + ## [0.31.11] ### Fixed diff --git a/plugins/claude-ops/skills/morning-brief/SKILL.md b/plugins/claude-ops/skills/morning-brief/SKILL.md index 4ffbb25109..67b72fc795 100644 --- a/plugins/claude-ops/skills/morning-brief/SKILL.md +++ b/plugins/claude-ops/skills/morning-brief/SKILL.md @@ -1,6 +1,6 @@ --- description: "Prints the operator's read-only morning view for the current GitHub repo in one pass — open counts per queue label (needs-triage / ready / needs-decision / needs-human), the gh-native merge-ready PR list, parked decisions with their RECOMMENDED lines, and loop-lane telemetry freshness (last-cycle age + flags). Use when: 'morning brief', 'morning view', 'ops dashboard', 'what needs attention', 'daily standup view', 'operator morning pass', 'queues and merge-ready'. Read-only and gh-based — never mutates issues, PRs, labels, or comments." -argument-hint: "[--repo owner/name] [--telemetry-issue N] [--stale-hours N] — read-only; omit to view the current repo" +argument-hint: "[--repo owner/name] [--telemetry-issue N] [--queue-labels A,B,C] [--decision-label L] [--stale-hours N] — read-only; omit to view the current repo" user-invocable: true disable-model-invocation: false shell: bash @@ -43,9 +43,9 @@ re-query the sections by hand. | Section | Source | Notes | |---|---|---| -| Queues | `gh issue list --label ` counts | Labels: `priority: needs-triage`, `status: ready`, `status: needs-decision`, `needs-human` | +| Queues | `gh issue list --label ` counts | Defaults to melodic-software queue labels; live runs filter to labels that exist in the repo (pass `--queue-labels` to pin a custom set) | | Merge-ready PRs | `gh pr list` filtered to non-draft + `mergeStateStatus=CLEAN` | A light glance signal; `reviewDecision` shown but not required (repos without required review leave it empty) | -| Parked decisions | open `status: needs-decision` issues | Surfaces each one's RECOMMENDED line — the uppercase marker wins over an incidental lowercase mention; a case-insensitive fallback catches lowercase markers | +| Parked decisions | open issues with the decision label (default `status: needs-decision`) | Surfaces each one's RECOMMENDED line — the uppercase marker wins over an incidental lowercase mention; a case-insensitive fallback catches lowercase markers; pass `--decision-label` to pin | | Lane telemetry | the loop-lane telemetry issue's per-lane comments | Each lane's `last-cycle` age (marked `STALE` past `--stale-hours`, default 6) and any `flags:` | | Stranded findings | merged PRs whose unresolved review threads were **created after the merge** | One line per PR at its worst severity, with a finding count; window is `--stranded-days`, default 3 | @@ -60,6 +60,13 @@ The telemetry issue is auto-discovered by title; pass `--telemetry-issue N` to p When no such issue exists (e.g. a consuming repo without loop-lane telemetry), that section reports "no telemetry issue found" and the rest of the brief still renders. +Queue labels default to the melodic-software taxonomy and are filtered to labels +that actually exist in the target repo on live runs, so a consuming repo with a +different scheme does not show misleading `0`/`?` rows. Pass `--queue-labels` to +pin a custom comma-separated set; pass `--decision-label` to pin the parked-decision +label. When none of the configured queue labels exist, the Queues section reports +"no queue labels found" and the rest of the brief still renders. + ## Cross-references - `/source-control:babysit-prs` — the **authoritative** PR merge gate and readiness diff --git a/plugins/claude-ops/skills/morning-brief/morning-brief.test.sh b/plugins/claude-ops/skills/morning-brief/morning-brief.test.sh index 89282d07b4..30dcb704f3 100755 --- a/plugins/claude-ops/skills/morning-brief/morning-brief.test.sh +++ b/plugins/claude-ops/skills/morning-brief/morning-brief.test.sh @@ -46,6 +46,7 @@ assert_exit() { if [[ "$2" == "$3" ]]; then pass "$1"; else fail "$1" "exit $2" # --- Fixtures ----------------------------------------------------------------- NOW="2026-07-20T08:00Z" +FIXTURE_REPO="example/test-repo" cat >"$TMP/counts.json" <<'EOF' {"priority: needs-triage": 28, "status: ready": 44, "status: needs-decision": 3, "needs-human": 7} @@ -224,6 +225,19 @@ cat >"$TMP/telemetry-no-lanes.json" <<'EOF' ] EOF +# A repo whose label taxonomy does not include the melodic-software defaults. +cat >"$TMP/repo-labels-alternate.json" <<'EOF' +["bug", "enhancement", "status: blocked"] +EOF + +# A repo that carries only a subset of the defaults (ready + needs-human). +cat >"$TMP/repo-labels-partial.json" <<'EOF' +["status: ready", "needs-human", "documentation"] +EOF + +# A repo with no labels at all (successful empty lookup, not a failed one). +printf '[]\n' >"$TMP/repo-labels-empty.json" + # --- Full render -------------------------------------------------------------- OUT="$(bash "$BRIEF" --now "$NOW" --stale-hours 6 \ --counts-json "$TMP/counts.json" \ @@ -306,6 +320,72 @@ OUT2="$(bash "$BRIEF" --now "$NOW" \ --telemetry-json "$TMP/no-telemetry.json" 2>&1)" assert_contains "missing telemetry degrades gracefully" "$OUT2" "no telemetry issue found" +# --- Graceful: queue labels absent in consuming repo ------------------------- +OUT_NO_QL="$(bash "$BRIEF" --now "$NOW" \ + --repo "$FIXTURE_REPO" \ + --repo-labels-json "$TMP/repo-labels-alternate.json" \ + --counts-json "$TMP/empty.json" \ + --pr-json "$TMP/empty.json" \ + --decisions-json "$TMP/empty.json" \ + --telemetry-json "$TMP/empty.json" \ + --merged-json "$TMP/merged-clean.json" 2>&1)" +assert_contains "no matching queue labels degrades gracefully" "$OUT_NO_QL" "no queue labels found in this repo" +assert_contains "degradation names the defaults" "$OUT_NO_QL" "pass --queue-labels to customize" +QUEUE_ONLY="$(printf '%s\n' "$OUT_NO_QL" | awk '/^== Queues/,/^== /{if (!/^== / || /^== Queues/) print}')" +assert_not_contains "absent defaults are not rendered as 0 count rows" "$QUEUE_ONLY" $'priority: needs-triage 0' +assert_not_contains "absent defaults are not rendered as ? count rows" "$QUEUE_ONLY" $'priority: needs-triage ?' + +OUT_PARTIAL_QL="$(bash "$BRIEF" --now "$NOW" \ + --repo-labels-json "$TMP/repo-labels-partial.json" \ + --counts-json "$TMP/counts.json" \ + --pr-json "$TMP/empty.json" \ + --decisions-json "$TMP/empty.json" \ + --telemetry-json "$TMP/empty.json" \ + --merged-json "$TMP/merged-clean.json" 2>&1)" +assert_contains "partial taxonomy shows only labels that exist" "$OUT_PARTIAL_QL" "status: ready" +assert_contains "partial taxonomy keeps another existing label" "$OUT_PARTIAL_QL" "needs-human" +assert_not_contains "partial taxonomy drops absent default labels" "$OUT_PARTIAL_QL" "priority: needs-triage" + +OUT_CUSTOM_QL="$(bash "$BRIEF" --now "$NOW" \ + --repo "$FIXTURE_REPO" \ + --queue-labels "bug, enhancement" \ + --repo-labels-json "$TMP/repo-labels-alternate.json" \ + --counts-json "$TMP/empty.json" \ + --pr-json "$TMP/empty.json" \ + --decisions-json "$TMP/empty.json" \ + --telemetry-json "$TMP/empty.json" \ + --merged-json "$TMP/merged-clean.json" 2>&1)" +assert_contains "pinned queue labels are honored" "$OUT_CUSTOM_QL" "bug" +assert_contains "pinned queue labels include the second entry" "$OUT_CUSTOM_QL" "enhancement" + +OUT_NO_DEC="$(bash "$BRIEF" --now "$NOW" \ + --repo "$FIXTURE_REPO" \ + --repo-labels-json "$TMP/repo-labels-alternate.json" \ + --counts-json "$TMP/counts.json" \ + --pr-json "$TMP/empty.json" \ + --telemetry-json "$TMP/empty.json" \ + --merged-json "$TMP/merged-clean.json" 2>&1)" +assert_contains "missing decision label degrades gracefully" "$OUT_NO_DEC" "decision label not found in this repo" + +OUT_EMPTY_LABELS="$(bash "$BRIEF" --now "$NOW" \ + --repo "$FIXTURE_REPO" \ + --repo-labels-json "$TMP/repo-labels-empty.json" \ + --counts-json "$TMP/empty.json" \ + --pr-json "$TMP/empty.json" \ + --decisions-json "$TMP/empty.json" \ + --telemetry-json "$TMP/empty.json" \ + --merged-json "$TMP/merged-clean.json" 2>&1)" +assert_contains "empty label inventory degrades queue section" "$OUT_EMPTY_LABELS" "no queue labels found in this repo" + +OUT_EMPTY_LABELS_DEC="$(bash "$BRIEF" --now "$NOW" \ + --repo "$FIXTURE_REPO" \ + --repo-labels-json "$TMP/repo-labels-empty.json" \ + --counts-json "$TMP/empty.json" \ + --pr-json "$TMP/empty.json" \ + --telemetry-json "$TMP/empty.json" \ + --merged-json "$TMP/merged-clean.json" 2>&1)" +assert_contains "empty label inventory degrades decision section" "$OUT_EMPTY_LABELS_DEC" "decision label not found in this repo" + # --- Graceful: empty queues / no parked decisions ---------------------------- OUT3="$(bash "$BRIEF" --now "$NOW" \ --counts-json "$TMP/counts.json" \ diff --git a/plugins/claude-ops/skills/morning-brief/scripts/morning-brief.sh b/plugins/claude-ops/skills/morning-brief/scripts/morning-brief.sh index 935e026825..2bbb05095a 100755 --- a/plugins/claude-ops/skills/morning-brief/scripts/morning-brief.sh +++ b/plugins/claude-ops/skills/morning-brief/scripts/morning-brief.sh @@ -18,6 +18,8 @@ # morning-brief.sh live view of the current repo # morning-brief.sh --repo owner/name target a specific repo # morning-brief.sh --telemetry-issue N pin the lane-telemetry issue +# morning-brief.sh --queue-labels A,B,C pin the queue-label set (comma-separated) +# morning-brief.sh --decision-label L pin the parked-decision label # morning-brief.sh --stale-hours N age past which a lane is STALE (default 6) # morning-brief.sh --stranded-days N age window for stranded review findings (default 3) # morning-brief.sh --rec-maxlen N truncate RECOMMENDED previews (default 240; 0 = full) @@ -26,6 +28,7 @@ # Fixture flags (skip the network; used by the test suite and for reuse): # --now ISO fixed clock for deterministic staleness # --counts-json FILE label->count object, e.g. {"status: ready":4} +# --repo-labels-json FILE array of label names (or {name} objects) for existence checks # --pr-json FILE array as emitted by `gh pr list --json ...` # --decisions-json FILE array of {number,title,url,body,comments:[{body}]} # --telemetry-json FILE array of {body} (the telemetry issue's comments) @@ -37,17 +40,22 @@ set -uo pipefail -# --- Queue labels (verified against `gh label list`) -------------------------- -QUEUE_LABELS=( +# --- Queue labels (melodic-software defaults; overridable / filtered live) ------ +DEFAULT_QUEUE_LABELS=( "priority: needs-triage" "status: ready" "status: needs-decision" "needs-human" ) -DECISION_LABEL="status: needs-decision" +DEFAULT_DECISION_LABEL="status: needs-decision" +QUEUE_LABELS=() +DECISION_LABEL="" +QUEUE_LABELS_ARG="" +DECISION_LABEL_ARG="" REPO="" TELEMETRY_ISSUE="" +REPO_LABELS_JSON="" STALE_HOURS="6" REC_MAXLEN="240" NOW_ISO="" @@ -110,6 +118,22 @@ while (($# > 0)); do TELEMETRY_ISSUE="$2" shift 2 ;; + --queue-labels) + require_value "$1" "${2:-}" + QUEUE_LABELS_ARG="$2" + shift 2 + ;; + --decision-label) + require_value "$1" "${2:-}" + DECISION_LABEL_ARG="$2" + shift 2 + ;; + --repo-labels-json) + require_value "$1" "${2:-}" + require_file "$1" "$2" + REPO_LABELS_JSON="$2" + shift 2 + ;; --stale-hours) require_value "$1" "${2:-}" require_uint "$1" "$2" @@ -182,8 +206,23 @@ have jq || { # gh is only needed for live sources; a fully fixtured run (tests) must not # require it. Demand it only when at least one section will hit the network. -ANY_LIVE=0 -[[ -z "$COUNTS_JSON" || -z "$PR_JSON" || -z "$DECISIONS_JSON" || -z "$TELEMETRY_JSON" ]] && ANY_LIVE=1 +NEEDS_LIVE_COUNTS=0 +NEEDS_LIVE_PRS=0 +NEEDS_LIVE_DECISIONS=0 +NEEDS_LIVE_TELEMETRY=0 +[[ -z "$COUNTS_JSON" ]] && NEEDS_LIVE_COUNTS=1 +[[ -z "$PR_JSON" ]] && NEEDS_LIVE_PRS=1 +[[ -z "$DECISIONS_JSON" ]] && NEEDS_LIVE_DECISIONS=1 +[[ -z "$TELEMETRY_JSON" ]] && NEEDS_LIVE_TELEMETRY=1 +# Parked-decisions can short-circuit on a fixture label inventory when the +# decision label is absent — no gh issue list needed for that probe. +if ((NEEDS_LIVE_DECISIONS)) && [[ -n "$REPO_LABELS_JSON" ]]; then + _probe_decision_label="${DECISION_LABEL_ARG:-$DEFAULT_DECISION_LABEL}" + if _probe_repo_labels="$(fetch_repo_label_names)"; then + label_exists_in_repo "$_probe_decision_label" "$_probe_repo_labels" || NEEDS_LIVE_DECISIONS=0 + fi +fi +ANY_LIVE=$((NEEDS_LIVE_COUNTS || NEEDS_LIVE_PRS || NEEDS_LIVE_DECISIONS || NEEDS_LIVE_TELEMETRY)) if ((ANY_LIVE)) && ! have gh; then printf 'morning-brief: gh required for live queries (pass fixtures to run offline)\n' >&2 exit 4 @@ -271,17 +310,91 @@ fmt_age() { fi } +# --- Queue label resolution --------------------------------------------------- +# Defaults match melodic-software's taxonomy. Live runs filter to labels that +# actually exist in the target repo so a consuming repo with a different scheme +# does not render misleading 0/? rows. Pass --queue-labels / --decision-label to +# pin a custom set (same spirit as --telemetry-issue). +resolve_queue_labels() { + QUEUE_LABELS=() + if [[ -n "$QUEUE_LABELS_ARG" ]]; then + local part + IFS=',' read -ra _parts <<<"$QUEUE_LABELS_ARG" + for part in "${_parts[@]}"; do + part="${part#"${part%%[![:space:]]*}"}" + part="${part%"${part##*[![:space:]]}"}" + [[ -n "$part" ]] && QUEUE_LABELS+=("$part") + done + else + QUEUE_LABELS=("${DEFAULT_QUEUE_LABELS[@]}") + fi +} + +resolve_decision_label() { + if [[ -n "$DECISION_LABEL_ARG" ]]; then + DECISION_LABEL="$DECISION_LABEL_ARG" + else + DECISION_LABEL="$DEFAULT_DECISION_LABEL" + fi +} + +fetch_repo_label_names() { + local raw="" + if [[ -n "$REPO_LABELS_JSON" ]]; then + jq -e 'type == "array"' "$REPO_LABELS_JSON" >/dev/null 2>&1 || return 1 + raw="$(jq -r '.[] | if type == "string" then . else .name end' "$REPO_LABELS_JSON" 2>/dev/null)" || return 1 + elif [[ -n "$REPO" ]]; then + raw="$(gh label list "${REPO_ARGS[@]}" --limit 500 --json name -q '.[].name' 2>/dev/null | tr -d '\r')" || return 1 + else + return 1 + fi + jq -R -s ' + split("\n") + | map(select(length > 0)) + | unique + ' <<<"$raw" +} + +label_exists_in_repo() { + local label="$1" names="$2" + jq -e --arg l "$label" 'index($l) != null' <<<"$names" >/dev/null 2>&1 +} + +resolve_queue_labels +resolve_decision_label + # ============================================================================= # Section 1 — queue label counts # ============================================================================= print_queues() { echo "== Queues (open issues per label) ==" - local counts="" + local counts="" repo_labels="" labels_to_show=() label n labels_available=0 if [[ -n "$COUNTS_JSON" ]]; then counts="$(cat "$COUNTS_JSON")" fi - local label n - for label in "${QUEUE_LABELS[@]}"; do + if [[ -n "$REPO_LABELS_JSON" || ( -z "$counts" && -n "$REPO" ) ]]; then + if repo_labels="$(fetch_repo_label_names)"; then + labels_available=1 + fi + fi + if ((labels_available)); then + for label in "${QUEUE_LABELS[@]}"; do + label_exists_in_repo "$label" "$repo_labels" && labels_to_show+=("$label") + done + if ((${#labels_to_show[@]} == 0)); then + echo " no queue labels found in this repo (nothing to report)" + if [[ -z "$QUEUE_LABELS_ARG" ]]; then + echo " defaults: ${DEFAULT_QUEUE_LABELS[*]} — pass --queue-labels to customize" + else + echo " pinned: ${QUEUE_LABELS[*]}" + fi + echo + return + fi + else + labels_to_show=("${QUEUE_LABELS[@]}") + fi + for label in "${labels_to_show[@]}"; do if [[ -n "$counts" ]]; then n="$(jq -r --arg l "$label" '.[$l] // 0' <<<"$counts" 2>/dev/null)" else @@ -332,7 +445,17 @@ print_merge_ready() { # ============================================================================= print_decisions() { echo "== Parked decisions (${DECISION_LABEL}) with RECOMMENDED lines ==" - local decisions + local decisions repo_labels="" labels_available=0 + if [[ -z "$DECISIONS_JSON" && ( -n "$REPO" || -n "$REPO_LABELS_JSON" ) ]]; then + if repo_labels="$(fetch_repo_label_names)"; then + labels_available=1 + fi + if ((labels_available)) && ! label_exists_in_repo "$DECISION_LABEL" "$repo_labels"; then + echo " (decision label not found in this repo — pass --decision-label to customize)" + echo + return + fi + fi if [[ -n "$DECISIONS_JSON" ]]; then decisions="$(cat "$DECISIONS_JSON")" else