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
52 changes: 37 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,21 +202,43 @@ only reasons.

## Running

```bash
bin/pyauto-brain help # list agents
bin/pyauto-brain intake "add data cube modelling to autolens" # conceive: raw text -> a formal Mind prompt (dry-run)
bin/pyauto-brain intake --apply ideas # sweep ideas.md into formal prompts
bin/pyauto-brain feature # select the best next PyAutoMind feature task
bin/pyauto-brain feature feature/autofit/sbi.md # plan a specific feature task
bin/pyauto-brain refactor # select + plan the best next behaviour-preserving refactor
bin/pyauto-brain refactor candidates # mine the refactor backlog + ideas.md (read-only)
bin/pyauto-brain build # consult vitals, then delegate execution to Build
bin/pyauto-brain build --dry-run # reason + plan only (emit the BuildDecision)
bin/pyauto-brain release # reason about readiness, then release on green
bin/pyauto-brain health # (conductor) run the health loop with a human, toward green
bin/pyauto-brain vitals # (faculty) one tick + the unified dashboard card (raw read)
bin/pyauto-brain review --task <name> # (faculty) a branch's ReviewSurface for the ship gate's review leg
```
<!-- pyauto:commands:begin -->
<!-- Generated by `PyAutoBrain/bin/install.sh --write-agents-surface` from the
agent registry in `PyAutoBrain/bin/pyauto-brain`. Do not edit between these
markers — edit the registry there and re-run. Checked by
`PyAutoBrain/bin/install.sh --check-agents-surface`. -->

The PyAuto **command surface** — every agent verb, runnable on any tool (Claude,
Codex, Cursor; CLI or web) as `bin/pyauto-brain <verb>`. This block lives once in
**PyAutoBrain**'s auto-loaded AGENTS.md, which is present in every session, so the
full verb set is always in context — no per-organ copy needed. Invoking a verb
runs its entrypoint here in PyAutoBrain. On Claude Code the same verbs are also
the `/<verb>` slash commands.

**Conductors** — front doors you drive (decide *and* act):

| Verb | Purpose | Entrypoint |
|------|---------|------------|
| `intake` | Conceive a task: turn raw input into a formal, headed PyAutoMind prompt (files it; never starts dev) | `bin/pyauto-brain intake` |
| `feature` | Reason over PyAutoMind feature tasks: select, size, phase, plan for start_dev | `bin/pyauto-brain feature` |
| `bug` | The immune system: classify a bug/regression/Heart finding, locate the fix, plan the repair | `bin/pyauto-brain bug` |
| `refactor` | The renewal function: plan behaviour-preserving restructuring — RefactorDecision; default-safe under --auto | `bin/pyauto-brain refactor` |
| `profiling` | The proprioceptive function — the organism's sense of its own effort: campaign/ingest/triage plans over the autolens_profiling workspace — ProfilingDecision | `bin/pyauto-brain profiling` |
| `hygiene` | The maintenance function — the organism's sense of its own upkeep: code-quality debt (dev-loop cost + tidiness), delegating fixes — HygieneDecision | `bin/pyauto-brain hygiene` |
| `clone` | The Mitosis Agent (v0: decision only): partition the reference assistant, analyze the domain, emit the CloneDecision — never writes | `bin/pyauto-brain clone` |
| `build` | Coordinate execution: consult the vitals faculty, then delegate to PyAutoBuild | `bin/pyauto-brain build` |
| `release` | Release door → the Build Agent release mode (single gate); 'release rehearse'/'release validate' drive release validation; 'release nightly' is the scheduled-nightly driver | `bin/pyauto-brain release` |
| `health` | The organism's clinician: run the health loop with a human, dispatch by dispatch, toward green | `bin/pyauto-brain health` |

**Faculties** — read-only opinions the conductors consult (also runnable):

