Kaynağa Gözat

feat(skills): Adopt term.sh design system in the staleness verifiers

Retrofit the four resource verifiers' human framing (all stream-separated:
findings/--json on stdout, framing on stderr — untouched data contract).

- terraform-ops/check-action-refs.sh: term.sh panel on stderr (term_init 2) —
  brand header, term_status_row check rows, footer health indicator — when the
  stream is a TTY (or FORCE_COLOR); piped/quiet keeps the legacy === / [TAG]
  lines. Exit codes and the stdout findings/JSON are unchanged.
- claude-api-ops/check-model-table.py, claude-code-ops/validate-hooks-json.py,
  playwright-ops/triage-flakes.py: inline Term helper (term.sh is bash-only;
  TERMINAL-DESIGN.md §9 keeps Python ports inline) colorizes the header /
  outcome / count lines. Each forces UTF-8 stdio and the helper also falls back
  to ASCII glyphs on a non-UTF stream encoding, so the Windows cp1252-pipe
  UnicodeEncodeError class can't bite.

tests/check-resources.sh (the offline CI gate that already exercises these)
gains a terminal-design block: every verifier's framing is asserted ASCII-pure
under TERM_ASCII=1 FORCE_COLOR=1, plus term.sh/inline-Term adoption. 40/40-style
offline checks stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0xDarkMatter 2 gün önce
ebeveyn
işleme
40906bdb83

+ 46 - 8
skills/claude-api-ops/scripts/check-model-table.py

@@ -53,6 +53,44 @@ for _stream in (sys.stdout, sys.stderr):
     except (AttributeError, ValueError):
         pass
 
+class Term:
+    """Tiny ANSI helper mirroring skills/_lib/term.sh (term.sh is bash-only; per
+    TERMINAL-DESIGN.md §9 the Python port is inline with matching keys/glyphs).
+    Honors FORCE_COLOR / NO_COLOR / TERM_ASCII; color tracks the bound stream's TTY,
+    and glyphs fall back to ASCII on TERM_ASCII or a non-UTF stream encoding."""
+
+    _C = {"green": "\033[32m", "yellow": "\033[33m", "orange": "\033[38;5;208m",
+          "red": "\033[31m", "cyan": "\033[36m", "dim": "\033[2m", "off": "\033[0m"}
+    _GLYPH = {"ok": "✓", "bad": "✗", "warn": "▲", "skip": "—", "na": "—", "unknown": "?"}
+    _ASCII = {"ok": "+", "bad": "x", "warn": "!", "skip": "-", "na": "-", "unknown": "?"}
+    _MARK_COLOR = {"ok": "green", "bad": "red", "warn": "orange", "skip": "dim",
+                   "na": "dim", "unknown": "yellow"}
+
+    def __init__(self, stream=sys.stderr):
+        enc = (getattr(stream, "encoding", "") or "").lower()
+        self.ascii = (os.environ.get("TERM_ASCII") == "1"
+                      or os.environ.get("FLEET_ASCII") == "1" or "utf" not in enc)
+        if os.environ.get("FORCE_COLOR"):
+            self.color = True
+        elif (os.environ.get("NO_COLOR") is not None or os.environ.get("TERM") == "dumb"
+              or not getattr(stream, "isatty", lambda: False)()):
+            self.color = False
+        else:
+            self.color = True
+
+    def c(self, name, text):
+        return f"{self._C.get(name, '')}{text}{self._C['off']}" if self.color else text
+
+    def mark(self, state):
+        return self.c(self._MARK_COLOR.get(state, ""),
+                      (self._ASCII if self.ascii else self._GLYPH).get(state, "."))
+
+    def hdr(self, text):
+        return self.c("cyan", f"=== {text} ===")
+
+
+TERM = Term(sys.stderr)
+
 EXIT_OK = 0
 EXIT_USAGE = 2
 EXIT_NOT_FOUND = 3
