Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
59 changes: 59 additions & 0 deletions bin/README.md
Original file line number Diff line number Diff line change
@@ -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/<name>/`.
- A directory with `<dirname>.md` (and no `SKILL.md`) → **command**, symlinked to
`~/.claude/commands/<name>.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`.
59 changes: 59 additions & 0 deletions bin/check_skill_line_counts.sh
Original file line number Diff line number Diff line change
@@ -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 `<dirname>.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
# <dirname>.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
152 changes: 152 additions & 0 deletions bin/install.sh
Original file line number Diff line number Diff line change
@@ -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/<name>/
# - A directory with <name>.md (and no SKILL.md) → installed as a flat
# command at ~/.claude/commands/<name>.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."