diff --git a/scripts/health.sh b/scripts/health.sh index d25fbed..6c82e6a 100644 --- a/scripts/health.sh +++ b/scripts/health.sh @@ -1,47 +1,5 @@ #!/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): -# -# 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 -} +# Forwarding shim — health.sh moved to PyAutoHeart/scripts/ (the Heart owns the +# health surface; Mind stores intent). Point your sourcing at the new home; +# this shim keeps old paths working. +source "$(dirname "${BASH_SOURCE[0]}")/../../PyAutoHeart/scripts/health.sh" diff --git a/scripts/health_audit.sh b/scripts/health_audit.sh index 4215f83..821b5c6 100644 --- a/scripts/health_audit.sh +++ b/scripts/health_audit.sh @@ -1,113 +1,5 @@ #!/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 -} +# Forwarding shim — health_audit.sh moved to PyAutoHeart/scripts/ (the Heart owns the +# health surface; Mind stores intent). Point your sourcing at the new home; +# this shim keeps old paths working. +source "$(dirname "${BASH_SOURCE[0]}")/../../PyAutoHeart/scripts/health_audit.sh" diff --git a/scripts/health_release.sh b/scripts/health_release.sh index 34dc804..96985c0 100755 --- a/scripts/health_release.sh +++ b/scripts/health_release.sh @@ -1,215 +1,5 @@ #!/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" -} +# Forwarding shim — health_release.sh moved to PyAutoHeart/scripts/ (the Heart owns the +# health surface; Mind stores intent). Point your sourcing at the new home; +# this shim keeps old paths working. +source "$(dirname "${BASH_SOURCE[0]}")/../../PyAutoHeart/scripts/health_release.sh" diff --git a/scripts/health_sync.sh b/scripts/health_sync.sh index 51a5cfd..4833cf2 100644 --- a/scripts/health_sync.sh +++ b/scripts/health_sync.sh @@ -1,312 +1,5 @@ #!/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 -} +# Forwarding shim — health_sync.sh moved to PyAutoHeart/scripts/ (the Heart owns the +# health surface; Mind stores intent). Point your sourcing at the new home; +# this shim keeps old paths working. +source "$(dirname "${BASH_SOURCE[0]}")/../../PyAutoHeart/scripts/health_sync.sh"