@@ -81,7 +119,7 @@ def fail_validation(message: str, details: dict, json_mode: bool) -> NoReturn:
     if json_mode:
         print(json.dumps({"error": {"code": "VALIDATION", "message": message,
                                     "details": details}}))
-    print(f"ERROR: {message}", file=sys.stderr)
+    print(f"{TERM.mark('bad')} ERROR: {message}", file=sys.stderr)
     for k, v in details.items():
         print(f"  {k}: {v}", file=sys.stderr)
     sys.exit(EXIT_VALIDATION)
@@ -203,7 +241,7 @@ def validate_offline(skill_dir: Path, json_mode: bool, quiet: bool) -> dict:
             print(f"ERROR: required file not found: {p}", file=sys.stderr)
             sys.exit(EXIT_NOT_FOUND)
 
-    note("=== offline model-table consistency check ===", quiet)
+    note(TERM.hdr("offline model-table consistency check"), quiet)
 
     model_rows, _ = parse_model_table(skill_md.read_text(encoding="utf-8"))
     if not model_rows:
@@ -274,7 +312,7 @@ def validate_offline(skill_dir: Path, json_mode: bool, quiet: bool) -> dict:
     note(f"  {len(models_out)} model rows, all well-formed", quiet)
     note(f"  {len(cache_rows)} cache-minimum rows, all integer", quiet)
     note("  cross-file model lineup consistent", quiet)
