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
45 changes: 27 additions & 18 deletions agents/conductors/hygiene/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,39 @@ paired repo**, boundaries) recorded in PyAutoMind

## Modes

| Mode | Question | Emits |
|------|----------|-------|
| `perf` | Which unit tests / integration-mode workspace scripts / imports are slow, and what is the cheapest win? | timing findings + route (refactor/bug; JAX-adapt only on a clear win) — *phase 3* |
| `tidy` | What git debris (stale branches, stashes, `[gone]` refs, dirty checkouts) is safe to remove? | the `repo_cleanup` sweep — *phase 2* |
| `noise` | What CLI noise (warnings, stray prints, library chatter) do tests/scripts emit? | the `cli_noise_clean` audit — *phase 2* |
| `deps` | Which dependency caps drift behind PyPI, and at what risk? | the `dep_audit` summary — *phase 2* |
| `docs` | Which `docs/api/*.rst` module paths are stale? | the `audit_docs` findings — *phase 2* |
| *(default)* | Across all of the above, what is the single most useful hygiene action now? | a prioritised `HygieneDecision` worklist |
Each mode does a cheap, read-only local **pre-scan** and **delegates** the full
audit + any execution to the owning skill — the conductor never runs a heavy
audit and never mutates a repo. A pre-scan is one of three kinds, which is what
makes its count comparable (or not):

- **debris** — finds directly-removable items; a real, rankable count (`tidy`).
- **surface** — only *sizes* the audit; the real problems emerge when the
delegated skill runs, so the count is **not** a problem count (`deps`, `docs`).
- **advisory** — no cheap local signal at all (`noise`).

| Mode | Pre-scan (kind) | Delegates to |
|------|-----------------|--------------|
| `perf` | *staged — phase 3* (dev-loop timing: slow tests / integration-mode scripts / imports) | `refactor` / `bug` |
| `tidy` | git debris — stale branches, stashes, `[gone]` refs, dirty checkouts (**debris**) | `/repo_cleanup` (Brain) |
| `noise` | none — needs a pytest + workspace-script run (**advisory**) | `/cli_noise_clean` (Heart) |
| `deps` | capped/pinned specifiers in library `pyproject.toml` (**surface**) | `/dep_audit` (Heart, hits PyPI) |
| `docs` | `docs/api/*.rst` + `currentmodule` counts across the 3 doc repos (**surface**) | `/audit_docs` (Heart, imports) |
| *(default)* | all of the above | a ranked `HygieneDecision` worklist — recommends `tidy` when debris exists (the only directly-actionable count), else prompts the periodic audits |

```
pyauto-brain hygiene # audit across modes → prioritised worklist
pyauto-brain hygiene # audit across modes → ranked worklist
pyauto-brain hygiene perf # dev-loop timing (staged: phase 3)
pyauto-brain hygiene tidy # git debris (staged: phase 2)
pyauto-brain hygiene noise # CLI noise (staged: phase 2)
pyauto-brain hygiene deps # dependency-cap drift (staged: phase 2)
pyauto-brain hygiene docs # stale API docs (staged: phase 2)
pyauto-brain hygiene tidy # git debris → /repo_cleanup
pyauto-brain hygiene noise # CLI noise → /cli_noise_clean
pyauto-brain hygiene deps # dependency-cap surface → /dep_audit
pyauto-brain hygiene docs # API-docs surface → /audit_docs
pyauto-brain hygiene <mode> --json
```

> **Staged.** This is the phase-1 scaffold: the conductor is real, routable and
> bounded, but the modes are stubs — each prints its staged notice. Phase 2
> absorbs `repo_cleanup` (`tidy`) and `cli_noise_clean` (`noise`) and consults
> `dep_audit` (`deps`) / `audit_docs` (`docs`); phase 3 builds `perf` and any
> PyAutoHeart observation legs.
Repos are read under `PYAUTO_ROOT` (default `~/Code/PyAutoLabs`). `noise`/`deps`/
`docs` route to **read-only PyAutoHeart observation skills** — measurement lives
in Heart; hygiene pre-scans, prioritises and routes. `perf` and any new standing
Heart legs (`import_time` / `cli_noise`) remain phase 3.

## Fundamental principles

