From 3d1c854f41644668a2fc5780d79a6084de82b007 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 11:41:47 +0100 Subject: [PATCH] Adopt the health shell scripts; worktree_status becomes a /health leg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/{health,health_sync,health_release,health_audit}.sh move here from PyAutoMind/scripts — the Heart owns the health surface; a health dashboard living in the intent organ was a boundary leak. Mind keeps forwarding shims. - skills/worktree_status/SKILL.md -> reference.md: retired as a standalone installed skill; it is now the procedure behind the new `/health worktrees` leg (same pattern as the pyauto-status legs). Co-Authored-By: Claude Fable 5 --- scripts/health.sh | 49 +++ scripts/health_audit.sh | 113 +++++++ scripts/health_release.sh | 215 ++++++++++++ scripts/health_sync.sh | 312 ++++++++++++++++++ .../{SKILL.md => reference.md} | 7 +- 5 files changed, 691 insertions(+), 5 deletions(-) create mode 100644 scripts/health.sh create mode 100644 scripts/health_audit.sh create mode 100755 scripts/health_release.sh create mode 100644 scripts/health_sync.sh rename skills/worktree_status/{SKILL.md => reference.md} (95%) diff --git a/scripts/health.sh b/scripts/health.sh new file mode 100644 index 0000000..376fe8f --- /dev/null +++ b/scripts/health.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# health.sh — the `health` shell dispatcher. +# +# Front door for the local health/dev shell tools, mirroring the Claude `/health` +# command door. Routes to the implementations defined in the sibling scripts +# (source all four via ~/.bashrc). Lives in PyAutoHeart — the Heart owns the +# health surface; PyAutoMind/scripts keeps forwarding shims for old sourcing +# paths: +# +# health cross-repo git-sync dashboard (health_sync.sh -> _health_sync) +# health sync explicit alias of the above +# health release last release-prep run dashboard (health_release.sh -> _health_release) +# health audit structural repo-health audit (health_audit.sh -> _health_audit) +# health help this usage +# +# Distinct from the `pyauto-heart` binary (the Heart organ CLI) — this is the +# local shell convenience layer. Any argument after the subcommand is passed +# through (e.g. `health release `). + +health() { + local sub="${1:-sync}" + # Consume the subcommand token only if one was actually given, so bare + # `health` (defaults to sync) and `health ` both pass the + # remaining "$@" straight through to the implementation. + [ "$#" -gt 0 ] && shift + case "$sub" in + sync) _health_sync "$@" ;; + release) _health_release "$@" ;; + audit) _health_audit "$@" ;; + -h|--help|help) + cat <<'EOF' +health — local health/dev shell dispatcher (mirrors the Claude /health door). + +Usage: health [sync|release|audit] + + health cross-repo git-sync dashboard (branch, behind/ahead, dirty) + health sync same as bare `health` + health release last PyAutoBuild release-prep run dashboard + health audit structural repo-health audit (non-repo dirs, stashes, dead branches) + +Release-run helpers: health-report / health-json / health-triage. +EOF + ;; + *) + echo "health: unknown subcommand '$sub' (try: health help)" >&2 + return 2 + ;; + esac +} diff --git a/scripts/health_audit.sh b/scripts/health_audit.sh new file mode 100644 index 0000000..4215f83 --- /dev/null +++ b/scripts/health_audit.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# health_audit.sh — on-demand structural repo-health audit. +# +# Defines `_health_audit` (run via `health audit`) that scans ~/Code/PyAutoLabs/ +# for state the git-sync dashboard (`health` / `health sync`) doesn't surface: +# +# 1. Top-level directories with no .git (intentionally-not-a-repo or bug). +# Skip prefixes "." (hidden) and "z_" (user's personal/staging convention). +# 2. Stashes older than $PYAUTO_AUDIT_STASH_DAYS (default 14) — drift-from- +# stash is a real failure mode. +# 3. Local-only branches with no upstream and last commit older than +# $PYAUTO_AUDIT_BRANCH_DAYS (default 30) — likely abandoned work. +# +# Run on demand. Always exits 0 — informational, the user reads + decides. +# +# Usage (normally sourced via ~/.bashrc, run through the `health` dispatcher): +# source ~/Code/PyAutoLabs/PyAutoMind/scripts/health_audit.sh +# health audit +# +# Override via env vars: +# PYAUTO_AUDIT_ROOT scan root (default $HOME/Code/PyAutoLabs) +# PYAUTO_AUDIT_STASH_DAYS stash age threshold in days (default 14) +# PYAUTO_AUDIT_BRANCH_DAYS branch age threshold in days (default 30) + +PYAUTO_AUDIT_ROOT="${PYAUTO_AUDIT_ROOT:-$HOME/Code/PyAutoLabs}" +PYAUTO_AUDIT_STASH_DAYS="${PYAUTO_AUDIT_STASH_DAYS:-14}" +PYAUTO_AUDIT_BRANCH_DAYS="${PYAUTO_AUDIT_BRANCH_DAYS:-30}" + +_health_audit() { + local root="$PYAUTO_AUDIT_ROOT" + if [[ ! -d "$root" ]]; then + echo "health audit: $root does not exist" >&2 + return 1 + fi + + local now stash_thresh branch_thresh + now=$(date +%s) + stash_thresh=$(( PYAUTO_AUDIT_STASH_DAYS * 86400 )) + branch_thresh=$(( PYAUTO_AUDIT_BRANCH_DAYS * 86400 )) + + # Section 1: non-git directories. Skip prefixes are hardcoded — if the + # legitimate exceptions ever exceed 3-4 patterns, switch to a snooze file. + local skip_prefixes=("." "z_") + local non_git=() dir name skip prefix + for dir in "$root"/*/; do + [[ ! -d "$dir" ]] && continue + name="$(basename "$dir")" + skip=false + for prefix in "${skip_prefixes[@]}"; do + [[ "$name" == "$prefix"* ]] && skip=true && break + done + [[ "$skip" == "true" ]] && continue + [[ -e "$dir.git" ]] && continue + non_git+=("$name") + done + + # Section 2: old stashes. `--format='%gd|%ad|%ct|%s'` gives the stash ref, + # short date, commit timestamp, and subject in one line per entry. + local stash_lines=() repo line ref short_date ts subj age + for dir in "$root"/*/.git; do + [[ -e "$dir" ]] || continue + repo="${dir%/.git}" + name="$(basename "$repo")" + while IFS='|' read -r ref short_date ts subj; do + [[ -z "$ts" ]] && continue + age=$(( now - ts )) + (( age < stash_thresh )) && continue + stash_lines+=("$name: $ref ($short_date) $subj") + done < <(git -C "$repo" stash list --date=short --format='%gd|%ad|%ct|%s' 2>/dev/null) + done + + # Section 3: abandoned local-only branches. `for-each-ref` with empty + # `%(upstream)` filters local-only branches, then we age-filter on commit + # timestamp. Pipe delimiter (not tab) because bash treats consecutive + # whitespace IFS chars as one separator, which would collapse the empty + # upstream column into the timestamp. + local branch_lines=() branch upstream branch_ts iso + for dir in "$root"/*/.git; do + [[ -e "$dir" ]] || continue + repo="${dir%/.git}" + name="$(basename "$repo")" + while IFS='|' read -r branch upstream branch_ts; do + [[ -z "$branch" ]] && continue + [[ -n "$upstream" ]] && continue + age=$(( now - branch_ts )) + (( age < branch_thresh )) && continue + iso=$(date -d "@$branch_ts" +%Y-%m-%d 2>/dev/null) + branch_lines+=("$name: $branch (last: $iso)") + done < <(git -C "$repo" for-each-ref --format='%(refname:short)|%(upstream)|%(committerdate:unix)' refs/heads/ 2>/dev/null) + done + + # Output. Sections suppressed when empty; each prints its own header. + local printed=false + if (( ${#non_git[@]} > 0 )); then + echo "Non-git directories under $root:" + for name in "${non_git[@]}"; do echo " $name/"; done + printed=true + fi + if (( ${#stash_lines[@]} > 0 )); then + [[ "$printed" == "true" ]] && echo "" + echo "Old stashes (>$PYAUTO_AUDIT_STASH_DAYS days):" + for line in "${stash_lines[@]}"; do echo " $line"; done + printed=true + fi + if (( ${#branch_lines[@]} > 0 )); then + [[ "$printed" == "true" ]] && echo "" + echo "Abandoned local-only branches (no upstream, last commit >$PYAUTO_AUDIT_BRANCH_DAYS days):" + for line in "${branch_lines[@]}"; do echo " $line"; done + printed=true + fi + [[ "$printed" == "false" ]] && echo "health audit: clean (no findings under $root)" + return 0 +} diff --git a/scripts/health_release.sh b/scripts/health_release.sh new file mode 100755 index 0000000..34dc804 --- /dev/null +++ b/scripts/health_release.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# health_release.sh — release-prep run dashboard. +# +# Defines `_health_release` (run via `health release`) that reads the latest +# PyAutoBuild full release-prep run (the one symlinked from +# PyAutoBuild/test_results/latest/) and prints a dashboard: +# +# - Run timestamp + path + ready/not-ready verdict + total duration +# - Per-workspace pass / fail / skipped / timeout / duration table +# - Failure counts grouped by classification +# - Top-25 slowest scripts (any status) — surfaces timing regressions +# before they cross the timeout threshold +# - Slow-skip / needs-fix banner counts +# - Pointer to triage.md if present (free-form analytical clustering) +# +# Usage (normally sourced via ~/.bashrc, run through the `health` dispatcher): +# source ~/Code/PyAutoLabs/PyAutoMind/scripts/health_release.sh +# health release +# +# Override the run path (e.g. to inspect a specific historical run) by passing +# it as the first argument: +# health release ~/Code/PyAutoLabs/PyAutoBuild/test_results/runs/2026-04-29T14-48-47Z +# +# Sibling helpers: health-report / health-json / health-triage open the last +# run's report.md / report.json / triage.md. +# +# Note: distinct from the Claude `/health full` command (the conversational +# layer over the same run artefacts). This function prints straight to stdout, +# no Claude needed. + +PYAUTO_STATUS_FULL_DEFAULT="${PYAUTO_STATUS_FULL_DEFAULT:-$HOME/Code/PyAutoLabs/PyAutoBuild/test_results/latest}" + +_health_release() { + local run_dir="${1:-$PYAUTO_STATUS_FULL_DEFAULT}" + + if [[ ! -e "$run_dir" ]]; then + cat >&2 <&2 + return 1 + fi + + python3 - "$run_dir" <<'PY' +import json +import sys +from pathlib import Path + +run_dir = Path(sys.argv[1]) +with open(run_dir / "report.json") as f: + r = json.load(f) + +ready = r.get("ready") +total = float(r.get("total_duration_seconds", 0.0) or 0.0) +summary = r.get("summary", {}) or {} +n_pass = summary.get("passed", 0) +n_fail = summary.get("failed", 0) +n_skip = summary.get("skipped", 0) +n_to = summary.get("timeout", 0) + +GREEN = "\033[32m" +RED = "\033[31m" +YEL = "\033[33m" +DIM = "\033[2m" +RST = "\033[0m" + +verdict = f"{GREEN}READY{RST}" if ready else f"{RED}NOT READY{RST}" +print(f"{'=' * 76}") +print(f" PyAuto Status Full") +print(f"{'=' * 76}") +print(f"Run: {r.get('run_label','')}") +print(f"Path: {run_dir}") +print(f"Status: {verdict} (passed: {n_pass}, failed: {n_fail}, skipped: {n_skip}, timeout: {n_to})") +print(f"Total: {total:.1f}s ({total/60:.1f} min)") +print() + +# Per-workspace +print("Per-workspace") +print("-" * 76) +print(f"{'Workspace':<22} {'Passed':>6} {'Failed':>6} {'Skipped':>7} {'Timeout':>7} {'Duration':>10}") +pp = r.get("per_project", {}) or {} +ppd = r.get("per_project_duration_seconds", {}) or {} +for proj in sorted(pp.keys()): + c = pp[proj] + f = c.get("failed", 0) + t = c.get("timeout", 0) + color = GREEN if (f == 0 and t == 0) else RED + print( + f"{color}{proj:<22}{RST} " + f"{c.get('passed',0):>6} {f:>6} " + f"{c.get('skipped',0):>7} {t:>7} " + f"{ppd.get(proj,0):>9.1f}s" + ) +print() + +# Failures by classification +failures = r.get("failures", []) or [] +if failures: + by_class = {} + for fr in failures: + cls = fr.get("classification", "unknown") + by_class.setdefault(cls, []).append(fr) + labels = { + "source_code_bug": "Source code bugs", + "workspace_issue": "Workspace issues", + "workspace_data": "Missing data files", + "environment": "Environment issues", + "timeout": "Timeouts", + "known_numerical": "Known numerical", + "unknown": "Unclassified", + } + print(f"Failures by classification ({len(failures)} total)") + print("-" * 76) + for cls in sorted(by_class.keys(), key=lambda c: -len(by_class[c])): + items = by_class[cls] + print(f" {labels.get(cls, cls):<22} {len(items)}") + print() + +# Slowest 25 +slowest = r.get("slowest", []) or [] +if slowest: + print(f"Slowest {len(slowest)} scripts") + print("-" * 76) + print(f"{'Duration':>9} {'Status':<8} {'Project':<16} Script") + for s in slowest: + proj = s.get("project", "") + stat = s.get("status", "") + fil = s.get("file", "") + # Trim absolute paths to last 3 segments for readability. + short = "/".join(fil.split("/")[-3:]) + dur = float(s.get("duration_seconds", 0.0) or 0.0) + color = RED if stat in ("failed", "timeout") else (YEL if dur > 180 else "") + print(f"{color}{dur:>8.1f}s {stat:<8} {proj:<16} {short}{RST}") + print() + +# Parked scripts banners +slow_skips = r.get("slow_skips") or [] +nf_skips = r.get("needs_fix_skips") or [] +if slow_skips or nf_skips: + print("Parked scripts (workspace no_run.yaml banners)") + print("-" * 76) + if slow_skips: + print(f" SLOW skips: {len(slow_skips)} (need performance fix)") + if nf_skips: + print(f" NEEDS_FIX skips: {len(nf_skips)} (parked broken)") + print() + +# Pointers +print("Pointers") +print("-" * 76) +print(f" Markdown report: {run_dir}/report.md {DIM}(health-report){RST}") +print(f" Run JSON: {run_dir}/report.json {DIM}(health-json){RST}") +triage = run_dir / "triage.md" +if triage.exists(): + print(f" {GREEN}Triage notes: {triage}{RST} {DIM}(health-triage){RST}") +PY +} + +# _pyauto_run_file [run-dir-arg] — resolve a file inside the latest +# (or supplied) run directory. Used by the pyauto-{report,json,triage} viewers. +_pyauto_run_file() { + local subpath="$1" + local run_dir="${2:-$PYAUTO_STATUS_FULL_DEFAULT}" + + if [[ ! -e "$run_dir" ]]; then + echo "pyauto: no run found at $run_dir" >&2 + return 1 + fi + run_dir="$(readlink -f "$run_dir")" + + local target="$run_dir/$subpath" + if [[ ! -f "$target" ]]; then + echo "pyauto: $target missing" >&2 + return 1 + fi + printf '%s' "$target" +} + +# health-report [run-dir] — view report.md in the pager. +health-report() { + local f + f="$(_pyauto_run_file report.md "$1")" || return 1 + "${PAGER:-less}" "$f" +} + +# health-json [run-dir] — view report.json. Uses jq for color + paging when +# available, falls back to plain cat otherwise. +health-json() { + local f + f="$(_pyauto_run_file report.json "$1")" || return 1 + if command -v jq >/dev/null 2>&1; then + jq -C . "$f" | "${PAGER:-less}" -R + else + "${PAGER:-less}" "$f" + fi +} + +# health-triage [run-dir] — view triage.md in the pager. +health-triage() { + local f + f="$(_pyauto_run_file triage.md "$1")" || return 1 + "${PAGER:-less}" "$f" +} diff --git a/scripts/health_sync.sh b/scripts/health_sync.sh new file mode 100644 index 0000000..51a5cfd --- /dev/null +++ b/scripts/health_sync.sh @@ -0,0 +1,312 @@ +#!/usr/bin/env bash +# health_sync.sh — cross-repo git-sync dashboard. +# +# Defines `_health_sync` (run via the `health` dispatcher: bare `health` or +# `health sync`) that prints, for every git repo under ~/Code/PyAutoLabs/, the +# branch, upstream tracking ref, behind/ahead counts vs @{u}, dirty file count, +# and a flag column. Designed to run in under 10 seconds — fetches are +# parallelised one background job per repo. +# +# Usage (normally sourced via ~/.bashrc, run through the `health` dispatcher): +# source ~/Code/PyAutoLabs/PyAutoMind/scripts/health_sync.sh +# health # (or: health sync) +# +# Override the repo root (e.g. for testing) via PYAUTO_STATUS_ROOT. +# +# Flag glyphs (FLAGS column): +# ↓ behind upstream +# ↑ ahead of upstream +# * dirty (modified or untracked files) +# ! no upstream / fetch failed +# b current branch ≠ upstream branch (forgotten feature branch) +# +# After the main table, four optional sections may follow: +# - "Dirty files:" — per-repo `git status --porcelain` for any repo +# with mod or untr > 0. +# - "Follow-up commands:" — copy-pasteable git invocations grouped by +# category (pull / set-upstream / investigate). +# Suppressed entirely when nothing is actionable. +# - "Smoke tests:" — per-workspace counts from +# ~/.cache/pyauto/smoke/*.json (written by the +# /smoke-test skill). Green when failed=0, red +# otherwise. Suppressed when no JSONs exist. +# - "Last autobuild run:" — aggregate from +# ~/Code/PyAutoLabs/PyAutoBuild/test_results/*.json +# (committed by the autobuild release pipeline). +# Suppressed when no JSONs exist. +# +# Note: distinct from the Claude `/health status` command (the active-work +# registry dashboard). This shell command shows git *sync* state across repos; +# `/health status` shows planned / active / complete tasks. Different views, +# both under the "health" vocabulary. + +PYAUTO_STATUS_ROOT="${PYAUTO_STATUS_ROOT:-$HOME/Code/PyAutoLabs}" + +_health_sync() { + local root="$PYAUTO_STATUS_ROOT" + if [[ ! -d "$root" ]]; then + echo "health sync: $root does not exist" >&2 + return 1 + fi + + # Discover repos. `.git` is a directory in normal checkouts and a file in + # linked worktrees, so accept both. mindepth/maxdepth 2 limits us to the + # immediate children of $root. + local repos=() + while IFS= read -r dir; do + repos+=("$dir") + done < <( + find "$root" -mindepth 2 -maxdepth 2 \ + \( -name .git -type d -o -name .git -type f \) \ + -printf '%h\n' 2>/dev/null | sort + ) + + if [[ ${#repos[@]} -eq 0 ]]; then + echo "health sync: no git repos found under $root" + return 0 + fi + + # Parallel fetch. One background job per repo; sentinel files mark fetch + # failures so the dashboard can flag stale rows with `!` instead of + # silently returning misleading counts. + local fetch_status_dir + fetch_status_dir="$(mktemp -d)" + trap 'rm -rf "$fetch_status_dir"' RETURN + + # Run inside a subshell with monitor mode disabled AND its stderr closed + # so the interactive shell's job-control notifications (`[N] PID` / + # `[N] Done ...`) cannot leak into the dashboard output. `set +m` alone + # is not reliable across all bash configurations, so the `2>/dev/null` + # on the closing `)` is the belt-and-suspenders guarantee — bash writes + # job-control lines to fd 2. Per-repo fetch failures are still surfaced + # via the sentinel files in $fetch_status_dir. + local repo + ( + set +m + for repo in "${repos[@]}"; do + ( + if ! git -C "$repo" fetch --quiet origin 2>/dev/null; then + touch "$fetch_status_dir/$(basename "$repo")" + fi + ) & + done + wait + ) 2>/dev/null + + # Header. + local fmt='%-32s %-30s %-36s %6s %5s %4s %4s %s\n' + printf "$fmt" REPO BRANCH UPSTREAM BEHIND AHEAD MOD UNTR FLAGS + printf "$fmt" "--------------------------------" \ + "------------------------------" \ + "------------------------------------" \ + "------" "-----" "----" "----" "-----" + + # Per-repo row. Porcelain is cached so the dirty-files listing below can + # reuse it without a second `git status` per repo. Action arrays collect + # actionable follow-ups for the "Follow-up commands:" section printed at + # the end. + declare -A repo_porcelain + local actions_pull=() actions_set_upstream=() actions_manual=() + local name branch upstream upstream_branch behind ahead mod untr flags counts porcelain branch_mismatch b_int a_int + for repo in "${repos[@]}"; do + name="$(basename "$repo")" + + branch="$(git -C "$repo" rev-parse --abbrev-ref HEAD 2>/dev/null)" + [[ "$branch" == "HEAD" ]] && branch="(detached)" + [[ -z "$branch" ]] && branch="?" + + upstream="$(git -C "$repo" rev-parse --abbrev-ref '@{u}' 2>/dev/null || true)" + flags="" + + if [[ -z "$upstream" ]]; then + upstream="NONE" + upstream_branch="" + behind="?" + ahead="?" + flags+="!" + else + # Strip the remote prefix (e.g. "origin/main" → "main"). Branch names + # may contain slashes (e.g. "feature/foo"), so #*/ is the right + # operator — it removes only up to the first slash. + upstream_branch="${upstream#*/}" + counts="$(git -C "$repo" rev-list --left-right --count "$upstream"...HEAD 2>/dev/null || true)" + if [[ -n "$counts" ]]; then + behind="${counts%%[[:space:]]*}" + ahead="${counts##*[[:space:]]}" + else + behind="?" + ahead="?" + fi + [[ -e "$fetch_status_dir/$name" ]] && flags+="!" + fi + + porcelain="$(git -C "$repo" status --porcelain 2>/dev/null || true)" + repo_porcelain["$name"]="$porcelain" + + if [[ -z "$porcelain" ]]; then + mod=0 + untr=0 + else + untr="$(printf '%s\n' "$porcelain" | grep -c '^??' || true)" + mod="$(printf '%s\n' "$porcelain" | grep -cv '^??' || true)" + fi + + # Branch-mismatch detection. With no upstream, the heuristic is + # "expected to be on main"; with an upstream, compare to its branch + # component. Detached HEAD never matches. + branch_mismatch=false + if [[ "$upstream" == "NONE" ]]; then + [[ "$branch" != "main" ]] && branch_mismatch=true + elif [[ "$branch" != "$upstream_branch" ]]; then + branch_mismatch=true + fi + + [[ "$behind" =~ ^[0-9]+$ ]] && (( behind > 0 )) && flags+="↓" + [[ "$ahead" =~ ^[0-9]+$ ]] && (( ahead > 0 )) && flags+="↑" + (( mod + untr > 0 )) && flags+="*" + [[ "$branch_mismatch" == "true" ]] && flags+="b" + + printf "$fmt" "$name" "$branch" "$upstream" "$behind" "$ahead" "$mod" "$untr" "$flags" + + # Categorise actionable follow-ups. Only the boring case (clean, behind, + # not ahead) becomes an auto-runnable command; everything else is + # surfaced for manual handling. + b_int=0; a_int=0 + [[ "$behind" =~ ^[0-9]+$ ]] && b_int="$behind" + [[ "$ahead" =~ ^[0-9]+$ ]] && a_int="$ahead" + if [[ "$upstream" == "NONE" ]]; then + if [[ "$branch" == "main" ]]; then + actions_set_upstream+=("$repo") + else + actions_manual+=("$name — branch=$branch, upstream=NONE; switch to main or set upstream") + fi + elif (( b_int > 0 && a_int == 0 )); then + if (( mod + untr == 0 )); then + actions_pull+=("$repo") + else + actions_manual+=("$name — behind=$b_int, dirty (mod=$mod untr=$untr); stash + pull manually") + fi + elif (( b_int > 0 && a_int > 0 )); then + actions_manual+=("$name — diverged: ahead=$a_int, behind=$b_int; investigate") + elif [[ "$branch_mismatch" == "true" ]]; then + actions_manual+=("$name — on branch $branch (upstream $upstream_branch); switch to $upstream_branch if not a worktree") + fi + done + + # Per-repo dirty-file listing. Only repos with non-empty porcelain are + # shown — keeps the output empty when everything is clean. The `??` and + # ` M` etc. prefixes from porcelain are preserved so users can tell + # untracked from modified at a glance. + local printed_header=false + for repo in "${repos[@]}"; do + name="$(basename "$repo")" + porcelain="${repo_porcelain[$name]}" + [[ -z "$porcelain" ]] && continue + if [[ "$printed_header" == "false" ]]; then + echo "" + echo "Dirty files:" + printed_header=true + fi + echo " $name:" + printf '%s\n' "$porcelain" | sed 's/^/ /' + done + + # Follow-up commands. Suppressed entirely when nothing is actionable so + # the clean case stays quiet. The `git -C ` form means each + # printed line is independently copy-pasteable. + local total=$(( ${#actions_pull[@]} + ${#actions_set_upstream[@]} + ${#actions_manual[@]} )) + if (( total > 0 )); then + echo "" + echo "Follow-up commands:" + if (( ${#actions_pull[@]} > 0 )); then + echo " # Pull (clean, behind, not ahead):" + local r + for r in "${actions_pull[@]}"; do + echo " git -C $r pull --ff-only" + done + fi + if (( ${#actions_set_upstream[@]} > 0 )); then + echo " # Set missing upstream (branch=main, upstream=NONE):" + local r + for r in "${actions_set_upstream[@]}"; do + echo " git -C $r branch --set-upstream-to=origin/main main" + done + fi + if (( ${#actions_manual[@]} > 0 )); then + echo " # Investigate manually:" + local line + for line in "${actions_manual[@]}"; do + echo " $line" + done + fi + fi + + # Smoke tests. Reads per-workspace JSON written by the /smoke-test skill + # (admin_jammy/skills/smoke_test/SKILL.md step 7). One python invocation + # parses all files; bash formats with ANSI color (green if failed=0). + local smoke_dir="$HOME/.cache/pyauto/smoke" + if [[ -d "$smoke_dir" ]] && compgen -G "$smoke_dir/*.json" > /dev/null; then + echo "" + echo "Smoke tests:" + local ws ts passed failed skipped total dur color symbol + while IFS='|' read -r ws ts passed failed skipped total dur; do + [[ -z "$ws" ]] && continue + if [[ "$failed" == "0" ]]; then + color='\033[32m'; symbol='✓' + else + color='\033[31m'; symbol='✗' + fi + printf " ${color}%-32s %3s passed %3s failed %3s skipped (%s) %s\033[0m\n" \ + "$ws" "$passed" "$failed" "$skipped" "${ts:0:10}" "$symbol" + done < <(python3 -c ' +import json, os, glob +for f in sorted(glob.glob(os.path.expanduser("~/.cache/pyauto/smoke/*.json"))): + try: + d = json.load(open(f)) + print("|".join(str(d.get(k, "")) for k in + ["workspace", "completed_at", "passed", "failed", "skipped", "total", "duration_seconds"])) + except Exception: + pass +' 2>/dev/null) + fi + + # Last autobuild run. Reads aggregate from PyAutoBuild/test_results/*.json + # (committed by the autobuild release pipeline). Counts only — failure + # detail lives in the per-job JSON / the GitHub Actions run. + local pab_dir="$HOME/Code/PyAutoLabs/PyAutoBuild/test_results" + if [[ -d "$pab_dir" ]] && compgen -G "$pab_dir/*.json" > /dev/null; then + local pab_summary + pab_summary=$(python3 -c ' +import json, os, glob +total_p = total_f = total_s = num = 0 +projects = set() +latest = "" +for f in sorted(glob.glob(os.path.expanduser("~/Code/PyAutoLabs/PyAutoBuild/test_results/*.json"))): + try: + d = json.load(open(f)) + s = d.get("summary", {}) + total_p += s.get("passed", 0) + total_f += s.get("failed", 0) + total_s += s.get("skipped", 0) + num += 1 + projects.add(d.get("project", "?")) + ct = d.get("completed_at", "") + if ct > latest: + latest = ct + except Exception: + pass +print(f"{latest[:10]}|{num}|{len(projects)}|{total_p}|{total_f}|{total_s}") +' 2>/dev/null) + if [[ -n "$pab_summary" ]]; then + local pab_date njobs nproj pab_p pab_f pab_s sha + IFS='|' read -r pab_date njobs nproj pab_p pab_f pab_s <<< "$pab_summary" + sha=$(git -C "$HOME/Code/PyAutoLabs/PyAutoBuild" rev-parse --short HEAD 2>/dev/null) + echo "" + printf "Last autobuild run: %s (PyAutoBuild commit %s)\n" "$pab_date" "${sha:-?}" + local color='\033[32m' + [[ "$pab_f" != "0" ]] && color='\033[31m' + printf " ${color}%s jobs across %s workspaces: %s passed, %s failed, %s skipped\033[0m\n" \ + "$njobs" "$nproj" "$pab_p" "$pab_f" "$pab_s" + fi + fi +} diff --git a/skills/worktree_status/SKILL.md b/skills/worktree_status/reference.md similarity index 95% rename from skills/worktree_status/SKILL.md rename to skills/worktree_status/reference.md index 1dcfb8f..ff3d108 100644 --- a/skills/worktree_status/SKILL.md +++ b/skills/worktree_status/reference.md @@ -1,9 +1,6 @@ ---- -name: worktree_status -description: Show the state of every task-scoped git worktree under ~/Code/PyAutoLabs-wt/, cross-referenced with PyAutoMind/active.md. Use this to see which parallel tasks are in flight and which repos each one is holding. ---- +# Worktree status — the /health worktrees leg -# Worktree Status +Procedure for the `/health worktrees` leg (the former standalone `worktree_status` skill). Diagnostic skill. Lists every worktree root under `$PYAUTO_WT_ROOT` (default `~/Code/PyAutoLabs-wt`), the task it belongs to per `active.md`, and the branch and dirty state of every real worktree inside it.