Explorar el Código

feat(github-ops): Wrap auditors in the full term.sh panel frame

Make the enclosing panel the default grammar (per direction). The three
read-only auditors now render their human output inside the fleet-style
frame — term_panel_open brand header, │ body rail, term_section
sub-headers, colored term_mark rows, score pip-bar, and a footer health
indicator — instead of the lighter bare-header section style. Single-repo
cards and --org fleet sweeps both use the panel.

Stream separation unchanged: framing on stderr, the --json/data product
plain on stdout (verified zero ANSI). Pure-ASCII under TERM_ASCII=1 (full
card scanned clean) — dropped the · separators for whitespace alignment,
which also fits the design principles.

term.sh: new term_panel_line (generic rail body-row, the open-ended
counterpart to the branch-shaped term_leaf_line) + github-ops/audit/
supply-chain/net-ops brand glyphs. TERMINAL-DESIGN.md documents the panel
as the default. Tests extended to assert ASCII purity across the panel +
checklist primitives; 40/40 offline, 11/11 skill suites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0xDarkMatter hace 1 semana
padre
commit
8f697afc99

+ 15 - 10
CHANGELOG.md

@@ -16,18 +16,23 @@ feature releases live in the README "Recent Updates" section.
 
 ### Changed (terminal output)
 - **Terminal design system promoted from experimental to the standard** for claude-mods
-  shell scripts (`docs/TERMINAL-DESIGN.md`). The `github-ops` audit family
-  (`repo-scorecard.sh`, `check-security-posture.sh`, `check-issues.sh`) now sources
-  `skills/_lib/term.sh` for all human-facing framing — cyan section headers, colored
-  status marks, a score pip-bar — while the `--json`/data product on stdout stays plain
-  (stream separation preserved; verified zero ANSI on stdout). Every glyph falls back to
-  ASCII under `TERM_ASCII=1` (a full scorecard renders pure-ASCII), and color follows the
-  stderr TTY so piping `--json | jq` keeps the framing colored.
+  shell scripts (`docs/TERMINAL-DESIGN.md`), with the **enclosing panel as the default
+  grammar**. The `github-ops` audit family (`repo-scorecard.sh`,
+  `check-security-posture.sh`, `check-issues.sh`) now sources `skills/_lib/term.sh` and
+  wraps its human output in the full `term_panel_open … term_panel_close` frame — brand
+  header, `│` body rail, `term_section` sub-headers, colored `term_mark` rows, a score
+  pip-bar, and a footer health indicator — matching the fleet-ops look. The `--json`/data
+  product on stdout stays plain (stream separation preserved; verified zero ANSI on
+  stdout). Every glyph falls back to ASCII under `TERM_ASCII=1` (a full scorecard renders
+  pure-ASCII), and color follows the stderr TTY so piping `--json | jq` keeps framing colored.
 - **`term.sh` additions**: `term_init` takes an optional fd (`term_init 2`) so
   stream-separated tools detect color on the stream the human actually sees; new