| Verb | Purpose | Entrypoint |
|------|---------|------------|
| `vitals` | Read-only: read the Heart's pulse — the PyAutoHeart readiness verdict (consulted by the conductors) | `bin/pyauto-brain vitals` |
| `review` | Read-only: prepare the branch ReviewSurface — the reviewing agent maps it to CLEAN/FINDINGS/BLOCKED (the ship gate's review leg) | `bin/pyauto-brain review` |
| `memory` | Read-only: recall what the organism knows — a cited digest over PyAutoMemory, autolens_assistant and Mind history | `bin/pyauto-brain memory` |
| `samplers` | Read-only: the motor faculty — SamplerSurface digest over the sampler script tiers, the PyAutoFit search catalogue and the benchmark record | `bin/pyauto-brain samplers` |
<!-- pyauto:commands:end -->

Like the other PyAuto repos, PyAutoBrain runs from its checkout (no pip install);
it resolves the sibling `pyauto-heart` and `autobuild` binaries from PATH or the
Expand Down
135 changes: 134 additions & 1 deletion bin/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,18 @@
# Safe to re-run — existing symlinks are replaced, non-symlink files are skipped.
#
# Usage:
# bash PyAutoBrain/bin/install.sh
# bash PyAutoBrain/bin/install.sh # install skills/commands
# bash PyAutoBrain/bin/install.sh --write-agents-surface # (re)generate the command
# # surface block in each organ AGENTS.md
# bash PyAutoBrain/bin/install.sh --check-agents-surface # drift-check that block (exit 1 on drift)
#
# The command-surface modes are the agent-agnostic half of command discovery:
# per-tool symlinks (above) are absent in cloud/web sessions, which load only
# committed repo files — so the verb → purpose → `bin/pyauto-brain <verb>` index
# is generated into PyAutoBrain's auto-loaded AGENTS.md (Brain is loaded in every
# session, so one copy reaches everywhere), sourced from the single agent registry
# in `bin/pyauto-brain`. Other organs opt in via the markers but need not — Brain's
# copy already travels with every session.

set -euo pipefail

Expand All @@ -44,6 +55,128 @@ PROFILING_SKILLS_DIR="$PYAUTO_ROOT/autolens_profiling/skills"
CLAUDE_HOME="${CLAUDE_HOME:-$HOME/.claude}"
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"

# ---------- Command surface (agent-agnostic discovery) ----------
#
# The organ repos whose auto-loaded AGENTS.md carries the generated command
# surface. Framework identity (a fork keeps the five organs), so it is fixed
# here — like the skills dirs above — rather than parsed out of repos.yaml. A
# repo opts in by adding the marker pair; absent repos / repos without markers
# are skipped, so this runs in a partial/web checkout.
COMMANDS_BEGIN="<!-- pyauto:commands:begin -->"
COMMANDS_END="<!-- pyauto:commands:end -->"
ORGAN_REPOS=(PyAutoMind PyAutoBrain PyAutoHeart PyAutoBuild PyAutoMemory)

# Emit the canonical command-surface block (identical in every organ) to stdout,
# sourced from the agent registry in bin/pyauto-brain — the single source of
# truth for `bin/pyauto-brain <verb>`. Sourcing defines the arrays without
# running the dispatcher (its main is guarded on direct execution).
agents_surface_block() {
# shellcheck disable=SC1090
source "$SCRIPT_DIR/pyauto-brain"
echo "$COMMANDS_BEGIN"
cat <<'EOF'
<!-- Generated by `PyAutoBrain/bin/install.sh --write-agents-surface` from the
agent registry in `PyAutoBrain/bin/pyauto-brain`. Do not edit between these
markers — edit the registry there and re-run. Checked by
`PyAutoBrain/bin/install.sh --check-agents-surface`. -->

The PyAuto **command surface** — every agent verb, runnable on any tool (Claude,
Codex, Cursor; CLI or web) as `bin/pyauto-brain <verb>`. This block lives once in
**PyAutoBrain**'s auto-loaded AGENTS.md, which is present in every session, so the
full verb set is always in context — no per-organ copy needed. Invoking a verb
runs its entrypoint here in PyAutoBrain. On Claude Code the same verbs are also
the `/<verb>` slash commands.

**Conductors** — front doors you drive (decide *and* act):

| Verb | Purpose | Entrypoint |
|------|---------|------------|
EOF
local name desc
for name in "${CONDUCTOR_ORDER[@]}"; do
desc="${AGENT_DESC[$name]//|/\\|}"
printf '| `%s` | %s | `bin/pyauto-brain %s` |\n' "$name" "$desc" "$name"
done
cat <<'EOF'

**Faculties** — read-only opinions the conductors consult (also runnable):

| Verb | Purpose | Entrypoint |
|------|---------|------------|
EOF
for name in "${FACULTY_ORDER[@]}"; do
desc="${AGENT_DESC[$name]//|/\\|}"
printf '| `%s` | %s | `bin/pyauto-brain %s` |\n' "$name" "$desc" "$name"
done
echo "$COMMANDS_END"
}

# Splice the block (read from a file, so no awk -v escape processing) between the
# markers in one AGENTS.md, replacing whatever is there.
_splice_surface() {
local file="$1" blockfile="$2" tmp
tmp="$(mktemp)"
awk -v begin="$COMMANDS_BEGIN" -v end="$COMMANDS_END" -v bf="$blockfile" '
$0 == begin { while ((getline line < bf) > 0) print line; close(bf); skip=1; next }
$0 == end { skip=0; next }
!skip { print }
' "$file" > "$tmp"
if cmp -s "$file" "$tmp"; then
echo "unchanged: $file"; rm -f "$tmp"
else
mv "$tmp" "$file"; echo "updated: $file"
fi
}

write_agents_surface() {
local blockfile repo agents
blockfile="$(mktemp)"; agents_surface_block > "$blockfile"
for repo in "${ORGAN_REPOS[@]}"; do
agents="$PYAUTO_ROOT/$repo/AGENTS.md"
if [ ! -f "$agents" ]; then
echo "skipped (absent): $agents"; continue
fi
if ! grep -qF -- "$COMMANDS_BEGIN" "$agents" || ! grep -qF -- "$COMMANDS_END" "$agents"; then
echo "skipped (no command markers): $agents"; continue
fi
_splice_surface "$agents" "$blockfile"
done
rm -f "$blockfile"
}

check_agents_surface() {
local blockfile repo agents cur drift=0
blockfile="$(mktemp)"; agents_surface_block > "$blockfile"
for repo in "${ORGAN_REPOS[@]}"; do
agents="$PYAUTO_ROOT/$repo/AGENTS.md"
if [ ! -f "$agents" ]; then
echo "skipped (absent): $agents"; continue
fi
if ! grep -qF -- "$COMMANDS_BEGIN" "$agents"; then
echo "skipped (no command markers): $agents"; continue
fi
cur="$(mktemp)"
awk -v begin="$COMMANDS_BEGIN" -v end="$COMMANDS_END" '
$0 == begin { grab=1 } grab { print } $0 == end { grab=0 }' "$agents" > "$cur"
if cmp -s "$cur" "$blockfile"; then
echo "OK: $agents"
else
echo "DRIFT: $agents — run: bash PyAutoBrain/bin/install.sh --write-agents-surface"
drift=1
fi
rm -f "$cur"
done
rm -f "$blockfile"
return "$drift"
}

case "${1:-}" in
--write-agents-surface) write_agents_surface; exit 0 ;;
--check-agents-surface) check_agents_surface; exit $? ;;
--help|-h)
sed -n '2,20p' "$0"; exit 0 ;;
esac