-    note("OK: tables internally consistent.", quiet)
+    note(f"{TERM.mark('ok')} OK: tables internally consistent.", quiet)
 
     return {
         "mode": "offline",
@@ -336,7 +374,7 @@ def validate_live(skill_dir: Path, json_mode: bool, quiet: bool) -> dict:
         sys.exit(EXIT_MISSING_DEP)
 
     # Reuse offline parse for the documented id set (also validates well-formedness).
-    note("=== live model-id coverage check ===", quiet)
+    note(TERM.hdr("live model-id coverage check"), quiet)
     skill_md = skill_dir / "SKILL.md"
     if not skill_md.is_file():
         print(f"ERROR: required file not found: {skill_md}", file=sys.stderr)
@@ -379,13 +417,13 @@ def validate_live(skill_dir: Path, json_mode: bool, quiet: bool) -> dict:
 
     if drift:
         if missing:
-            note("DRIFT: documented id(s) absent from live Models API:", quiet)
+            note(f"{TERM.mark('bad')} {TERM.c('red', 'DRIFT: documented id(s) absent from live Models API:')}", quiet)
             for m in missing:
-                note(f"  - {m}", quiet)
+                note(f"  {TERM.c('red', '-')} {m}", quiet)
         if new_models:
-            note("DRIFT: live Models API has alias id(s) the table lacks:", quiet)
+            note(f"{TERM.mark('bad')} {TERM.c('red', 'DRIFT: live Models API has alias id(s) the table lacks:')}", quiet)
             for m in new_models:
-                note(f"  + {m}", quiet)
+                note(f"  {TERM.c('green', '+')} {m}", quiet)
         if json_mode:
             print(json.dumps({"data": result, "meta": {"schema": SCHEMA,
                                                         "status": "drift"}}))

+ 55 - 5
skills/claude-code-ops/scripts/validate-hooks-json.py

@@ -28,6 +28,52 @@ import os
 import subprocess
 import sys
 
+# Windows consoles default to cp1252; force UTF-8 so glyphs/em-dashes in framing
+# don't raise UnicodeEncodeError (the repo's standard fix).
+for _stream in (sys.stdout, sys.stderr):
+    try:
+        _stream.reconfigure(encoding="utf-8")  # type: ignore[attr-defined]
+    except (AttributeError, ValueError):
+        pass
+
+
+class Term:
+    """Tiny ANSI helper mirroring skills/_lib/term.sh (bash-only; per
+    TERMINAL-DESIGN.md §9 the Python port is inline). Honors FORCE_COLOR /
+    NO_COLOR / TERM_ASCII; ASCII glyph fallback on TERM_ASCII or a non-UTF stream."""
+
+    _C = {"green": "\033[32m", "yellow": "\033[33m", "orange": "\033[38;5;208m",
+          "red": "\033[31m", "cyan": "\033[36m", "dim": "\033[2m", "off": "\033[0m"}
+    _GLYPH = {"ok": "✓", "bad": "✗", "warn": "▲", "skip": "—", "na": "—", "unknown": "?"}
+    _ASCII = {"ok": "+", "bad": "x", "warn": "!", "skip": "-", "na": "-", "unknown": "?"}
+    _MARK_COLOR = {"ok": "green", "bad": "red", "warn": "orange", "skip": "dim",
+                   "na": "dim", "unknown": "yellow"}
+
+    def __init__(self, stream=sys.stderr):
+        enc = (getattr(stream, "encoding", "") or "").lower()
+        self.ascii = (os.environ.get("TERM_ASCII") == "1"
+                      or os.environ.get("FLEET_ASCII") == "1" or "utf" not in enc)
+        if os.environ.get("FORCE_COLOR"):
+            self.color = True
+        elif (os.environ.get("NO_COLOR") is not None or os.environ.get("TERM") == "dumb"
+              or not getattr(stream, "isatty", lambda: False)()):
+            self.color = False
+        else:
+            self.color = True
+
+    def c(self, name, text):
+        return f"{self._C.get(name, '')}{text}{self._C['off']}" if self.color else text
+
+    def mark(self, state):
+        return self.c(self._MARK_COLOR.get(state, ""),
+                      (self._ASCII if self.ascii else self._GLYPH).get(state, "."))
+
+    def hdr(self, text):
+        return self.c("cyan", f"=== {text} ===")
+
+
+TERM = Term(sys.stderr)
+
 SCHEMA = "claude-mods.claude-code-ops.hooks-lint/v1"
 
 EXIT_OK = 0
@@ -267,7 +313,7 @@ def main(argv):
         print("ERROR: %s" % msg, file=sys.stderr)
         return EXIT_MALFORMED
 
-    print("=== hooks-lint: %s ===" % path, file=sys.stderr)
+    print(TERM.hdr("hooks-lint: %s" % path), file=sys.stderr)
     findings = lint(doc)
 
     errors = [f for f in findings if f.severity == "error"]
@@ -286,12 +332,16 @@ def main(argv):
 
     # Human framing → stderr.
     for f in findings:
-        tag = "ERROR" if f.severity == "error" else "warn "
-        print("  [%s] %s: %s" % (tag, f.pointer or "/", f.message),
+        if f.severity == "error":
+            mk, tag = TERM.mark("bad"), TERM.c("red", "ERROR")
+        else:
+            mk, tag = TERM.mark("warn"), TERM.c("orange", "warn")
+        print("  %s %s %s: %s" % (mk, tag, f.pointer or "/", f.message),
               file=sys.stderr)
     if not findings:
-        print("  clean — no findings", file=sys.stderr)
-    print("--- %d error(s), %d warning(s) ---" % (len(errors), len(warnings)),
+        print("  %s clean, no findings" % TERM.mark("ok"), file=sys.stderr)
+    print("--- %s error(s), %s warning(s) ---"
+          % (TERM.c("red", str(len(errors))), TERM.c("orange", str(len(warnings)))),
           file=sys.stderr)
 
     if errors or (args.strict and warnings):

+ 51 - 2
skills/playwright-ops/scripts/triage-flakes.py

@@ -23,9 +23,56 @@
 
 import argparse
 import json
+import os
 import sys
 from pathlib import Path
 
+# Windows consoles default to cp1252; force UTF-8 so glyphs in framing don't raise
+# UnicodeEncodeError (the repo's standard fix).
+for _stream in (sys.stdout, sys.stderr):
+    try:
+        _stream.reconfigure(encoding="utf-8")  # type: ignore[attr-defined]
+    except (AttributeError, ValueError):
+        pass
+
+
+class Term:
+    """Tiny ANSI helper mirroring skills/_lib/term.sh (bash-only; per
+    TERMINAL-DESIGN.md §9 the Python port is inline). Honors FORCE_COLOR /
+    NO_COLOR / TERM_ASCII; ASCII glyph fallback on TERM_ASCII or a non-UTF stream."""
+
+    _C = {"green": "\033[32m", "yellow": "\033[33m", "orange": "\033[38;5;208m",
+          "red": "\033[31m", "cyan": "\033[36m", "dim": "\033[2m", "off": "\033[0m"}
+    _GLYPH = {"ok": "✓", "bad": "✗", "warn": "▲", "skip": "—", "na": "—", "unknown": "?"}
+    _ASCII = {"ok": "+", "bad": "x", "warn": "!", "skip": "-", "na": "-", "unknown": "?"}
+    _MARK_COLOR = {"ok": "green", "bad": "red", "warn": "orange", "skip": "dim",
+                   "na": "dim", "unknown": "yellow"}
+
+    def __init__(self, stream=sys.stderr):
+        enc = (getattr(stream, "encoding", "") or "").lower()
+        self.ascii = (os.environ.get("TERM_ASCII") == "1"
+                      or os.environ.get("FLEET_ASCII") == "1" or "utf" not in enc)
+        if os.environ.get("FORCE_COLOR"):
+            self.color = True
+        elif (os.environ.get("NO_COLOR") is not None or os.environ.get("TERM") == "dumb"
+              or not getattr(stream, "isatty", lambda: False)()):
+            self.color = False
+        else:
+            self.color = True
+
+    def c(self, name, text):
+        return f"{self._C.get(name, '')}{text}{self._C['off']}" if self.color else text
+
+    def mark(self, state):
+        return self.c(self._MARK_COLOR.get(state, ""),
+                      (self._ASCII if self.ascii else self._GLYPH).get(state, "."))
+
+    def hdr(self, text):
+        return self.c("cyan", f"=== {text} ===")
+
+
+TERM = Term(sys.stderr)
+
 SCHEMA = "claude-mods.playwright-ops.flake-triage/v1"
 
 EXIT_OK = 0
@@ -198,8 +245,10 @@ def main(argv=None):
     flaky_n = sum(1 for f in finds if f["outcome"] == "flaky")
     unexp_n = sum(1 for f in finds if f["outcome"] == "unexpected")
     if not args.quiet:
-        err(f"=== Flake triage: {path.name} ===")
-        err(f"  {total} tests | {flaky_n} flaky | {unexp_n} unexpected | showing {len(capped)} of {len(shown)}")
+        err(TERM.hdr(f"Flake triage: {path.name}"))
+        flaky_txt = TERM.c("orange", f"{flaky_n} flaky") if flaky_n else "0 flaky"
+        unexp_txt = TERM.c("red", f"{unexp_n} unexpected") if unexp_n else "0 unexpected"
+        err(f"  {total} tests | {flaky_txt} | {unexp_txt} | showing {len(capped)} of {len(shown)}")
 
     if args.json:
         envelope = {

+ 40 - 7
skills/terraform-ops/scripts/check-action-refs.sh

@@ -30,6 +30,12 @@ set -uo pipefail
 EXIT_OK=0; EXIT_USAGE=2; EXIT_NOT_FOUND=3; EXIT_MALFORMED=4
 EXIT_MISSING_DEP=5; EXIT_UNAVAILABLE=7; EXIT_DRIFT=10
 
+# Terminal design system (skills/_lib/term.sh). Framing rides stderr (term_init 2);
+# the findings list / --json stay plain on stdout. Degrade if the lib is gone.
+__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; __HAVE_TERM=1
+else __HAVE_TERM=0; TERM_DOT="|"; fi
+
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 DEFAULT_FILE="${SCRIPT_DIR}/../assets/github-actions-terraform.yml"
 
@@ -61,6 +67,22 @@ fi
 
 emit() { [[ "$QUIET" -eq 1 ]] && return; printf '%s\n' "$1" >&2; }
 
+# Panel framing applies to the human stderr stream when it's a TTY (or FORCE_COLOR
+# forces a render); piped/quiet consumers keep the legacy "=== / [TAG]" lines.
+PANEL=0
+if [[ "$__HAVE_TERM" -eq 1 && "$QUIET" -eq 0 ]] && { [ -t 2 ] || [ -n "${FORCE_COLOR:-}" ]; }; then PANEL=1; fi
+__PANEL_OPEN=0
+popen() {
+  [[ "$PANEL" -eq 1 && "$__PANEL_OPEN" -eq 0 ]] || return 0
+  { term_panel_open terraform "action-refs ${TERM_DOT} ${MODE}"; term_panel_vert; } >&2
+  __PANEL_OPEN=1
+}
+# prow <mark> <legacy-prefix> <text> — panel status row, or the legacy tagged line.
+prow() {
+  if [[ "$PANEL" -eq 1 ]]; then popen; term_status_row "$1" "$3" >&2
+  else emit "  $2 $3"; fi
+}
+
 # State accumulators
 malformed=0; drift=0; unavailable=0; warned=0
 declare -a JSON_OBJS=()
@@ -155,11 +177,11 @@ add_json() {  # file line ref status
     '{file:$f, line:$l, ref:$r, status:$s}')")
 }
 
-emit "=== check-action-refs (${MODE}) ==="
+if [[ "$PANEL" -eq 1 ]]; then popen; else emit "=== check-action-refs (${MODE}) ==="; fi
 
 for f in "${FILES[@]}"; do
   if [[ ! -f "$f" ]]; then
-    emit "ERROR: file not found: $f"
+    prow bad "ERROR:" "file not found: $f"
     # In JSON mode still report a structured error per §5
     if [[ "$JSON" -eq 1 ]]; then
       echo "{\"error\":{\"code\":\"NOT_FOUND\",\"message\":\"file not found: $f\"}}"
@@ -189,13 +211,13 @@ for f in "${FILES[@]}"; do
     case "$C_STATUS" in
       malformed)
         malformed=1
-        emit "  [MALFORMED] ${f}:${lineno}  uses: ${val}"
+        prow bad "[MALFORMED]" "${f}:${lineno}  uses: ${val}"
         TEXT_ROWS+=("${f}:${lineno}	${val}	malformed")
         add_json "$f" "$lineno" "$val" "malformed"
         ;;
       warn)
         warned=1
-        emit "  [WARN floating] ${f}:${lineno}  ${val}  (prefer SHA pin)"
+        prow warn "[WARN floating]" "${f}:${lineno}  ${val}  (prefer SHA pin)"
         TEXT_ROWS+=("${f}:${lineno}	${val}	warn")
         add_json "$f" "$lineno" "$val" "warn"
         ;;
@@ -204,17 +226,17 @@ for f in "${FILES[@]}"; do
           res=$(resolve_ref "$C_OWNER" "$C_REPO" "$C_REF")
           case "$res" in
             resolved)