Expand Down
199 changes: 156 additions & 43 deletions agents/conductors/hygiene/hygiene.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,123 @@
# Owns code-quality upkeep across the organism and emits a HygieneDecision the
# human/session executes, delegating the actual fixes to the dev-flow conductors
# (refactor/bug/feature) via ship_*. It reasons; it never edits source itself,
# and (like profiling) it stays stdlib/bash so it never drags the JAX stack into
# the Brain.
# never mutates a repo, and (like profiling) it stays stdlib/bash so it never
# drags the JAX stack into the Brain.
#
# Each mode does a cheap, read-only local PRE-SCAN (signals bash can compute in
# seconds) and DELEGATES the full audit + any execution to the owning skill:
# tidy -> /repo_cleanup (Brain), noise -> /cli_noise_clean, deps -> /dep_audit,
# docs -> /audit_docs (the last three are read-only PyAutoHeart observation
# skills — measurement lives in Heart; hygiene routes and prioritises).
#
# Usage:
# hygiene.sh # audit across modes -> prioritised worklist (default)
# hygiene.sh perf # dev-loop timing: unit tests / integration scripts / imports (phase 3)
# hygiene.sh tidy # git debris: the repo_cleanup sweep (phase 2)
# hygiene.sh noise # CLI noise: the cli_noise_clean audit (phase 2)
# hygiene.sh deps # dependency-cap drift: dep_audit vs PyPI (phase 2)
# hygiene.sh docs # stale API docs: audit_docs over docs/api/*.rst (phase 2)
# hygiene.sh tidy # git debris pre-scan -> /repo_cleanup
# hygiene.sh noise # CLI-noise route -> /cli_noise_clean
# hygiene.sh deps # dependency-cap pre-scan -> /dep_audit
# hygiene.sh docs # API-docs pre-scan -> /audit_docs
# hygiene.sh <mode> --json # machine-readable HygieneDecision
#
# PHASE 1 SCAFFOLD: the conductor is real, routable and bounded, but the modes
# are staged stubs — each reports where it lands. Per-mode behaviour and
# exit-code semantics arrive with the modes (phases 2-3).
# perf stays staged for phase 3; the other four modes are live (pre-scan +
# delegate). The conductor reads repos under PYAUTO_ROOT (default ~/Code/PyAutoLabs).

set -uo pipefail

HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
source "$HERE/../../_common.sh"

# PYAUTO_ROOT is exported/defaulted by _common.sh (~/Code/PyAutoLabs). Scan the
# canonical checkouts there, never the worktree symlinks.
ROOT="${PYAUTO_ROOT:-$HOME/Code/PyAutoLabs}"
LIB_REPOS=(PyAutoConf PyAutoFit PyAutoArray PyAutoGalaxy PyAutoLens)
ORG_REPOS=(PyAutoBrain PyAutoBuild PyAutoHeart PyAutoMind)
DOC_REPOS=(PyAutoFit PyAutoGalaxy PyAutoLens)

