From a5cb5d74c1ac5881e6129e33662c4aa75ebf2e17 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sun, 17 May 2026 07:23:49 -0700 Subject: [PATCH] =?UTF-8?q?feat(@scripts/session-tools):=20=E2=9C=A8=20upd?= =?UTF-8?q?ate=20priority=20convention=20to=20p0-p4=20triage=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- bin/_claude-triage | 16 +++-- bin/rclaude | 162 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 166 insertions(+), 12 deletions(-) diff --git a/bin/_claude-triage b/bin/_claude-triage index 17fe6cf..72dc1b6 100755 --- a/bin/_claude-triage +++ b/bin/_claude-triage @@ -142,7 +142,7 @@ SYSTEM_PROMPT = """You triage Claude Code coding sessions. For each session in t - ref_index: integer matching the input's ref_index - summary: ONE short sentence describing what's happening - status: one of done, in_progress, blocked, waiting_on_user, abandoned -- priority: integer 1-5 (5 = critical to resume now, 1 = abandonable) +- priority: integer 0-4 (0 = critical to resume now, 4 = abandonable). Follows the P0/P1 incident convention — LOWER number means HIGHER priority. - next_action: ONE short imperative phrase, or empty string if status is done/abandoned Output ONLY a JSON array. No markdown, no prose.""" @@ -217,7 +217,8 @@ async def main_async(args: argparse.Namespace) -> None: # the cache BEFORE reading the JSONL (the expensive part for a triage run # over hundreds of sessions where most are unchanged). cache = ResponseCache(CACHE_DIR) - template_id = "rclaude-triage-v1" + # v2: priority scale flipped to P0=critical / P4=abandonable convention. + template_id = "rclaude-triage-v2" cached_results: list[dict] = [] uncached: list[tuple[int, Path, str]] = [] for mtime, jsonl in candidates: @@ -288,14 +289,17 @@ async def main_async(args: argparse.Namespace) -> None: await client.close() results.extend(new_results) + # Lower priority number = higher importance (P0 convention). Sort + # ascending by priority, descending by mtime so the most-recent within + # each priority bucket floats up. def sort_key(r: dict) -> tuple[int, int]: try: - prio = int(r.get("priority", 0)) + prio = int(r.get("priority", 9)) except (TypeError, ValueError): - prio = 0 - return (prio, int(r.get("mtime", 0))) + prio = 9 + return (prio, -int(r.get("mtime", 0))) - for r in sorted(results, key=sort_key, reverse=True): + for r in sorted(results, key=sort_key): line = "\t".join( [ str(r.get("mtime", 0)), diff --git a/bin/rclaude b/bin/rclaude index 8ab6d31..d9cdfa4 100755 --- a/bin/rclaude +++ b/bin/rclaude @@ -326,6 +326,121 @@ _REMOTE_TRIAGE_BOOT='export PATH=$HOME/.local/bin:/opt/homebrew/bin:$PATH; '"$_P # work itself is already protected by tmux on the remote. _SSH_LIVE_OPTS='-o ServerAliveInterval=30 -o ServerAliveCountMax=6 -o TCPKeepAlive=yes' +# --------------------------------------------------------------------------- +# Setup / dependency auto-install +# --------------------------------------------------------------------------- + +# Cache dir for per-host setup markers. A marker file means we've already +# probed + installed deps on that host within RCLAUDE_SETUP_TTL days. +_SETUP_CACHE_DIR=${XDG_CACHE_HOME:-$HOME/.cache}/rclaude +_SETUP_TTL_DAYS=${RCLAUDE_SETUP_TTL:-7} + +# Detect package-manager family on . Output: macos | rhel | debian | unknown. +detect_os_on() { + _h=$1 + _probe='if [ "$(uname -s)" = "Darwin" ]; then echo macos + elif command -v dnf >/dev/null 2>&1; then echo rhel + elif command -v apt-get >/dev/null 2>&1; then echo debian + else echo unknown; fi' + if is_local "$_h"; then + sh -c "$_probe" 2>/dev/null + else + ssh -o BatchMode=yes -o ConnectTimeout=5 "$_h" "$_probe" 2>/dev/null + fi +} + +# Install on using its native package manager. Uses sudo for +# system pkgs on Linux; brew (no sudo) on macOS. +install_pkgs_on() { + _h=$1; _os=$2; shift 2 + _pkgs=$* + [ -z "$_pkgs" ] && return 0 + case $_os in + macos) _cmd="brew install $_pkgs" ;; + rhel) _cmd="sudo dnf install -y $_pkgs" ;; + debian) _cmd="sudo apt-get update && sudo apt-get install -y $_pkgs" ;; + *) + echo "rclaude: don't know how to install $_pkgs on $_h ($_os) — do it manually" >&2 + return 1 ;; + esac + printf 'rclaude: installing on %s: %s\n' "$_h" "$_pkgs" >&2 + if is_local "$_h"; then + sh -c "$_cmd" >&2 + else + ssh -t "$_h" "$_cmd" >&2 + fi +} + +# Run a probe command on , return its stdout. Used by setup_host. +_probe_on() { + _h=$1; _cmd=$2 + if is_local "$_h"; then sh -c "$_cmd" 2>/dev/null + else ssh -o BatchMode=yes -o ConnectTimeout=5 "$_h" "$_cmd" 2>/dev/null + fi +} + +# Idempotently install rclaude's deps on . Honors a per-host marker so +# we don't re-probe on every invocation. Pass `force` to bypass the marker. +setup_host() { + _h=$1; _force=${2:-} + mkdir -p "$_SETUP_CACHE_DIR" 2>/dev/null + _marker_id=$(printf %s "$_h" | tr -c 'A-Za-z0-9' '_') + _marker="$_SETUP_CACHE_DIR/setup-$_marker_id" + if [ "$_force" != "force" ] && [ -f "$_marker" ]; then + # Marker exists and is recent enough → assume deps are fine. + if [ -z "$(find "$_marker" -mtime +"$_SETUP_TTL_DAYS" 2>/dev/null)" ]; then + return 0 + fi + fi + _os=$(detect_os_on "$_h") + if [ "$_os" = "unknown" ] || [ -z "$_os" ]; then + echo "rclaude: couldn't detect OS on $_h; skipping setup" >&2 + return 0 + fi + # Probe system binaries. + _missing="" + _have=$(_probe_on "$_h" 'for c in tmux rsync mosh; do command -v "$c" >/dev/null 2>&1 && echo "$c"; done') + for c in tmux rsync mosh; do + case " $_have " in *" $c "*) ;; *) _missing="$_missing $c" ;; esac + done + if [ -n "$_missing" ]; then + install_pkgs_on "$_h" "$_os" $_missing || true + fi + # Python SDK for triage. Try to install per-user without sudo. + _has_sdk=$(_probe_on "$_h" 'for p in python3.13 python3.12 python3.11 python3; do b=$(command -v "$p" 2>/dev/null) || continue; "$b" -c "import claude_code_batch_sdk" 2>/dev/null && echo "$b" && break; done') + if [ -z "$_has_sdk" ]; then + _pick=$(_probe_on "$_h" 'for p in python3.12 python3.11 python3; do command -v "$p" 2>/dev/null && break; done | head -1') + if [ -n "$_pick" ]; then + printf 'rclaude: installing claude-code-batch-sdk via %s on %s\n' "$_pick" "$_h" >&2 + if is_local "$_h"; then + "$_pick" -m pip install --user --quiet claude-code-batch-sdk >&2 || true + else + ssh "$_h" "$_pick -m pip install --user --quiet claude-code-batch-sdk" >&2 || true + fi + fi + fi + touch "$_marker" 2>/dev/null +} + +# Prefer mosh when available on both ends — unless explicitly disabled via +# RCLAUDE_TRANSPORT=ssh. Echoes "mosh" or "ssh". Caches result per host. +pick_transport() { + _h=$1 + case ${RCLAUDE_TRANSPORT:-auto} in + ssh) echo ssh; return ;; + mosh) echo mosh; return ;; + esac + if ! command -v mosh >/dev/null 2>&1; then echo ssh; return; fi + _cache="/tmp/rclaude-transport.$(whoami).$(printf %s "$_h" | tr -c 'A-Za-z0-9' '_')" + if [ -s "$_cache" ]; then cat "$_cache"; return; fi + if _probe_on "$_h" 'command -v mosh-server >/dev/null 2>&1' | grep -q . \ + || _probe_on "$_h" 'command -v mosh-server' >/dev/null; then + echo mosh > "$_cache"; echo mosh + else + echo ssh > "$_cache"; echo ssh + fi +} + # Run the triage helper on with the supplied extra args. Stdout is the # raw TSV emitted by _claude-triage (one row per session). list_triage_on() { @@ -488,7 +603,7 @@ cmd_resume() { # Re-sort globally by (priority desc, mtime desc) so the top of the # picker is the actual highest priority across the fleet. _triage=$(scan_hosts | while IFS= read -r h; do list_triage_on "$h"; done \ - | sort -t"$(printf '\t')" -k4,4nr -k9,9nr) + | sort -t"$(printf '\t')" -k4,4n -k9,9nr) _t_count=0 [ -n "$_tmux" ] && _t_count=$(printf '%s\n' "$_tmux" | wc -l | tr -d ' ') [ -n "$_triage" ] && _d_total=$(printf '%s\n' "$_triage" | wc -l | tr -d ' ') @@ -566,6 +681,9 @@ cmd_resume() { _R=$(printf '\033[0m') _Chost=$(printf '\033[36m'); _Ctmux=$(printf '\033[1;32m') _Cdim=$(printf '\033[2m'); _Ckey=$(printf '\033[1;35m') + # Triage uses P0=critical / P4=abandonable (P0 incident convention). + # _Cp5 / _Cp4 names are kept for diff size; what they color is the + # *top two* priority levels, whatever the scale is. _Cp5=$(printf '\033[1;31m'); _Cp4=$(printf '\033[33m') _Cblk=$(printf '\033[31m'); _Cwait=$(printf '\033[33m') _Cinp=$(printf '\033[36m'); _Cdone=$(printf '\033[32m') @@ -577,7 +695,7 @@ cmd_resume() { # same kind. Kind is now encoded by row shape: tmux has a ▶ marker, # triage shows P + status, session shows the snippet plain. _fmt_row=' - function prio_c(p) { if (p=="5") return c_p5; if (p=="4") return c_p4; return "" } + function prio_c(p) { if (p=="0") return c_p5; if (p=="1") return c_p4; return "" } function stat_c(s) { if (s=="blocked") return c_blk if (s=="waiting_on_user") return c_wait @@ -700,6 +818,10 @@ cmd_resume() { if is_local "$_host"; then exec tmux attach -t "$_target" else + setup_host "$_host" + if [ "$(pick_transport "$_host")" = "mosh" ]; then + exec mosh "$_host" -- tmux attach -t "$_target" + fi exec ssh -t $_SSH_LIVE_OPTS "$_host" tmux attach -t "$_target" fi ;; @@ -765,11 +887,33 @@ cmd_version() { fi } +cmd_setup() { + # Args: + # (none) → install on every host in scan_hosts + # [...] → install on each named host + # --on → install on a single host (parity with `resume --on`) + _hosts="" + while [ $# -gt 0 ]; do + case $1 in + --on) shift; _hosts="$_hosts $1"; shift ;; + --on=*) _hosts="$_hosts ${1#--on=}"; shift ;; + *) _hosts="$_hosts $1"; shift ;; + esac + done + if [ -z "$_hosts" ]; then + scan_hosts | while IFS= read -r h; do setup_host "$h" force; done + else + for h in $_hosts; do setup_host "$h" force; done + fi +} + case ${1:-} in - list) shift; cmd_list "$@"; exit ;; - resume) shift; cmd_resume "$@"; exit ;; - triage) shift; cmd_triage "$@"; exit ;; - -v|--version) cmd_version; exit ;; + list) shift; cmd_list "$@"; exit ;; + resume) shift; cmd_resume "$@"; exit ;; + triage) shift; cmd_triage "$@"; exit ;; + setup) shift; cmd_setup "$@"; exit ;; + -v|--version) cmd_version; exit ;; + -h|--help|help) cmd_help; exit ;; esac # --------------------------------------------------------------------------- @@ -912,10 +1056,16 @@ if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$host" "test -d ${dir}" 2>/dev/nu fi fi +setup_host "$host" sync_tmux_conf "$host" inner=$(build_inner "$dir") # `new-session -A` attaches if a session of that name already exists, so # re-running rclaude after a broken pipe lands you back in the same tmux # session instead of erroring with "duplicate session". Combined with # _SSH_LIVE_OPTS this tolerates short network drops without losing work. +# Mosh is preferred when available (handles sleep/roam/long blips natively); +# falls back to ssh+keepalives otherwise. +if [ "$(pick_transport "$host")" = "mosh" ]; then + exec mosh "$host" -- tmux new-session -A -s "${session}" "${inner}" +fi exec ssh -t $_SSH_LIVE_OPTS "$host" "tmux new-session -A -s '${session}' \"${inner}\""