-  `term_mark <ok|bad|warn|skip|na|unknown>` checklist primitive (with registered ASCII
-  proxies) and a `TERM_ARROW` (-> ) pointer glyph. github-ops test suite gained 6
-  assertions (source-check + ASCII-fallback purity); 40/40 offline.
+  `term_panel_line` (generic rail body-row, the open-ended counterpart to the
+  branch-shaped `term_leaf_line`), `term_mark <ok|bad|warn|skip|na|unknown>` checklist
+  primitive, a `TERM_ARROW` pointer glyph, and `github-ops`/`audit`/`supply-chain`/`net-ops`
+  brand glyphs — all with registered ASCII proxies. github-ops test suite gained 6
+  assertions (source-check + ASCII-fallback purity across panel + checklist primitives);
+  40/40 offline.
 
 ### Fixed (docs)
 - **`SKILL-SUBAGENT-REFERENCE.md` was self-contradictory and misleading** (surfaced by

+ 3 - 1
docs/TERMINAL-DESIGN.md

@@ -1,6 +1,8 @@
 # Terminal Panel Design System
 
-> **Status:** Active — the standard for terminal output across claude-mods shell scripts. Toolkit: [`skills/_lib/term.sh`](../skills/_lib/term.sh). Consumers: `fleet-ops` (panels), `github-ops` audit family (stream-separated `term_init 2` + `term_mark` checklists). New TTY-facing scripts should source `term.sh` rather than hand-roll ANSI.
+> **Status:** Active — the standard for terminal output across claude-mods shell scripts. Toolkit: [`skills/_lib/term.sh`](../skills/_lib/term.sh).
+>
+> **The enclosing panel is the default grammar.** A TTY-facing script wraps its human output in `term_panel_open … term_panel_close` with body rows on the `│` rail (`term_panel_line`, `term_section`, `term_mark`), so the whole toolkit reads as one instrument. Consumers: `fleet-ops` (the panel + commit-rail dashboard) and the `github-ops` audit family (`repo-scorecard` / `check-security-posture` / `check-issues`, stream-separated via `term_init 2` — panel framing on stderr, the `--json`/data product plain on stdout). New scripts source `term.sh` rather than hand-roll ANSI, and reach for the panel by default; the bare-header section style is a deliberate exception, not the norm.
 >
 > **Format:** Adapted from [google-labs-code/design.md](https://github.com/google-labs-code/design.md) — a structured design-spec template — and remapped to bash CLIs. Where that spec talks about screens, components, and tokens, this one talks about panels, sections, and glyphs.
 

+ 12 - 0
skills/_lib/term.sh

@@ -191,6 +191,10 @@ __term_lookup() {
     BRAND::git)                 entry="🌿|[G]" ;;
     BRAND::windows-ops)         entry="🩺|[H]" ;;
     BRAND::mac-ops)             entry="🩺|[M]" ;;
+    BRAND::github-ops)          entry="🐙|[G]" ;;
+    BRAND::audit)               entry="🔎|[A]" ;;
+    BRAND::supply-chain)        entry="🛡|[S]" ;;
+    BRAND::net-ops)             entry="📡|[N]" ;;
     HEALTH_GLYPH::healthy)      entry="•|(+)" ;;
     HEALTH_GLYPH::pending)      entry="•|(.)" ;;
     HEALTH_GLYPH::warning)      entry="•|(!)" ;;
@@ -341,6 +345,14 @@ term_panel_vert() {
   printf '%s\n' "$(term_color dim "$TERM_TREE_VERT")"
 }
 
+# term_panel_line <text>  — a generic body row on the rail:  │   <text>
+# The fleet-specific term_leaf_line is shaped for branch + commit-rail + age; this
+# is the open-ended counterpart for any panel body. `text` may already carry colored
+# marks (term_mark / term_color) — it is printed verbatim so the caller owns styling.
+term_panel_line() {
+  printf '%s   %s\n' "$(term_color dim "$TERM_TREE_VERT")" "$*"
+}
+
 # ─── Body components ──────────────────────────────────────────────────────
 
 # term_section <state> <label> <count>

+ 15 - 7
skills/github-ops/scripts/check-issues.sh

@@ -33,9 +33,13 @@ GH_TIMEOUT="${GH_TIMEOUT:-15}"   # seconds; bounds the network call
 __lib="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../_lib" 2>/dev/null && pwd || true)"
 if [ -n "${__lib:-}" ] && [ -f "$__lib/term.sh" ]; then . "$__lib/term.sh"; term_init 2
 else
-  term_header() { printf '%s\n' "${1:-}"; }
-  term_color()  { shift; printf '%s' "$*"; }
-  term_mark()   { case "${1:-}" in ok) printf '+';; bad|gap) printf 'x';; warn) printf '!';; skip|na) printf '-';; unknown) printf '?';; *) printf '.';; esac; }
+  term_panel_open()  { printf '== %s %s ==\n' "${2:-}" "${3:-}"; }
+  term_panel_close() { [ -n "${1:-}" ] && printf '%s\n' "$1"; }
+  term_panel_vert()  { :; }
+  term_panel_line()  { printf '  %s\n' "$*"; }
+  term_color()       { shift; printf '%s' "$*"; }
+  term_mark()        { case "${1:-}" in ok) printf '+';; bad|gap) printf 'x';; warn) printf '!';; skip|na) printf '-';; unknown) printf '?';; *) printf '.';; esac; }
+  term_health()      { shift; printf '%s' "$*"; }
   TERM_ARROW="->"
 fi
 