MODE_ORDER=(perf tidy noise deps docs)
declare -A MODE_PHASE=(
[perf]="phase 3" [tidy]="phase 2" [noise]="phase 2" [deps]="phase 2" [docs]="phase 2"
declare -A MODE_DELEGATE=(
[perf]="(phase 3 — not yet implemented)"
[tidy]="/repo_cleanup"
[noise]="/cli_noise_clean"
[deps]="/dep_audit"
[docs]="/audit_docs"
)
declare -A MODE_DESC=(
[perf]="dev-loop timing — unit tests / integration-mode scripts / import cost; route to refactor/bug"
[tidy]="git debris — the repo_cleanup sweep"
[noise]="CLI noise — the cli_noise_clean audit"
[deps]="dependency-cap drift — dep_audit vs PyPI"
[docs]="stale API docs — audit_docs over docs/api/*.rst"
# A mode's pre-scan is one of three kinds, which is what makes its count
# comparable (or not): 'debris' pre-scans find directly-removable items (a real,
# rankable count); 'surface' pre-scans only size the audit (the actual problems
# emerge only when the delegated skill runs — the count is NOT a problem count);
# 'advisory' has no cheap local signal. Only 'debris' counts drive the ranking.
declare -A MODE_KIND=(
[tidy]="debris" [deps]="surface" [docs]="surface" [noise]="advisory"
)

mode="default"
json=0
# --- Pre-scan helpers (read-only; each echoes "count|one-line summary"). -------

# tidy: git debris across managed checkouts — stale branches, stashes, [gone]
# tracking refs, dirty trees. The prioritisable count is the total debris.
prescan_tidy() {
local branches=0 stashes=0 gone=0 dirty=0 scanned=0 repo dir
for repo in "${LIB_REPOS[@]}" "${ORG_REPOS[@]}"; do
dir="$ROOT/$repo"
[[ -d "$dir/.git" || -f "$dir/.git" ]] || continue
scanned=$((scanned + 1))
local b s g
b=$(git -C "$dir" for-each-ref --format='%(refname:short)' refs/heads 2>/dev/null \
| grep -vxE 'main|master|HEAD' | wc -l | tr -d ' ')
s=$(git -C "$dir" stash list 2>/dev/null | wc -l | tr -d ' ')
g=$(git -C "$dir" branch -vv 2>/dev/null | grep -c '\[gone\]' || true)
branches=$((branches + b)); stashes=$((stashes + s)); gone=$((gone + g))
[[ -n "$(git -C "$dir" status --porcelain 2>/dev/null)" ]] && dirty=$((dirty + 1))
done
local total=$((branches + stashes + gone + dirty))
echo "${total}|${scanned} repos: ${branches} stale branches, ${stashes} stashes, ${gone} [gone] refs, ${dirty} dirty checkouts"
}

# deps: count capped dependency specifiers (<, <=, ==) in library pyproject.toml.
# A cheap "how many caps could be stale" signal; /dep_audit does the PyPI compare.
prescan_deps() {
local caps=0 files=0 repo pj
for repo in "${LIB_REPOS[@]}"; do
pj="$ROOT/$repo/pyproject.toml"
[[ -f "$pj" ]] || continue
files=$((files + 1))
local c
c=$(grep -oE '[<>=!~]=?[[:space:]]*[0-9]' "$pj" 2>/dev/null | grep -cE '<|==' || true)
caps=$((caps + c))
done
echo "${caps}|${caps} capped/pinned specifiers across ${files} library pyproject.toml"
}

# docs: count docs/api/*.rst files and currentmodule directives in the doc repos.
# /audit_docs does the actual import validation.
prescan_docs() {
local rst=0 cm=0 repo d
for repo in "${DOC_REPOS[@]}"; do
d="$ROOT/$repo/docs/api"
[[ -d "$d" ]] || continue
local n c
n=$(find "$d" -maxdepth 1 -name '*.rst' 2>/dev/null | wc -l | tr -d ' ')
c=$(grep -rhE '^\s*\.\.\s+currentmodule::' "$d" 2>/dev/null | wc -l | tr -d ' ')
rst=$((rst + n)); cm=$((cm + c))
done
echo "${cm}|${rst} api .rst files, ${cm} currentmodule directives across ${#DOC_REPOS[@]} repos"
}

# noise: no cheap local signal (needs running pytest + workspace scripts).
prescan_noise() {
echo "-1|no cheap local signal — runs pytest + workspace scripts (PYAUTO_TEST_MODE=2)"
}

prescan() {
case "$1" in
tidy) prescan_tidy ;; deps) prescan_deps ;;
docs) prescan_docs ;; noise) prescan_noise ;;
esac
}

# --- Arg parse. ----------------------------------------------------------------

mode="default"; json=0
for arg in "$@"; do
case "$arg" in
perf|tidy|noise|deps|docs) mode="$arg" ;;
Expand All @@ -53,46 +135,77 @@ for arg in "$@"; do
done

if [[ "$mode" == "help" ]]; then
# Print the "# Usage:" block from this script's own header (robust to the
# header moving — no hard-coded line numbers).
awk '/^# Usage:/{u=1;next} u{ if($0 ~ /^# /){sub(/^# /," "); print} else exit }' "$HERE/hygiene.sh"
exit 0
fi