-              emit "  [ok] ${f}:${lineno}  ${C_OWNER}/${C_REPO}@${C_REF}"
+              prow ok "[ok]" "${f}:${lineno}  ${C_OWNER}/${C_REPO}@${C_REF}"
               TEXT_ROWS+=("${f}:${lineno}	${val}	ok")
               add_json "$f" "$lineno" "$val" "ok" ;;
             notfound)
               drift=1
-              emit "  [DRIFT 404] ${f}:${lineno}  ${C_OWNER}/${C_REPO}@${C_REF}"
+              prow bad "[DRIFT 404]" "${f}:${lineno}  ${C_OWNER}/${C_REPO}@${C_REF}"
               TEXT_ROWS+=("${f}:${lineno}	${val}	drift")
               add_json "$f" "$lineno" "$val" "drift" ;;
             unavailable)
               unavailable=1
-              emit "  [unavailable] ${f}:${lineno}  ${C_OWNER}/${C_REPO}@${C_REF} (API unreachable/rate-limited)"
+              prow warn "[unavailable]" "${f}:${lineno}  ${C_OWNER}/${C_REPO}@${C_REF} (API unreachable/rate-limited)"
               TEXT_ROWS+=("${f}:${lineno}	${val}	unavailable")
               add_json "$f" "$lineno" "$val" "unavailable" ;;
           esac
