diff --git a/README.md b/README.md index 7e87022..02e42b3 100644 --- a/README.md +++ b/README.md @@ -96,4 +96,10 @@ bin/pyauto-brain vitals # (faculty) one tick + the unified dashboard card ( PyAutoBrain runs from its checkout (no pip install), resolving the sibling `pyauto-heart` and `autobuild` binaries from PATH or `~/Code/PyAutoLabs/`. +`bin/` also hosts the cross-organ Claude skill **installer** (`install.sh`) and +line-count **guard** (`check_skill_line_counts.sh`) — organism-wide infrastructure +that used to live in `admin_jammy/skills/`. Run `bash PyAutoBrain/bin/install.sh` +to (re-)symlink every organ's skills/commands into `~/.claude/`. See +[`bin/README.md`](bin/README.md). + See [`AGENTS.md`](AGENTS.md) for the full description. diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 0000000..703c6a2 --- /dev/null +++ b/bin/README.md @@ -0,0 +1,59 @@ +# PyAutoBrain/bin/ + +Executable tooling for the PyAuto organism. + +- **`pyauto-brain`** — the PyAutoBrain CLI (reasoning-layer entry point). +- **`install.sh`** — the cross-organ Claude skill/command **installer**. +- **`check_skill_line_counts.sh`** — the skill line-count **guard** (primary + skill `.md` files must stay < 200 lines). + +The installer and guard used to live in `admin_jammy/skills/`. They moved here +because they are organism-wide infrastructure, not admin_jammy's own tooling — +admin_jammy hosts no skills and is slated to leave `PyAutoLabs/`. + +## install.sh + +Auto-discovers (no hardcoded skill list) by scanning each organ repo's +`skills/` dir for `*/` subdirs, then symlinks each into `~/.claude/`: + +- **`PyAutoMind/skills/`** — registry-coupled skills (`create_issue`). +- **`PyAutoBrain/skills/`** — development-workflow skills (`start_dev`, + `start_dev_for_user`, `plan_branches`, `start_library`, `start_workspace`, + `ship_library`, `ship_workspace`, `register_and_iterate`, `repo_cleanup`, + `update_issue`). +- **`PyAutoHeart/skills/`** — status / readiness / validation skills + (`pyauto-status`, `pyauto-status-full`, `worktree_status`, `smoke_test`, + `dep_audit`, `verify_install`, `review_release`, `audit_docs`, `cli_noise_clean`). +- **`PyAutoBuild/skills/`** — release-execution skills **only** (`pre_build`). + Build owns no dev-workflow skills; `ship_*` (Brain) only *call* its release step. +- **`autolens_profiling/skills/`** — science-profiling skills (`profile_likelihood`). +- **`admin_jammy/skills/`** — vestigial: hosts no skills; scanned only so an old + checkout still resolves, auto-skipped once admin_jammy leaves. + +Discovery rule per root: + +- A directory with `SKILL.md` → **skill**, symlinked to `~/.claude/skills//`. +- A directory with `.md` (and no `SKILL.md`) → **command**, symlinked to + `~/.claude/commands/.md`. + +Roots that aren't checked out are skipped. It is idempotent — existing symlinks +are replaced, non-symlink files are left alone, and broken symlinks pointing into +a PyAuto root are pruned. Re-run it after pulling updates. + +## Bootstrap on a new machine + +```bash +cd ~/Code/PyAutoLabs +git clone git@github.com:PyAutoLabs/PyAutoBrain.git +git clone git@github.com:PyAutoLabs/PyAutoMind.git +git clone git@github.com:PyAutoLabs/PyAutoHeart.git +bash PyAutoBrain/bin/install.sh +``` + +## Execution environments + +There is **no special "mobile" or "phone" mode**. Skills run the same logic in +any environment and only differ in where they read state from: a `local-dev` +checkout uses local git + task worktrees; a `web-github` / `ci-only` session uses +the clones present in the working directory (and the GitHub API for branch +state). The full environment model is in `PyAutoBrain/skills/WORKFLOW.md`. diff --git a/bin/check_skill_line_counts.sh b/bin/check_skill_line_counts.sh new file mode 100755 index 0000000..3509033 --- /dev/null +++ b/bin/check_skill_line_counts.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Report any *primary* skill .md file over the line limit (default 200). +# +# Claude skill guidance keeps primary skill files short; long background, +# templates and examples belong in supporting docs (`reference.md`, shared +# `WORKFLOW.md`), which are exempt here. A "primary" file is a skill's `SKILL.md` +# or its command body `.md`. +# +# Scans the same discovery roots as install.sh. Exit 0 = all within limit, +# exit 1 = at least one primary file is over the limit. +# +# Usage: +# bash PyAutoBrain/bin/check_skill_line_counts.sh [limit] + +set -euo pipefail + +LIMIT="${1:-200}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PYAUTO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +ROOTS=( + "$PYAUTO_ROOT/admin_jammy/skills" # vestigial (hosts no skills) + "$PYAUTO_ROOT/PyAutoMind/skills" + "$PYAUTO_ROOT/PyAutoBrain/skills" + "$PYAUTO_ROOT/PyAutoHeart/skills" + "$PYAUTO_ROOT/PyAutoBuild/skills" + "$PYAUTO_ROOT/autolens_profiling/skills" +) + +over=0 +checked=0 + +for root in "${ROOTS[@]}"; do + [ -d "$root" ] || continue + for dir in "$root"/*/; do + [ -d "$dir" ] || continue + name="$(basename "$dir")" + # Primary files: SKILL.md (dispatcher/frontmatter) and the command/body + # .md. Supporting docs (reference.md, examples, etc.) are exempt. + for primary in "$dir/SKILL.md" "$dir/$name.md"; do + [ -f "$primary" ] || continue + checked=$((checked + 1)) + lines=$(wc -l < "$primary") + if [ "$lines" -gt "$LIMIT" ]; then + echo "OVER ${lines} ${primary#$PYAUTO_ROOT/}" + over=$((over + 1)) + fi + done + done +done + +echo "" +if [ "$over" -eq 0 ]; then + echo "OK: all $checked primary skill files are within $LIMIT lines." + exit 0 +else + echo "FAIL: $over primary skill file(s) exceed $LIMIT lines — factor detail into reference.md." + exit 1 +fi diff --git a/bin/install.sh b/bin/install.sh new file mode 100755 index 0000000..a64be5b --- /dev/null +++ b/bin/install.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# Bootstrap Claude Code skills/commands into ~/.claude/ from every PyAuto +# organism repo that hosts skills. +# +# This installer lives in PyAutoBrain (the reasoning/orchestration organ). It +# scans every organ repo's skills/ dir and symlinks their skills+commands into +# ~/.claude/. Roots that aren't checked out are simply skipped. +# +# Discovery roots (scanned in order): +# - admin_jammy/skills/ — vestigial: admin_jammy hosts no skills and is +# slated to leave PyAutoLabs/; kept only so an old +# checkout still resolves, auto-skipped once gone +# - PyAutoMind/skills/ — registry-coupled skills (create_issue, handoff) +# - PyAutoBrain/skills/ — development-workflow skills (start_*/ship_*/plan_branches/…) +# - PyAutoHeart/skills/ — status / readiness / diagnostic skills +# - PyAutoBuild/skills/ — release-execution skills ONLY (pre_build) +# - autolens_profiling/skills/ — science-profiling skills (profile_likelihood) +# +# (PyAutoBuild's root is for its own release/packaging-execution skills only — it +# owns NO dev-workflow skills; the ship_* skills merely call its release step.) +# +# Auto-discovers skills in each root: +# - A directory with SKILL.md → installed as ~/.claude/skills// +# - A directory with .md (and no SKILL.md) → installed as a flat +# command at ~/.claude/commands/.md +# +# Safe to re-run — existing symlinks are replaced, non-symlink files are skipped. +# +# Usage: +# bash PyAutoBrain/bin/install.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PYAUTO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +ADMIN_SKILLS_DIR="$PYAUTO_ROOT/admin_jammy/skills" +MIND_SKILLS_DIR="$PYAUTO_ROOT/PyAutoMind/skills" +BRAIN_SKILLS_DIR="$PYAUTO_ROOT/PyAutoBrain/skills" +HEART_SKILLS_DIR="$PYAUTO_ROOT/PyAutoHeart/skills" +BUILD_SKILLS_DIR="$PYAUTO_ROOT/PyAutoBuild/skills" +PROFILING_SKILLS_DIR="$PYAUTO_ROOT/autolens_profiling/skills" + +# ---------- Execution-environment note ---------- +# +# Skill discovery is identical in every execution environment (local-dev, +# web-github, ci-only). When a root repo is not checked out, it is simply +# skipped — the skills from the roots that ARE present still install. + +if [ -d "$PYAUTO_ROOT/PyAutoFit" ] || [ -d "$HOME/Code/PyAutoLabs/PyAutoFit" ]; then + echo "Environment: local-dev (PyAuto repos detected)" +else + echo "Environment: web-github / ci-only (clone roots on demand)" +fi +echo "" + +mkdir -p "$HOME/.claude/skills" "$HOME/.claude/commands" + +# ---------- Prune stale symlinks ---------- +# +# Self-healing: when a skill is removed or re-homed, its old ~/.claude symlink +# would otherwise dangle. Remove only BROKEN symlinks whose target points into a +# PyAuto root — never touch real files or symlinks that resolve, and never touch +# links pointing outside the managed roots (e.g. user-added skills). + +prune_stale_symlinks() { + local dir="$1" + [ -d "$dir" ] || return 0 + for link in "$dir"/*; do + [ -L "$link" ] || continue + [ -e "$link" ] && continue # resolves fine — keep + local tgt; tgt="$(readlink "$link")" + case "$tgt" in + "$PYAUTO_ROOT"/*|"$HOME/Code/PyAutoLabs"/*) + echo " PRUNE $(basename "$link") (stale → $tgt)"; rm -f "$link" ;; + esac + done +} + +prune_stale_symlinks "$HOME/.claude/skills" +prune_stale_symlinks "$HOME/.claude/commands" + +# ---------- Install one source dir's skills/commands ---------- + +install_from_dir() { + local source_dir="$1" + local label="$2" + + if [ ! -d "$source_dir" ]; then + echo "${label}: (source dir not present, skipping)" + return + fi + + echo "$label" + local installed_count=0 + + for entry in "$source_dir"/*/; do + [ -d "$entry" ] || continue + local name + name=$(basename "$entry") + + # Skip the install.sh dir itself, or any non-skill dirs + [ "$name" = "skills" ] && continue + + if [ -f "$entry/SKILL.md" ]; then + # Skill — symlink the directory + local dst="$HOME/.claude/skills/$name" + _link_symlink "$entry" "$dst" "skill" + installed_count=$((installed_count + 1)) + elif [ -f "$entry/$name.md" ]; then + # Command — symlink the flat .md file + local dst="$HOME/.claude/commands/$name.md" + _link_symlink "$entry/$name.md" "$dst" "command" + installed_count=$((installed_count + 1)) + else + echo " SKIP $name (no SKILL.md or $name.md found)" + fi + done + + echo " ($installed_count installed from $label)" + echo "" +} + +_link_symlink() { + local src="$1" + local dst="$2" + local kind="$3" + local name + name=$(basename "$dst") + + if [ -L "$dst" ]; then + rm "$dst" + elif [ -e "$dst" ]; then + echo " SKIP $name ($kind — non-symlink exists at $dst)" + return + fi + + ln -s "$src" "$dst" + echo " LINK $name ($kind)" +} + +# ---------- Run installs ---------- + +install_from_dir "$ADMIN_SKILLS_DIR" "admin_jammy/skills/ — vestigial (hosts no skills)" +install_from_dir "$MIND_SKILLS_DIR" "PyAutoMind/skills/ — registry-coupled skills (Mind)" +install_from_dir "$BRAIN_SKILLS_DIR" "PyAutoBrain/skills/ — development-workflow skills (Brain)" +install_from_dir "$HEART_SKILLS_DIR" "PyAutoHeart/skills/ — status / readiness skills (Heart)" +install_from_dir "$BUILD_SKILLS_DIR" "PyAutoBuild/skills/ — release-execution skills (Hands)" +install_from_dir "$PROFILING_SKILLS_DIR" "autolens_profiling/skills/ — science-profiling skills" + +# ---------- Summary ---------- + +echo "Done. Restart Claude Code to pick up changes."