# --- JSON footing (staged): a HygieneDecision shell the Brain session can read.
# --- JSON footing: a HygieneDecision the Brain session can consume. ------------
emit_json_row() { # mode
local m="$1"
if [[ "$m" == "perf" ]]; then
printf '{"mode":"perf","status":"staged","lands":"phase 3"}'; return
fi
local res count summary kind status
res="$(prescan "$m")"; count="${res%%|*}"; summary="${res#*|}"; kind="${MODE_KIND[$m]}"
if [[ "$kind" == "advisory" ]]; then status="advisory"
elif [[ "$kind" == "surface" ]]; then status="surface"
elif [[ "$count" == "0" ]]; then status="clean"
else status="debris"; fi
printf '{"mode":"%s","kind":"%s","status":"%s","count":%s,"summary":"%s","delegate":"%s"}' \
"$m" "$kind" "$status" "$([[ "$count" == "-1" ]] && echo null || echo "$count")" \
"${summary//\"/\\\"}" "${MODE_DELEGATE[$m]}"
}

if [[ "$json" -eq 1 ]]; then
if [[ "$mode" == "default" ]]; then
printf '{"decision":"HygieneDecision","status":"staged-phase-1","mode":"default","modes":['
printf '{"decision":"HygieneDecision","mode":"default","rows":['
sep=""
for m in "${MODE_ORDER[@]}"; do
printf '%s{"mode":"%s","status":"staged","lands":"%s"}' "$sep" "$m" "${MODE_PHASE[$m]}"
sep=","
done
for m in "${MODE_ORDER[@]}"; do printf '%s' "$sep"; emit_json_row "$m"; sep=","; done
printf ']}\n'
else
printf '{"decision":"HygieneDecision","status":"staged-phase-1","mode":"%s","lands":"%s"}\n' \
"$mode" "${MODE_PHASE[$mode]}"
printf '{"decision":"HygieneDecision","mode":"%s","row":' "$mode"; emit_json_row "$mode"; printf '}\n'
fi
exit 0
fi

# --- Human footing.
echo "== HygieneDecision (phase-1 scaffold — modes staged) =="
echo "The hygiene conductor owns code-quality upkeep: developer-loop cost and repo"
echo "tidiness, distinct from Heart (proof-of-works) and profiling (modelling speed)."
echo "It finds and prioritises debt, then delegates the fix to refactor/bug/feature."
# --- Human footing. ------------------------------------------------------------
echo "== HygieneDecision =="
echo "The hygiene conductor pre-scans code-quality debt (read-only) and delegates the"
echo "audit + fix to the owning skill — it never mutates a repo itself."
echo

render_row() { # mode
local m="$1"
if [[ "$m" == "perf" ]]; then
printf ' %-6s %-9s %s\n' "perf" "staged" "dev-loop timing (phase 3) — not yet implemented"
return
fi
local res count summary kind tag
res="$(prescan "$m")"; count="${res%%|*}"; summary="${res#*|}"; kind="${MODE_KIND[$m]}"
if [[ "$kind" == "advisory" ]]; then tag="advisory"
elif [[ "$kind" == "surface" ]]; then tag="surface"
elif [[ "$count" == "0" ]]; then tag="clean"
else tag="${count} debris"; fi
printf ' %-6s %-9s %s\n' "$m" "$tag" "$summary"
printf ' %-6s %-9s → run %s for the full audit\n' "" "" "${MODE_DELEGATE[$m]}"
}

if [[ "$mode" == "default" ]]; then
printf ' %-6s %-9s %s\n' "MODE" "LANDS" "SCOPE"
for m in "${MODE_ORDER[@]}"; do
printf ' %-6s %-9s %s\n' "$m" "${MODE_PHASE[$m]}" "${MODE_DESC[$m]}"
done
# Only 'debris' pre-scans yield a directly-actionable count, so the ranking is
# honest: recommend tidy when there is removable debris; otherwise the
# 'surface'/'advisory' modes are periodic audits (their real findings only
# emerge when the delegated skill runs — the pre-scan can't rank them).
tidy_n="$(prescan tidy)"; tidy_n="${tidy_n%%|*}"
for m in "${MODE_ORDER[@]}"; do render_row "$m"; done
echo
echo "Run 'pyauto-brain hygiene <mode>' for a mode once it lands; --json for the"
echo "machine footing. Design: PyAutoMind research/pyautobrain/hygiene_agent_decision.md."
if [[ "$tidy_n" -gt 0 ]]; then
echo "Recommended next: hygiene tidy (${tidy_n} removable debris items), then run /repo_cleanup."
echo " deps/docs report audit *surface*, not problems — run their skills periodically to surface real findings."
else
echo "Recommended next: no removable debris — run deps/docs/noise audits periodically (their pre-scans size the surface only)."
fi
echo "Design: PyAutoMind research/pyautobrain/hygiene_agent_decision.md."
else
echo " mode: $mode"
echo " scope: ${MODE_DESC[$mode]}"
echo " lands: ${MODE_PHASE[$mode]} — not yet implemented (phase-1 scaffold)."
render_row "$mode"
fi
exit 0
24 changes: 12 additions & 12 deletions skills/hygiene/hygiene.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ Shared routing context: `PyAutoBrain/skills/COMMANDS.md`.

