From c6000d7159521d21cab6db2932d4e1f7cba48a48 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 11:47:10 +0000 Subject: [PATCH 1/2] feat: add the Build Agent, the second canonical PyAutoBrain agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Build Agent is the executive/orchestration layer for execution work. It does not build software itself — PyAutoBuild does. It decides whether, what, and which PyAutoBuild capability to invoke, then delegates. This makes it the canonical example of the Brain coordinating multiple organs. Architectural centrepiece: Brain agents consult one another. The Build Agent does not query PyAutoHeart directly — it consults the sibling Health Agent, and only the Health Agent talks to the Heart organ: Mind -> Build Agent -> Health Agent -> Heart -> GREEN/YELLOW/RED -> Build Agent -> PyAutoBuild (execute) Release is kept as a *mode* (build/deploy/release) with a clean seam to a future Release Agent: release reasoning is isolated, consults health more strictly (refreshes the verdict first), and never bleeds into generic build execution. - agents/build/build.sh: deterministic entrypoint. Parses mode/action, consults the Health Agent, applies a per-mode gate (build lenient, deploy/release need --force on yellow, red always aborts, unknown collapses to yellow), emits a structured BuildDecision (JSON), then delegates to the matching autobuild capability. --dry-run plans without executing. - agents/build/AGENTS.md: concise agent doc (<200 lines) incl. the BuildDecision schema and mode/gate table. - agents/build/BUILD_CAPABILITIES.md: audit of every PyAutoBuild execution capability the agent calls, plus the boundary finding that the health-shim commands (verify_install, url_check, watch/status/tick/fix) belong to Heart and are refused here — reached via `pyauto-brain health`. - agents/_common.sh: consult_health_agent_verdict helper (agent-to-agent consult, pipefail-safe). - bin/pyauto-brain: register the build agent (listed first). - README.md, AGENTS.md: document the organism, multi-organ coordination, the society-of-agents pattern, and the future Release Agent split. Validated: dispatcher listing, help, all gate policies (green/yellow/red/ unknown across the three modes), health-shim rejection, invalid mode/action, and the full green happy path delegating to a stubbed autobuild. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 41 +++++- README.md | 18 +++ agents/_common.sh | 46 +++++++ agents/build/AGENTS.md | 107 +++++++++++++++ agents/build/BUILD_CAPABILITIES.md | 82 ++++++++++++ agents/build/build.sh | 202 +++++++++++++++++++++++++++++ bin/pyauto-brain | 5 +- 7 files changed, 497 insertions(+), 4 deletions(-) create mode 100644 agents/build/AGENTS.md create mode 100644 agents/build/BUILD_CAPABILITIES.md create mode 100755 agents/build/build.sh diff --git a/AGENTS.md b/AGENTS.md index 0be70be..2e7266d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,6 +71,24 @@ The Brain asks `pyauto-heart readiness --json`, reasons over the result, and onl on a **green** verdict triggers Build's release. Heart never triggers Build; Build never re-derives a decision the Brain already made. +## Brain agents consult one another (a society of agents) + +Brain agents are not limited to driving organs — they can **consult each other**. +The canonical example is the **Build Agent**, which does not query Heart directly: +it consults the **Health Agent**, and only the Health Agent talks to the Heart +organ. So the Build Agent's full chain is: + +``` +Mind → Build Agent → Health Agent → Heart → GREEN/YELLOW/RED + → Build Agent → Build (execute) +``` + +This generalises: a future Feature Agent can ask the Health Agent whether the +tree is fit for a refactor; a future Release Agent can ask the Build Agent to +package a release. Reasoning lives in Brain agents that consult one another; the +organs (Heart, Hands/Build, Memory) provide capabilities and state. The Build +Agent is the reusable template for this pattern. + ## Specialist reasoning agents Each agent is a directory under `agents//` with: @@ -81,19 +99,36 @@ Each agent is a directory under `agents//` with: Current agents: +- **`agents/build/`** — the executive function for execution work. Consults the + Health Agent, reasons over the verdict, and on a healthy result delegates to + the appropriate PyAutoBuild capability. The canonical example of the Brain + coordinating *multiple* organs. Has `build` / `deploy` / `release` modes — + release is isolated as a mode now, with a clean seam to a future Release Agent. - **`agents/release/`** — reasons over `pyauto-heart readiness`, and on green triggers the PyAutoBuild release executor (`autobuild pre_build` → `release.yml`). - **`agents/health/`** — reasons over the PyAutoHeart monitoring/readiness surface. +> **Build Agent vs. release mode vs. the release agent.** The Build Agent owns +> all execution orchestration and keeps release as one of its modes (broad build +> scope: generate, run, aggregate, package, tag). `agents/release/` is the older, +> narrower readiness→`pre_build` driver. The mature architecture splits a +> dedicated **Release Agent** out of the Build Agent's release mode — making +> release-specific decisions (versioning, changelogs, PyPI/tags, human approval), +> consulting the Health Agent *more strictly*, then requesting execution from the +> Build Agent / PyAutoBuild. Until then: one agent now, clean seam for two later. + More specialist agents are expected over time (e.g. a Feature agent that reasons -over PyAutoMind tasks, a Build agent that coordinates execution, several health -agents each reasoning over a different part of Heart); add them as new -`agents//` directories. +over PyAutoMind tasks, Bug / Refactor / Documentation / Research agents, and a +split-out Release agent). The Build Agent is the reusable template — add new +ones as `agents//` directories following its shape (a concise `AGENTS.md`, +a deterministic entrypoint, and a capability audit of any organ it drives). ## Running ```bash bin/pyauto-brain help # list agents +bin/pyauto-brain build # consult health, 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 # one health tick + readiness verdict ``` diff --git a/README.md b/README.md index 3267640..5ec486f 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,32 @@ Brain → Heart (gate) → Build (execute) ## Specialist reasoning agents +- **`agents/build/`** — the executive function for execution work. Consults the + Health Agent, reasons over the verdict, and on a healthy result delegates to + PyAutoBuild. The canonical example of the Brain coordinating multiple organs; + has `build` / `deploy` / `release` modes. - **`agents/release/`** — reasons over `pyauto-heart readiness` → on green, runs the PyAutoBuild release executor. - **`agents/health/`** — reasons over the PyAutoHeart monitoring / readiness surface. +Brain agents can also **consult one another**: the Build Agent doesn't query +Heart directly — it asks the Health Agent, which is the only agent that talks to +the Heart organ. + +``` +Mind → Build Agent → Health Agent → Heart → GREEN/YELLOW/RED + → Build Agent → Build (execute) +``` + +Release is a **mode** of the Build Agent today (because PyAutoBuild owns +release/build/deploy execution), isolated so it can split into a dedicated +**Release Agent** later — one agent now, clean seam for two later. + ## Usage ```bash bin/pyauto-brain help # list agents +bin/pyauto-brain build # consult health, then delegate execution to Build bin/pyauto-brain release # reason about readiness, then release on green bin/pyauto-brain health # one health tick + readiness verdict ``` diff --git a/agents/_common.sh b/agents/_common.sh index e9f0c70..15facdd 100755 --- a/agents/_common.sh +++ b/agents/_common.sh @@ -42,3 +42,49 @@ readiness_verdict() { heart="$(resolve_heart)" || return $? "$heart" readiness --json | python3 -c 'import json,sys; print(json.load(sys.stdin).get("verdict","unknown"))' } + +# _agents_dir — directory holding the sibling agents (this file lives in it). +_agents_dir() { + cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd +} + +# consult_health_agent_verdict [--refresh] — ask the *sibling Health Agent* for +# the readiness verdict, rather than querying PyAutoHeart directly. This is the +# Brain-agent-consults-Brain-agent pattern: a specialist agent reasons *with* +# another specialist agent, and only the Health Agent talks to the Heart organ. +# It keeps the Build Agent decoupled from Heart's surface and lets future agents +# (Feature, Release, ...) consult one another the same way. +# +# --refresh ask the Health Agent to refresh Heart's state first (a fresh +# gate); release-grade work uses this, ordinary build work does not. +# +# Echoes one of: green | yellow | red | unknown. Never fails the caller — an +# unresolvable/again-unknown verdict is reported as "unknown" (treated as YELLOW +# by callers), never silently as green. +consult_health_agent_verdict() { + local refresh=0 + [[ "${1:-}" == "--refresh" ]] && refresh=1 + local health + health="$(_agents_dir)/health/health.sh" + if [[ ! -f "$health" ]]; then + echo "unknown" + return 0 + fi + if [[ "$refresh" -eq 1 ]]; then + bash "$health" tick >/dev/null 2>&1 || true + fi + # Capture into a variable rather than piping straight out: the caller may have + # `set -o pipefail`, under which a non-zero exit from the (possibly + # Heart-less) Health Agent would otherwise double-fire a fallback. The python + # below always prints exactly one token, even on empty/garbage input. + local out + out="$(bash "$health" readiness --json 2>/dev/null | python3 -c ' +import json, sys +try: + v = json.load(sys.stdin).get("verdict", "unknown") + print(v or "unknown") +except Exception: + print("unknown") +' 2>/dev/null)" + printf '%s\n' "${out:-unknown}" +} diff --git a/agents/build/AGENTS.md b/agents/build/AGENTS.md new file mode 100644 index 0000000..dc319e6 --- /dev/null +++ b/agents/build/AGENTS.md @@ -0,0 +1,107 @@ +# Build agent + +The second canonical **PyAutoBrain** reasoning agent, and the reference example +of how the Brain coordinates *multiple* organs. It is the executive function for +execution work: it owns the build *workflow* but delegates the *building* to +PyAutoBuild and the *health decision* to the Health Agent. + +``` +Mind → Build Agent → Health Agent → Heart → GREEN/YELLOW/RED + → Build Agent → PyAutoBuild (execute) +``` + +## Fundamental principle + +**The Build Agent does not build software itself — PyAutoBuild does.** The Build +Agent decides *whether* building should happen, *what* to build, *which* +PyAutoBuild capability to invoke, and *whether to proceed or stop*. It reasons; +PyAutoBuild executes. It must never duplicate PyAutoBuild functionality. + +## Brain agents consult one another + +The Build Agent does not call PyAutoHeart directly. It **consults the sibling +Health Agent**, which is the only agent that talks to the Heart organ. This is +the society-of-agents pattern: specialist Brain agents reason *with* each other, +while the organs (Heart, Hands, Memory) provide capabilities and state. Future +agents generalise the same way — a Feature Agent asking the Health Agent if the +tree is fit for a refactor; a Release Agent asking the Build Agent to package. + +## Modes (one agent now, clean seam for a Release Agent later) + +Release is in scope today because PyAutoBuild currently owns release/build/deploy +execution. It is isolated as its own **mode** so release-specific reasoning never +bleeds into generic build execution, and can split into a dedicated Release Agent +later with no churn to build mode. + +| Mode | Default action | Gate policy | Routes to (PyAutoBuild) | +|------|----------------|-------------|--------------------------| +| `build` | `run_all` | lenient — GREEN/YELLOW proceed, RED aborts | `generate`, `run`, `run_python`, `run_all`, `script_matrix`, `aggregate_results`, `slow_skip_check`, `repro_command`, `bump_colab_urls` | +| `deploy` | `generate` | cautious — GREEN proceeds, YELLOW needs `--force`, RED aborts | `generate`, `bump_colab_urls` | +| `release` | `pre_build` | strict — refreshes health first; GREEN proceeds, YELLOW needs `--force`, RED aborts | `pre_build`, `tag_and_merge`, `generate_release_notes`, `create_analysis_issue`, `aggregate_results` | + +Release consults health *more strictly*: it asks the Health Agent to refresh +Heart's state first (`--refresh`) so a release is never gated on a stale verdict. +An **unknown** verdict collapses to YELLOW — never silently GREEN. + +## Build lifecycle + +1. Receive a build request (mode + action). +2. Validate the action against the mode (reject health-shim commands — those are + Heart's surface, reached via `pyauto-brain health`). +3. Consult the Health Agent for the readiness verdict. +4. Interpret it: **GREEN** proceed · **YELLOW** caution (proceed in build mode, + else `--force`) · **RED** abort with blockers. +5. Invoke the appropriate PyAutoBuild capability. +6. Emit a structured `BuildDecision`. + +## Run + +```bash +bin/pyauto-brain build # build mode, run_all, after health consult +bin/pyauto-brain build generate autolens # build mode, generate notebooks for autolens +bin/pyauto-brain build --dry-run # reason + plan only, do not execute +bin/pyauto-brain build --json generate ag # emit only the BuildDecision JSON +bin/pyauto-brain build --mode deploy --force generate +bin/pyauto-brain build --mode release -- 2 # release mode; forward minor_version 2 to pre_build +``` + +Anything after `--` is forwarded verbatim to the PyAutoBuild subcommand. + +Exit codes: `0` proceeded/delegated (or dry-run) · `2` yellow blocked (use +`--force`) · `3` red blocked · `4` unknown/could-not-consult · `5` invalid +mode/action. + +## BuildDecision (the structured return) + +`--json` (or the `-- BuildDecision --` block) emits: + +```json +{ + "agent": "build", + "mode": "build|deploy|release", + "requested_action": "", + "health_status": "green|yellow|red|unknown", + "decision": "proceed|proceed-with-caution|abort", + "execution_plan": ["autobuild "], + "execution_summary": "", + "warnings": ["..."], + "blockers": ["..."], + "follow_up_recommendations": ["..."], + "dry_run": false +} +``` + +A future Python `BuildAgent().execute(...)` wrapper can return this same shape. + +## What this agent must never do + +- Build, package, tag, or publish anything itself — that is PyAutoBuild's job. +- Query PyAutoHeart directly or re-derive a readiness verdict — consult the + Health Agent. +- Re-own a health-shim command (`verify_install`, `url_check`, `watch`, `status`, + `tick`, `fix`) — those belong to Heart, reached via `pyauto-brain health`. +- Mix release-specific reasoning into generic build execution — keep it in + release mode. + +See [`BUILD_CAPABILITIES.md`](./BUILD_CAPABILITIES.md) for the audit of every +PyAutoBuild capability the agent calls, and the execution/health boundary. diff --git a/agents/build/BUILD_CAPABILITIES.md b/agents/build/BUILD_CAPABILITIES.md new file mode 100644 index 0000000..ff0b619 --- /dev/null +++ b/agents/build/BUILD_CAPABILITIES.md @@ -0,0 +1,82 @@ +# PyAutoBuild capabilities known to the Build Agent + +This audit records the execution surface the PyAutoBrain Build Agent reasons over +and **calls**. The agent must treat every item here as a PyAutoBuild capability, +not as logic to reimplement inside Brain. Source of truth: the `autobuild` +dispatcher (`PyAutoBuild/bin/autobuild`) and `PyAutoBuild/CLAUDE.md`. + +## Execution capabilities (the Build Agent calls these) + +From `autobuild help`: + +| Capability | What it does | Build Agent mode | +|------------|--------------|------------------| +| `pre_build` | Format, regenerate notebooks, push workspaces, then dispatch `release.yml`. | release | +| `tag_and_merge` | Commit and tag every library repo for a release. | release | +| `generate_release_notes` | Generate release notes from merged PRs and create GitHub Releases. | release | +| `create_analysis_issue` | Open a GitHub issue with the release report and assign Copilot. | release | +| `generate` | Convert a workspace's `scripts/` to `notebooks/`. | build, deploy | +| `run` | Execute notebooks in a workspace folder. | build | +| `run_python` | Execute Python scripts in a workspace folder. | build | +| `run_all` | Run scripts across one or more workspaces, produce summary reports. | build | +| `script_matrix` | Output a JSON `{name, directory}` matrix for GitHub Actions. | build | +| `aggregate_results` | Aggregate per-job JSON into a release-readiness report. | build, release | +| `slow_skip_check` | Surface SLOW / NEEDS_FIX entries in workspace `no_run.yaml`. | build | +| `repro_command` | Emit the shell command autobuild uses to run one script. | build | +| `bump_colab_urls` | Rewrite Colab URLs in cwd from an old to a new date-tag. | build, deploy | + +Underlying implementation assets the agent never re-owns: `autobuild/run_python.py`, +`run.py`, `generate.py`, `script_matrix.py`, `aggregate_results.py`, +`tag_and_merge.sh`, `build_util.py`, the `release.yml` GitHub workflow, and the +per-workspace `config/build/{no_run,env_vars,copy_files,visualise_notebooks}.yaml`. + +## Boundary audit — execution vs. health + +PyAutoBuild is meant to be a **pure executor**: it runs no readiness checks of +its own. Confirmed against `PyAutoBuild/CLAUDE.md` ("PyAutoBuild is the executor +… it runs **no** release-readiness checks of its own"). + +It does, however, still expose **health-shim commands** that delegate to the +health authority (named PyAutoPulse in current PyAutoBuild docs; that is +PyAutoHeart, which keeps a `pyauto-pulse` back-compat shim): + +- `verify_install` — shim → health authority (deep install-path checks). +- `url_check` — shim → health authority (forbidden Binder/Colab URL guard). +- `watch` / `status` / `tick` / `fix` — monitoring-daemon shims. + +**Decision:** these are health concerns, not build actions. The Build Agent +**refuses to route them** and points the caller at `pyauto-brain health ` +instead. They belong to PyAutoHeart and are reached through the Health Agent — +never re-owned by the Build Agent, and never duplicated in Brain. No non-trivial +readiness logic was found living *inside* PyAutoBuild itself (the shims only +delegate); if any ever appears, migrate it to PyAutoHeart and leave only +delegation in PyAutoBuild. + +This keeps the architecture clean: + +``` +reasoning → PyAutoBrain (Build Agent, Health Agent) +health → PyAutoHeart (via the Health Agent) +execution → PyAutoBuild (via the Build Agent) +``` + +## Drift note + +Current PyAutoBuild guidance still uses the older name **PyAutoPulse** for the +health authority and **PyAutoAgent** for the reasoning layer in places. The Build +Agent reasons about *categories of capability* (execution vs. health), not fixed +names, so a rename in PyAutoBuild does not break it. When PyAutoBuild gains or +renames an execution subcommand, update the table above; do not encode the list +anywhere the agent must re-derive at runtime beyond the per-mode allowlists in +`build.sh`. + +## Build Agent decision policy (recap) + +- **GREEN** — proceed; invoke the requested PyAutoBuild capability. +- **YELLOW** — build mode proceeds with a warning; deploy/release require + `--force`. An unknown verdict is treated as YELLOW. +- **RED** — abort; surface Heart's blockers (via the Health Agent) and do not + execute. + +The Build Agent may sequence, plan, and explain execution, but it must never run +a build step itself, query Heart directly, or re-derive the readiness verdict. diff --git a/agents/build/build.sh b/agents/build/build.sh new file mode 100755 index 0000000..0a39f92 --- /dev/null +++ b/agents/build/build.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# agents/build/build.sh — the build agent (the 2nd canonical PyAutoBrain agent). +# +# The Build Agent is the executive / orchestration layer for execution work. It +# does NOT build software itself — PyAutoBuild does. The Build Agent decides +# whether building should happen, what to build, which PyAutoBuild capability to +# invoke, and whether to proceed or stop. It reasons; PyAutoBuild executes. +# +# Call chain (the Brain coordinating multiple organs): +# +# Mind -> Build Agent -> Health Agent -> Heart -> GREEN/YELLOW/RED +# -> Build Agent -> PyAutoBuild (execute) +# +# Note the consult step goes through the *sibling Health Agent*, not Heart +# directly: Brain agents consult one another (a society of reasoning agents), +# and only the Health Agent talks to the Heart organ. +# +# Modes (one agent now, clean seam for a future Release Agent): +# build generic execution (generate notebooks, run scripts, aggregate). +# Lenient gate: GREEN/YELLOW proceed, RED aborts. +# deploy publish generated artifacts. Cautious gate: GREEN proceeds, +# YELLOW needs --force, RED aborts. +# release high-stakes release execution (pre_build, tag, release notes). +# Strict gate: refreshes health first; GREEN proceeds, YELLOW needs +# --force, RED aborts. Release reasoning is isolated here so it can +# later split into its own PyAutoBrain Release Agent. +# +# Usage: +# build.sh [--mode build|deploy|release] [--force] [--dry-run] [--json] +# [] [-- ] +# +# --force proceed on a YELLOW verdict (deploy/release). RED always blocks. +# --dry-run reason + plan only; emit the BuildDecision, do not execute. +# --json emit the BuildDecision as JSON only (machine-readable). +# +# Exit codes: 0 proceeded/delegated (or dry-run) · 2 yellow blocked (use +# --force) · 3 red blocked · 4 unknown/could-not-consult · 5 invalid mode/action. + +set -uo pipefail + +HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" +source "$HERE/../_common.sh" + +# ----- mode -> allowed actions, default action, gate strictness ----- +# Actions are PyAutoBuild capabilities. The Build Agent calls them; it never +# reimplements them. Health-shim commands (verify_install, url_check, watch, +# status, tick, fix) are deliberately NOT routable here — those are Heart's +# surface, reached through the Health Agent, never re-owned by Build. +BUILD_ACTIONS="generate run run_python run_all script_matrix aggregate_results slow_skip_check repro_command bump_colab_urls" +DEPLOY_ACTIONS="generate bump_colab_urls" +RELEASE_ACTIONS="pre_build tag_and_merge generate_release_notes create_analysis_issue aggregate_results" +HEALTH_SHIMS="verify_install url_check watch status tick fix" + +mode="build" +force=0 +dry_run=0 +json_only=0 +action="" +forward=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --mode) mode="${2:-}"; shift 2 ;; + --mode=*) mode="${1#*=}"; shift ;; + --force) force=1; shift ;; + --dry-run) dry_run=1; shift ;; + --json) json_only=1; shift ;; + -h|--help) sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; + --) shift; forward=("$@"); break ;; + -*) echo "build agent: unknown flag '$1'" >&2; exit 5 ;; + *) if [[ -z "$action" ]]; then action="$1"; else forward+=("$1"); fi; shift ;; + esac +done + +case "$mode" in + build) allowed="$BUILD_ACTIONS"; default_action="run_all" ;; + deploy) allowed="$DEPLOY_ACTIONS"; default_action="generate" ;; + release) allowed="$RELEASE_ACTIONS"; default_action="pre_build" ;; + *) echo "build agent: unknown mode '$mode' (build|deploy|release)" >&2; exit 5 ;; +esac +[[ -z "$action" ]] && action="$default_action" + +# Reject health-shim commands with a pointer to the right organ. +if [[ " $HEALTH_SHIMS " == *" $action "* ]]; then + echo "build agent: '$action' is a health concern owned by PyAutoHeart, not a" >&2 + echo " build action. Consult it via: pyauto-brain health $action" >&2 + exit 5 +fi +if [[ " $allowed " != *" $action "* ]]; then + echo "build agent: action '$action' is not valid in '$mode' mode." >&2 + echo " Allowed: $allowed" >&2 + exit 5 +fi + +# ----- consult the sibling Health Agent (not Heart directly) ----- +[[ "$json_only" -eq 1 ]] || echo "== build agent ($mode): consulting Health Agent for readiness ==" +if [[ "$mode" == "release" ]]; then + verdict="$(consult_health_agent_verdict --refresh)" +else + verdict="$(consult_health_agent_verdict)" +fi + +# ----- reason: map (mode, verdict) -> decision ----- +# An unknown verdict collapses to YELLOW (caution), never to GREEN or RED. +eff="$verdict" +[[ "$eff" == "unknown" ]] && eff="yellow" + +decision="" ; decision_code=0 +warnings=() ; blockers=() ; follow_up=() + +[[ "$verdict" == "unknown" ]] && warnings+=("Readiness verdict unknown; treated as YELLOW. Run 'pyauto-brain health' to refresh.") + +case "$eff" in + red) + decision="abort"; decision_code=3 + blockers+=("PyAutoHeart reports RED. Resolve the blockers (see 'pyauto-brain health') before $mode work.") + ;; + yellow) + if [[ "$mode" == "build" ]]; then + decision="proceed-with-caution"; decision_code=0 + warnings+=("Health is YELLOW; build mode proceeds, but review warnings before release-grade work.") + else + if [[ "$force" -eq 1 ]]; then + decision="proceed-with-caution"; decision_code=0 + warnings+=("Health is YELLOW; proceeding under --force ($mode mode).") + else + decision="abort"; decision_code=2 + blockers+=("Health is YELLOW and $mode mode requires GREEN. Re-run with --force to accept the caution.") + fi + fi + ;; + green) + decision="proceed"; decision_code=0 + ;; + *) + decision="abort"; decision_code=4 + blockers+=("Could not obtain a readiness verdict ('$verdict').") + ;; +esac + +# ----- execution plan ----- +plan_cmd="autobuild $action" +[[ ${#forward[@]} -gt 0 ]] && plan_cmd+=" ${forward[*]}" +plan=("$plan_cmd") + +if [[ "$mode" == "release" ]]; then + follow_up+=("Release reasoning lives in 'release mode'; it may later split into a dedicated PyAutoBrain Release Agent.") +fi +[[ "$decision_code" -eq 0 && "$dry_run" -eq 0 ]] || true + +# ----- emit the BuildDecision (structured) ----- +summary="$decision via '$plan_cmd' (health=$verdict, mode=$mode)" +[[ "$dry_run" -eq 1 ]] && summary="DRY-RUN: would $summary" + +emit_decision() { + HEALTH="$verdict" MODE="$mode" ACTION="$action" DECISION="$decision" \ + PLAN="${plan[*]}" SUMMARY="$summary" DRYRUN="$dry_run" \ + WARNINGS="$(printf '%s\n' "${warnings[@]:-}")" \ + BLOCKERS="$(printf '%s\n' "${blockers[@]:-}")" \ + FOLLOWUP="$(printf '%s\n' "${follow_up[@]:-}")" \ + python3 -c ' +import json, os +def lines(k): + return [x for x in os.environ.get(k, "").splitlines() if x.strip()] +print(json.dumps({ + "agent": "build", + "mode": os.environ["MODE"], + "requested_action": os.environ["ACTION"], + "health_status": os.environ["HEALTH"], + "decision": os.environ["DECISION"], + "execution_plan": [os.environ["PLAN"]] if os.environ.get("PLAN") else [], + "execution_summary": os.environ["SUMMARY"], + "warnings": lines("WARNINGS"), + "blockers": lines("BLOCKERS"), + "follow_up_recommendations": lines("FOLLOWUP"), + "dry_run": os.environ.get("DRYRUN") == "1", +}, indent=2)) +' +} + +if [[ "$json_only" -eq 1 ]]; then + emit_decision +else + echo "-- BuildDecision --" + emit_decision + echo +fi + +# ----- act on the decision ----- +if [[ "$decision_code" -ne 0 ]]; then + [[ "$json_only" -eq 1 ]] || echo "build agent: $decision — not executing." >&2 + exit "$decision_code" +fi + +if [[ "$dry_run" -eq 1 ]]; then + [[ "$json_only" -eq 1 ]] || echo "build agent: dry-run, not executing '$plan_cmd'." + exit 0 +fi + +autobuild="$(resolve_autobuild)" || exit $? +[[ "$json_only" -eq 1 ]] || echo "== exec: $plan_cmd ==" +exec "$autobuild" "$action" "${forward[@]}" diff --git a/bin/pyauto-brain b/bin/pyauto-brain index b4fd6a9..4d8931d 100755 --- a/bin/pyauto-brain +++ b/bin/pyauto-brain @@ -7,6 +7,7 @@ # the CLIs feel the same. Each subcommand is a specialist reasoning agent under # agents//. # +# pyauto-brain build [args] coordinate execution: consult health, run Build # pyauto-brain release [args] reason about readiness, then release on green # pyauto-brain health [args] reason over the PyAutoHeart health surface # pyauto-brain help [name] list agents or show one agent's docs @@ -21,14 +22,16 @@ BRAIN_HOME="$(cd "$(dirname "$_self")/.." && pwd)" AGENTS_DIR="$BRAIN_HOME/agents" declare -A AGENT_SCRIPT=( + [build]="$AGENTS_DIR/build/build.sh" [release]="$AGENTS_DIR/release/release.sh" [health]="$AGENTS_DIR/health/health.sh" ) declare -A AGENT_DESC=( + [build]="Coordinate execution: consult the Health Agent, then delegate to PyAutoBuild" [release]="Reason about pyauto-heart readiness, then run the Build release on green" [health]="Reason over the PyAutoHeart monitoring / readiness surface" ) -AGENT_ORDER=(release health) +AGENT_ORDER=(build release health) cmd_help() { if [[ $# -gt 0 ]]; then From d76269bebc6d17d0e1db660b4678df6817d76cde Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 14:25:51 +0000 Subject: [PATCH 2/2] fix: harden build agent --mode parsing and drop a dead line Self-review of the Build Agent surfaced one real bug: - `--mode` as the final argument (no value) caused an infinite loop: under `set -uo pipefail` (no errexit), `shift 2` fails when only one positional remains, the loop condition stays true, and `$1` is still `--mode`, so it spins forever. Guard with an explicit arity check that errors (exit 5) when `--mode` has no value. - Removed a leftover no-op line (`[[ ... ]] || true`) that computed and discarded a condition. Co-Authored-By: Claude Opus 4.8 --- agents/build/build.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/agents/build/build.sh b/agents/build/build.sh index 0a39f92..8495d19 100755 --- a/agents/build/build.sh +++ b/agents/build/build.sh @@ -60,7 +60,9 @@ forward=() while [[ $# -gt 0 ]]; do case "$1" in - --mode) mode="${2:-}"; shift 2 ;; + --mode) + [[ $# -ge 2 ]] || { echo "build agent: --mode needs a value (build|deploy|release)" >&2; exit 5; } + mode="$2"; shift 2 ;; --mode=*) mode="${1#*=}"; shift ;; --force) force=1; shift ;; --dry-run) dry_run=1; shift ;; @@ -146,7 +148,6 @@ plan=("$plan_cmd") if [[ "$mode" == "release" ]]; then follow_up+=("Release reasoning lives in 'release mode'; it may later split into a dedicated PyAutoBrain Release Agent.") fi -[[ "$decision_code" -eq 0 && "$dry_run" -eq 0 ]] || true # ----- emit the BuildDecision (structured) ----- summary="$decision via '$plan_cmd' (health=$verdict, mode=$mode)"