@@ -227,6 +249,17 @@ for f in "${FILES[@]}"; do
   done < <(grep -nE '^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*' "$f" 2>/dev/null)
 done
 
+# --- panel footer (stderr framing only) ---------------------------------------
+if [[ "$PANEL" -eq 1 && "$__PANEL_OPEN" -eq 1 ]]; then
+  ph_state="healthy"; ph_text="refs well-formed"
+  if [[ "$malformed" -eq 1 || "$drift" -eq 1 ]]; then ph_state="critical"; ph_text="findings present"
+  elif [[ "$unavailable" -eq 1 ]]; then ph_state="warning"; ph_text="api unavailable"
+  elif [[ "$warned" -eq 1 ]]; then ph_state="warning"; ph_text="floating refs"; fi
+  { term_panel_vert
+    term_panel_close "--live to resolve ${TERM_DOT} --json for data" "$(term_health "$ph_state" "$ph_text")"
+  } >&2
+fi
+
 # --- output -------------------------------------------------------------------
 if [[ "$JSON" -eq 1 ]]; then
   printf '%s\n' "${JSON_OBJS[@]:-}" | jq -s \

+ 26 - 0
tests/check-resources.sh

@@ -66,6 +66,32 @@ bash -n skills/ffmpeg-ops/scripts/verify-commands.sh 2>/dev/null \
 bash -n skills/ytdlp-ops/scripts/check-ytdlp-version.sh 2>/dev/null \
     && pass "bash -n check-ytdlp-version.sh" || bad "bash -n check-ytdlp-version.sh"
 