## Do

1. Run `bin/pyauto-brain hygiene [perf | tidy | noise | deps | docs]` (no arg =
audit across modes → a prioritised worklist). This is a **dry run** — it emits
a `HygieneDecision`. Nothing is executed.
2. Execute the emitted plan through the normal dev workflow: hygiene *finds and
prioritises* quality debt and *delegates the fix* — restructuring to
`/refactor`, regressions to `/bug`, larger changes to `/feature` — shipped via
1. Run `bin/pyauto-brain hygiene [tidy | noise | deps | docs]` (no arg = pre-scan
across modes → a ranked worklist). This is a **dry run** — each mode does a
cheap read-only pre-scan and emits a `HygieneDecision` naming the skill to run
for the full audit. Nothing is executed or mutated.
2. Execute the emitted plan: run the named delegate — `/repo_cleanup` (git
debris), `/cli_noise_clean`, `/dep_audit`, `/audit_docs` — for the full audit,
then route any code fixes to `/refactor` / `/bug` / `/feature`, shipped via
`ship_library` / `ship_workspace`.

The Hygiene Agent **reasons; it never edits source.** Measurement lives in Heart
(the `script_timing` / `test_run` signals); hygiene acts on it.
The Hygiene Agent **reasons; it never edits source and never mutates a repo.**
Measurement lives in Heart (`noise`/`deps`/`docs` route to read-only PyAutoHeart
skills, plus the `script_timing` / `test_run` signals); hygiene pre-scans + routes.

> **Staged (phase 1):** the conductor is real and bounded but its modes are
> stubs. `tidy` / `noise` / `deps` / `docs` land in phase 2 (absorbing
> `repo_cleanup` + `cli_noise_clean`, consulting `dep_audit` + `audit_docs`);
> `perf` in phase 3.
> **Staged:** only `perf` (dev-loop timing) remains staged — it lands in phase 3
> with any new PyAutoHeart legs. `tidy` / `noise` / `deps` / `docs` are live.
15 changes: 8 additions & 7 deletions skills/repo_cleanup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ Cleanup is not release work, so it never touches PyAutoBuild. It reads the
PyAutoMind registry (`active.md`) to know what's claimed. Organ boundary +
execution-environment model: [`../WORKFLOW.md`](../WORKFLOW.md).

> **The hygiene conductor.** This skill is the inline form; its reasoning is now
> owned by the PyAutoBrain **Hygiene Agent** (`agents/conductors/hygiene/`),
> which generalises hygiene across the organism and absorbs this sweep as its
> `tidy` mode. That conductor's phase-1 scaffold exists and documents the
> boundary; the `tidy` mode itself lands in phase 2. Until it does, run the
> reasoning here per this file — this remains the working form for git-debris
> cleanup.
> **The hygiene conductor.** The PyAutoBrain **Hygiene Agent**
> (`agents/conductors/hygiene/`) generalises hygiene across the organism; its
> `tidy` mode (`pyauto-brain hygiene tidy`) is the front door — it pre-scans the
> git debris (stale branches / stashes / `[gone]` refs / dirty checkouts) and
> routes here. **This skill remains the executor:** the conductor reasons and
> never mutates a repo, so the interactive per-bucket audit + deletion below is
> what actually does the cleanup. Reach for `hygiene tidy` to see the debris and
> the recommendation; run this skill to act on it.

**Distinct from:** `/health worktrees` (Heart read-only diagnostic — consulted here,
but this also mutates); post-merge cleanup in `CLAUDE.md` (once per shipped task —
Expand Down
Loading