@@ -111,10 +115,14 @@ if [ "$flagged_n" -eq 0 ]; then
 fi
 
 {
-  term_header "OPEN ISSUES: $REPO" "$flagged_n of $total open flagged"
-  printf '%s' "$analysis" | jq -r --arg m "$(term_mark warn)" '.flagged[]
-    | "  \($m) #\(.number)  [\(if .external then "external" else "yours" end)\(if .stale then ",stale" else "" end)]  by \(.author.login)  \(.title)"'
-  echo "$(term_color dim "  ${TERM_ARROW} gh issue view <n> --repo $REPO    (read-only; this never blocks a push)")"
+  term_panel_open github-ops "OPEN ISSUES" "$REPO  $flagged_n of $total flagged"
+  term_panel_vert
+  while IFS= read -r ln; do term_panel_line "$ln"; done < <(printf '%s' "$analysis" | jq -r --arg m "$(term_mark warn)" '.flagged[]
+    | "\($m) #\(.number)  [\(if .external then "external" else "yours" end)\(if .stale then ",stale" else "" end)]  by \(.author.login)  \(.title)"')
+  term_panel_vert
+  term_panel_close \
+    "$(term_color dim "${TERM_ARROW} gh issue view <n>    read-only, never blocks a push")" \
+    "$(term_health warning "$flagged_n flagged")"
 } >&2
 
 exit "$EX_FINDINGS"

+ 34 - 17
skills/github-ops/scripts/check-security-posture.sh

@@ -53,9 +53,14 @@ GH_TIMEOUT="${GH_TIMEOUT:-20}"   # seconds; bounds every network call
 __lib="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../_lib" 2>/dev/null && pwd || true)"
 if [ -n "${__lib:-}" ] && [ -f "$__lib/term.sh" ]; then . "$__lib/term.sh"; term_init 2
 else
-  term_header() { printf '%s\n' "${1:-}"; }
-  term_color()  { shift; printf '%s' "$*"; }
-  term_mark()   { case "${1:-}" in ok) printf '+';; bad|gap) printf 'x';; warn) printf '!';; skip|na) printf '-';; unknown) printf '?';; *) printf '.';; esac; }
+  term_panel_open()  { printf '== %s %s ==\n' "${2:-}" "${3:-}"; }
+  term_panel_close() { [ -n "${1:-}" ] && printf '%s\n' "$1"; }
+  term_panel_vert()  { :; }
+  term_panel_line()  { printf '  %s\n' "$*"; }
+  term_section()     { printf '%s (%s)\n' "${2:-}" "${3:-}"; }
+  term_color()       { shift; printf '%s' "$*"; }
+  term_mark()        { case "${1:-}" in ok) printf '+';; bad|gap) printf 'x';; warn) printf '!';; skip|na) printf '-';; unknown) printf '?';; *) printf '.';; esac; }
+  term_health()      { shift; printf '%s' "$*"; }
 fi
 
 REPO=""; REMOTE="origin"; ORG=""; COMMANDS=0; JSON=0; STRICT=0; ADVISORY=0