+echo "== terminal design: verifier framing adopts term.sh and is ASCII-pure"
+# Each verifier renders its human framing on stderr; under TERM_ASCII=1 every
+# glyph must fall back to its registered ASCII proxy (design principle #3).
+purity() { # desc, cmd...
+    local desc="$1"; shift
+    local errout
+    errout="$(TERM_ASCII=1 FORCE_COLOR=1 "$@" 2>&1 1>/dev/null)"
+    if printf '%s' "$errout" | LC_ALL=C grep -q '[^[:print:][:cntrl:]]'; then
+        bad "$desc framing emits non-ASCII under TERM_ASCII=1"
+    else pass "$desc framing pure ASCII under TERM_ASCII=1"; fi
+}
+purity "action-refs" bash skills/terraform-ops/scripts/check-action-refs.sh --offline
+purity "model-table" "$PY" skills/claude-api-ops/scripts/check-model-table.py --offline
+purity "hooks-lint"  "$PY" skills/claude-code-ops/scripts/validate-hooks-json.py hooks/hooks.json
+__tf="$(mktemp)"; printf '{"suites":[]}' > "$__tf"
+purity "flake-triage" "$PY" skills/playwright-ops/scripts/triage-flakes.py "$__tf"
+rm -f "$__tf"
+grep -q '_lib/term.sh' skills/terraform-ops/scripts/check-action-refs.sh \
+    && pass "check-action-refs sources term.sh" || bad "check-action-refs missing term.sh"
+for s in skills/claude-api-ops/scripts/check-model-table.py \
+         skills/claude-code-ops/scripts/validate-hooks-json.py \
+         skills/playwright-ops/scripts/triage-flakes.py; do
+    grep -q 'class Term' "$s" && pass "$(basename "$s") carries inline Term" \
+        || bad "$(basename "$s") missing inline Term"
+done
+
 echo
 if [ "$fail" -eq 0 ]; then echo "resource checks: clean"; exit 0; fi
 echo "resource checks: failures above"; exit 1