# ---------- Execution-environment note ----------
#
# Skill discovery is identical in every execution environment (local-dev,
Expand Down
8 changes: 7 additions & 1 deletion bin/pyauto-brain
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,10 @@ main() {
exec bash "$script" "$@"
}

main "$@"
# Only dispatch when executed directly. When sourced (e.g. by
# bin/install.sh --write-agents-surface, which reuses the AGENT_DESC registry
# below as the single source of the command surface) the arrays are defined but
# main is not run.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
28 changes: 28 additions & 0 deletions tests/test_skill_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,31 @@ def test_invalid_codex_name_does_not_suppress_claude_surfaces(tmp_path):
assert (claude_home / "commands" / "legacy_skill.md").is_symlink()
assert not (codex_home / "skills" / "legacy_skill").exists()
assert "Codex skill name invalid" in result.stdout


def test_command_surface_block_is_current():
"""This repo's committed command-surface block matches the agent registry.
Guards against editing bin/pyauto-brain without regenerating the block
(bash PyAutoBrain/bin/install.sh --write-agents-surface)."""
result = subprocess.run(
["bash", str(INSTALLER), "--check-agents-surface"],
cwd=BRAIN_HOME,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stdout + result.stderr
assert "DRIFT" not in result.stdout


def test_command_surface_covers_every_public_agent():
agents = public_agents()
assert agents
text = (BRAIN_HOME / "AGENTS.md").read_text()
begin = text.index("<!-- pyauto:commands:begin -->")
end = text.index("<!-- pyauto:commands:end -->")
surface = text[begin:end]
missing = [
name for name in sorted(agents)
if f"`bin/pyauto-brain {name}`" not in surface
]
assert missing == []