@@ -285,30 +290,37 @@ print_human() { # repo_json
   local o="$1" repo vis
   repo="$(printf '%s' "$o" | jq -r '.repo')"
   vis="$(printf '%s' "$o" | jq -r '.visibility')"
+  local hgaps health
+  hgaps="$(printf '%s' "$o" | jq '[.features[]|select(.applicable==true and ((.state=="off") or (.state=="unknown") or ((.open_alerts//0)>0)))]|length')"
+  if [ "$hgaps" -gt 0 ]; then health="$(term_health warning "$hgaps gap(s)/alert(s)")"; else health="$(term_health healthy clean)"; fi
   {
-    term_header "SECURITY POSTURE: $repo" "$vis"
-    printf '%s' "$o" | jq -r \
+    term_panel_open github-ops "SECURITY POSTURE" "$repo  $vis"
+    term_panel_vert
+    while IFS= read -r ln; do term_panel_line "$ln"; done < <(printf '%s' "$o" | jq -r \
       --arg ok "$(term_mark ok)" --arg bad "$(term_mark bad)" \
       --arg na "$(term_mark na)" --arg unk "$(term_mark unknown)" '
       .features[] |
       if .state=="on" then
-        "  \($ok) \(.feature)" +
+        "\($ok) \(.feature)" +
           (if (.open_alerts // 0) > 0 then "  — \(.open_alerts) OPEN alert(s)" + (if .max_severity then ", max \(.max_severity)" else "" end) else "" end) +
           (if .max_severity=="unknown" then "  (alerts: couldn’t read — needs security_events scope)" else "" end)
       elif .state=="n/a" then
-        "  \($na) \(.feature)  n/a (needs GitHub Advanced Security on a private repo)"
+        "\($na) \(.feature)  n/a (needs GitHub Advanced Security on a private repo)"
       elif .state=="unknown" then
-        "  \($unk) \(.feature)  n/a (couldn’t read)"
+        "\($unk) \(.feature)  n/a (couldn’t read)"
       else
-        "  \($bad) \(.feature)  [\(.severity)]"
-      end'
+        "\($bad) \(.feature)  [\(.severity)]"
+      end')
     # Enable commands for gaps.
     local has_gap
     has_gap="$(printf '%s' "$o" | jq '[.features[]|select(.applicable==true and (.state=="off"))]|length')"
     if [ "$has_gap" -gt 0 ]; then
-      term_header "enable commands" "review before running; this script never runs them"
-      printf '%s' "$o" | jq -r '.features[]|select(.applicable==true and .state=="off")|"     \(.enable_command)"'
+      term_panel_vert
+      term_section "" "enable commands" "$has_gap"
+      while IFS= read -r ln; do term_panel_line "$(term_color dim "$ln")"; done < <(printf '%s' "$o" | jq -r '.features[]|select(.applicable==true and .state=="off")|.enable_command')
     fi
+    term_panel_vert
+    term_panel_close "$(term_color dim "review before running    this script never runs them")" "$health"
   } >&2
 }
 
@@ -340,23 +352,26 @@ if [ -n "$ORG" ]; then
   mapfile -t repos < <(printf '%s' "$list" | jq -r '.[].nameWithOwner' | tr -d '\r')
   [ "${#repos[@]}" -gt 0 ] || skip "no non-archived repos for $ORG"
 
+  human=0; [ "$JSON" -eq 0 ] && [ "$COMMANDS" -eq 0 ] && human=1
+  [ "$human" -eq 1 ] && { term_panel_open github-ops "SECURITY POSTURE" "$ORG  fleet sweep" >&2; term_panel_vert >&2; }
+
   all="[]"; any_findings=0; swept=0; unread=0
   for r in "${repos[@]}"; do
     valid_repo "$r" || continue
     obj="$(audit_repo "$r")"; rc=$?
     if [ "$rc" -eq 7 ] || [ -z "$obj" ]; then
       unread=$((unread+1))
-      [ "$JSON" -eq 1 ] || echo "  $(term_mark unknown) $r — couldn't read (skipped)" >&2
+      [ "$human" -eq 1 ] && term_panel_line "$(term_mark unknown) $r — couldn't read (skipped)" >&2
       continue
     fi
     swept=$((swept+1))
     [ "$rc" -eq 10 ] && any_findings=1
     all="$(jq -c --argjson o "$obj" '. + [$o]' <<<"$all")"
-    if [ "$JSON" -eq 0 ] && [ "$COMMANDS" -eq 0 ]; then
+    if [ "$human" -eq 1 ]; then
       gaps="$(printf '%s' "$obj" | jq '[.features[]|select(.applicable==true and ((.state=="off") or (.state=="unknown") or ((.open_alerts//0)>0)))]|length')"
       vis="$(printf '%s' "$obj" | jq -r '.visibility')"
-      if [ "$gaps" -eq 0 ]; then echo "  $(term_mark ok) $r ($vis) — clean" >&2
-      else echo "  $(term_mark bad) $r ($vis) — $gaps gap(s)/alert(s)" >&2; fi
+      if [ "$gaps" -eq 0 ]; then term_panel_line "$(term_mark ok) $r ($vis) — clean" >&2
+      else term_panel_line "$(term_mark bad) $r ($vis) — $gaps gap(s)/alert(s)" >&2; fi
     fi
   done
 
@@ -368,7 +383,9 @@ if [ -n "$ORG" ]; then
     echo "# review before running — these change repo settings" >&2
     printf '%s' "$all" | jq -r '.[] | "# \(.repo)", (.features[]|select(.applicable==true and .state=="off")|"  \(.enable_command)")'
   else
-    term_header "swept $swept repo(s) in $ORG" "$unread unreadable" >&2
+    local_health="$([ "$any_findings" -eq 1 ] && term_health warning "$swept swept  gaps found" || term_health healthy "$swept swept  all clean")"
+    term_panel_vert >&2
+    term_panel_close "$(term_color dim "$unread unreadable")" "$local_health" >&2
   fi
   [ "$any_findings" -eq 1 ] && exit "$EX_FINDINGS"
   exit "$EX_OK"

+ 51 - 27
skills/github-ops/scripts/repo-scorecard.sh

@@ -67,10 +67,15 @@ ISS="$HERE/check-issues.sh"
 __lib="$(cd "$HERE/../../_lib" 2>/dev/null && pwd || true)"
 if [ -n "${__lib:-}" ] && [ -f "$__lib/term.sh" ]; then . "$__lib/term.sh"; term_init 2
 else
-  term_header()  { printf '%s\n' "${1:-}"; }
-  term_color()   { shift; printf '%s' "$*"; }
-  term_mark()    { case "${1:-}" in ok) printf '+';; bad|gap) printf 'x';; warn) printf '!';; skip|na) printf '-';; unknown) printf '?';; *) printf '.';; esac; }
-  term_pip_bar() { :; }
+  term_panel_open()  { printf '== %s %s ==\n' "${2:-}" "${3:-}"; }
+  term_panel_close() { [ -n "${1:-}" ] && printf '%s\n' "$1"; }
+  term_panel_vert()  { :; }
+  term_panel_line()  { printf '  %s\n' "$*"; }
+  term_section()     { printf '%s (%s)\n' "${2:-}" "${3:-}"; }
+  term_color()       { shift; printf '%s' "$*"; }
+  term_mark()        { case "${1:-}" in ok) printf '+';; bad|gap) printf 'x';; warn) printf '!';; skip|na) printf '-';; unknown) printf '?';; *) printf '.';; esac; }
+  term_health()      { shift; printf '%s' "$*"; }
+  term_pip_bar()     { :; }
   TERM_ARROW="->"
 fi
 
@@ -353,20 +358,31 @@ print_card() { # repo_json
   local o="$1" repo vis score grade
   repo="$(jq -r '.repo' <<<"$o")"; vis="$(jq -r '.visibility' <<<"$o")"
   score="$(jq -r '.score' <<<"$o")"; grade="$(jq -r '.grade' <<<"$o")"
+  local health
+  case "$grade" in
+    A|B) health="$(term_health healthy "grade $grade")" ;;
+    C|D) health="$(term_health warning "grade $grade")" ;;
+    *)   health="$(term_health critical "grade $grade")" ;;
+  esac
   {
-    term_header "REPO SCORECARD: $repo" "$vis"
-    echo "  SCORE  $(term_pip_bar score "$score" 100)  $score/100   GRADE $grade"
-    term_header "dimensions (weight)"
-    printf '  %-9s %s  %s\n' "security"  "$(mark "$(jq -r '.dimensions.security.status' <<<"$o")")"  "$(jq -r '.dimensions.security.detail' <<<"$o")  $(term_color dim "(w35)")"
-    printf '  %-9s %s  %s\n' "metadata"  "$(mark "$(jq -r '.dimensions.metadata.status' <<<"$o")")"  "$(jq -r '.dimensions.metadata.detail' <<<"$o")  $(term_color dim "(w25)")"
-    printf '  %-9s %s  %s\n' "release"   "$(mark "$(jq -r '.dimensions.release.status' <<<"$o")")"   "$(jq -r '.dimensions.release.detail' <<<"$o")  $(term_color dim "(w15)")"
-    printf '  %-9s %s  %s\n' "issues"    "$(mark "$(jq -r '.dimensions.issues.status' <<<"$o")")"    "$(jq -r '.dimensions.issues.detail' <<<"$o")  $(term_color dim "(w15)")"
-    printf '  %-9s %s  %s\n' "actions"   "$(mark "$(jq -r '.dimensions.actions.status' <<<"$o")")"   "$(jq -r '.dimensions.actions.detail' <<<"$o")  $(term_color dim "(w10)")"
+    term_panel_open github-ops "REPO SCORECARD" "$repo  $vis"
+    term_panel_vert
+    term_panel_line "SCORE  $(term_pip_bar score "$score" 100)  $score/100   GRADE $grade"
+    term_panel_vert
+    term_section "" "dimensions (weight)" 5
+    local d name w
+    for d in "security:w35" "metadata:w25" "release:w15" "issues:w15" "actions:w10"; do
+      name="${d%%:*}"; w="${d##*:}"
+      term_panel_line "$(printf '%-9s %s  %s' "$name" "$(mark "$(jq -r ".dimensions.$name.status" <<<"$o")")" "$(jq -r ".dimensions.$name.detail" <<<"$o")  $(term_color dim "($w)")")"
+    done
     local nf; nf="$(jq -r '.top_fixes | length' <<<"$o")"
     if [ "$nf" -gt 0 ]; then
-      term_header "top fixes (highest-severity first)"
-      jq -r --arg b "$(term_mark warn)" '.top_fixes[] | "     \($b) " + .' <<<"$o"
+      term_panel_vert
+      term_section "" "top fixes (highest-severity first)" "$nf"
+      while IFS= read -r ln; do term_panel_line "$ln"; done < <(jq -r --arg b "$(term_mark warn)" '.top_fixes[] | "\($b) " + .' <<<"$o")
     fi
+    term_panel_vert
+    term_panel_close "$(term_color dim "weighted: security 35  metadata 25  release 15  issues 15  actions 10")" "$health"
   } >&2
 }
 
@@ -392,13 +408,16 @@ if [ -n "$ORG" ]; then
   mapfile -t repos < <(printf '%s' "$list" | jq -r '.[].nameWithOwner' | tr -d '\r')
   [ "${#repos[@]}" -gt 0 ] || skip "no non-archived repos for $ORG"
 
+  human=0; [ "$JSON" -eq 0 ] && human=1
+  [ "$human" -eq 1 ] && { term_panel_open github-ops "REPO SCORECARD" "$ORG  fleet sweep" >&2; term_panel_vert >&2; }
+
   all="[]"; any_findings=0; swept=0; unread=0; below_min=0
   for r in "${repos[@]}"; do
     valid_repo "$r" || continue
     obj="$(score_repo "$r")"; rc=$?
     if [ "$rc" -eq 7 ] || [ -z "$obj" ] || ! printf '%s' "$obj" | jq -e . >/dev/null 2>&1; then
       unread=$((unread+1))
-      [ "$JSON" -eq 1 ] || echo "  $(term_mark unknown)    $r — couldn't read (skipped)" >&2
+      [ "$human" -eq 1 ] && term_panel_line "$(term_mark unknown)  $r — couldn't read (skipped)" >&2
       continue
     fi
     swept=$((swept+1))
@@ -406,13 +425,13 @@ if [ -n "$ORG" ]; then
     all="$(jq -c --argjson o "$obj" '. + [$o]' <<<"$all")"
     sc="$(jq -r '.score' <<<"$obj")"
     if [ -n "$MIN_SCORE" ] && [ "$sc" -lt "$MIN_SCORE" ]; then below_min=$((below_min+1)); fi
-    if [ "$JSON" -eq 0 ]; then
+    if [ "$human" -eq 1 ]; then
       # matrix row: per-dimension colored marks + score + grade.
       m() { case "$(jq -r ".dimensions.$1.status" <<<"$obj")" in
         ok) term_mark ok;; warn) term_mark warn;; gap) term_mark bad;; "n/a") term_mark na;; *) printf ' ';; esac; }
-      printf '  %-34s S:%s M:%s R:%s I:%s A:%s  %3s %s\n' \
+      term_panel_line "$(printf '%-34s S:%s M:%s R:%s I:%s A:%s  %3s %s' \
         "$r" "$(m security)" "$(m metadata)" "$(m release)" "$(m issues)" "$(m actions)" \
-        "$sc" "$(jq -r '.grade' <<<"$obj")" >&2
+        "$sc" "$(jq -r '.grade' <<<"$obj")")" >&2
     fi
   done
 
@@ -447,16 +466,21 @@ if [ -n "$ORG" ]; then
         min_score:(if $minscore=="" then null else ($minscore|tonumber) end),
         schema:"claude-mods.github-ops.repo-scorecard/v1"})}'
   else
+    if [ "$any_findings" -eq 1 ] || [ "$below_min" -gt 0 ]; then health_roll="$(term_health warning "$swept scored")"
+    else health_roll="$(term_health healthy "$swept scored")"; fi
     {
-      term_header "roll-up: $ORG"
-      printf '%s' "$rollup" | jq -r '
-        "  scored: \(.repos_scored)   unreadable: \(.repos_unreadable)",
-        "  avg score: \(.avg_score)   median: \(.median_score)",
-        "  total open security alerts (fleet): \(.total_open_alerts)",
-        "  repos with a GAP — security:\(.failing_by_dimension.security) metadata:\(.failing_by_dimension.metadata) release:\(.failing_by_dimension.release) issues:\(.failing_by_dimension.issues) actions:\(.failing_by_dimension.actions)",
-        "  worst: " + ([ .worst[] | "\(.repo) (\(.score)/\(.grade))" ] | join(", "))'
-      [ -n "$MIN_SCORE" ] && echo "  below --min-score $MIN_SCORE: $below_min repo(s)"
-      echo "  legend: $(term_mark ok)=ok $(term_mark warn)=warn $(term_mark bad)=gap $(term_mark na)=n/a  ·  S=security M=metadata R=release I=issues A=actions"
+      term_panel_vert
+      term_section "" "roll-up: $ORG" "$swept"
+      while IFS= read -r ln; do term_panel_line "$ln"; done < <(printf '%s' "$rollup" | jq -r '
+        "scored: \(.repos_scored)   unreadable: \(.repos_unreadable)",
+        "avg score: \(.avg_score)   median: \(.median_score)",
+        "total open security alerts (fleet): \(.total_open_alerts)",
+        "gaps by dimension  security:\(.failing_by_dimension.security) metadata:\(.failing_by_dimension.metadata) release:\(.failing_by_dimension.release) issues:\(.failing_by_dimension.issues) actions:\(.failing_by_dimension.actions)",
+        "worst: " + ([ .worst[] | "\(.repo) (\(.score)/\(.grade))" ] | join(", "))')
+      [ -n "$MIN_SCORE" ] && term_panel_line "below --min-score $MIN_SCORE: $below_min repo(s)"
+      term_panel_line "$(term_color dim "legend:") $(term_mark ok) ok  $(term_mark warn) warn  $(term_mark bad) gap  $(term_mark na) n/a   $(term_color dim "S M R I A = the five dimensions")"
+      term_panel_vert
+      term_panel_close "" "$health_roll"
     } >&2
   fi
   { [ "$any_findings" -eq 1 ] || [ "$below_min" -gt 0 ]; } && exit "$EX_FINDINGS"

+ 4 - 2
skills/github-ops/tests/run.sh

@@ -151,9 +151,11 @@ if [ -f "$LIBTERM" ]; then
   ok "term.sh present"
   # Under TERM_ASCII=1 every framing primitive must fall back to pure ASCII
   # (design principle #3: every glyph has a registered ASCII proxy).
-  marks="$(TERM_ASCII=1 LT="$LIBTERM" bash -c '. "$LT"; term_init; printf "%s%s%s%s%s%s%s" \
+  marks="$(TERM_ASCII=1 LT="$LIBTERM" bash -c '. "$LT"; term_init; printf "%s%s%s%s%s%s%s%s%s%s%s" \
     "$(term_mark ok)" "$(term_mark bad)" "$(term_mark warn)" "$(term_mark na)" \
-    "$(term_mark unknown)" "$(term_header hdr)" "$TERM_ARROW"')"
+    "$(term_mark unknown)" "$(term_header hdr)" "$TERM_ARROW" \
+    "$(term_panel_open github-ops PANEL meta)" "$(term_panel_line body)" \
+    "$(term_section "" sect 3)" "$(term_panel_close hk "$(term_health warning x)")"')"
   if printf '%s' "$marks" | LC_ALL=C grep -q '[^[:print:][:cntrl:]]'; then
     no "term.sh TERM_ASCII=1 still emits non-ASCII bytes"
   else ok "term.sh TERM_ASCII=1 primitives are pure ASCII"; fi