diff --git a/AGENTS.md b/AGENTS.md index 0572c1a..63520aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,6 +119,16 @@ humans invoke identically, so behaviour isn't re-derived from prose each time. context and (for risky work) the vitals faculty, and emits a `FeatureDecision` that the existing `start_dev → ship_library/ship_workspace` workflow consumes. It reasons; it never edits source. (Organism-facing name: *Growth Agent*.) +- **`agents/conductors/bug/`** — the organism's **immune system**: recognises a + pathogen (bug, regression, failing test or PyAutoHeart finding), tells it from + benign self, classifies it (severity/scope/type/confidence), consults PyAutoMemory + as immune memory, and mounts a *targeted* response — deciding **where the fix + belongs** (source-first; never degrading a user-facing workspace script, the + autoimmune failure mode) and emitting a `BugDecision` the `start_dev → ship_*` + workflow consumes. Health mode reads two inputs: the live vitals verdict **and** + the filed PyAutoHeart issues. Reuses the Feature Agent's core; consults the vitals + faculty, never Heart directly. It reasons; it never edits source. (Organism-facing + name: *Immune Agent*.) - **`agents/conductors/build/`** — the executive function for execution work. Consults the vitals faculty, reasons over the verdict, and on a healthy result delegates to the appropriate PyAutoBuild capability. The canonical example of @@ -155,8 +165,9 @@ humans invoke identically, so behaviour isn't re-derived from prose each time. > consulting the vitals faculty *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 (Bug / Refactor / Documentation / -Research agents, cost/risk faculties, …). When adding one, **place it by tier**: +More specialist agents are expected over time (Refactor / Documentation / Research +agents, a `diagnosis` faculty split from the Bug Agent, cost/risk faculties, …). +When adding one, **place it by tier**: a side-effecting decider you drive → `agents/conductors//`; a read-only opinion the conductors consult → `agents/faculties//`. Follow the Build Agent's shape (a concise `AGENTS.md` opening with its `Tier:` line, a @@ -194,16 +205,17 @@ to the right agent; normal usage never says "PyAutoBrain". | Command | Routes to | Tier | |---------|-----------|------| | `/feature` | Feature Agent → `start_dev` | real conductor | +| `/bug` | Bug Agent → `start_dev` (health mode → vitals + Heart issues) | real conductor | | `/build` | Build Agent → vitals → Heart → PyAutoBuild | real conductor | | `/health` | Health Agent loop → vitals → Heart | real conductor | -| `/bug` `/refactor` `/docs` `/research` | `start_dev` pre-tagged with the work-type | work-type entry* | +| `/refactor` `/docs` `/research` | `start_dev` pre-tagged with the work-type | work-type entry* | | `/route ` | infers the work-type and dispatches to one of the above | NL router | | `/brain ` | raw `bin/pyauto-brain` passthrough | debug door | -\* No dedicated Bug/Refactor/Docs/Research conductor exists yet — those verbs -route through the Brain dev-flow with their PyAutoMind work-type fixed (still -through the Brain, nothing bypassed), until each earns promotion to its own -conductor. Every command routes **through** the Brain; none replaces it. +\* No dedicated Refactor/Docs/Research conductor exists yet — those verbs route +through the Brain dev-flow with their PyAutoMind work-type fixed (still through the +Brain, nothing bypassed), until each earns promotion to its own conductor (as `/bug` +now has). Every command routes **through** the Brain; none replaces it. The command bodies live in `skills//.md` (thin; installed as flat commands by `bin/install.sh`); the shared architecture prose is in diff --git a/README.md b/README.md index 3dcf8db..7944f9e 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,9 @@ faculties multiply behind them. - **`agents/conductors/feature/`** — the growth function: reasons over PyAutoMind `feature/*` intent and plans how the organism grows. +- **`agents/conductors/bug/`** — the immune system: recognises a bug / regression / + failing test / PyAutoHeart finding, classifies it, decides where the fix belongs + (source-first, never degrading a user-facing script), and plans the repair. - **`agents/conductors/build/`** — the executive function for execution work. Consults the vitals faculty, reasons over the verdict, and on a healthy result delegates to PyAutoBuild. Has `build` / `deploy` / `release` modes. @@ -108,11 +111,11 @@ natural language) and it routes to the right agent — normal usage never says > **Users speak in short commands; PyAutoBrain performs the routing.** -- **Real conductors:** `/feature` → Feature Agent, `/build` → Build Agent, - `/health` → Health Agent (each → vitals/Heart/Build as needed). -- **Work-type entries:** `/bug` `/refactor` `/docs` `/research` route through the - Brain dev-flow (`start_dev`) with their PyAutoMind work-type fixed — honest - interim doors until each earns its own conductor. +- **Real conductors:** `/feature` → Feature Agent, `/bug` → Bug Agent, `/build` → + Build Agent, `/health` → Health Agent (each → vitals/Heart/Build as needed). +- **Work-type entries:** `/refactor` `/docs` `/research` route through the Brain + dev-flow (`start_dev`) with their PyAutoMind work-type fixed — honest interim + doors until each earns its own conductor. - **Router + debug:** `/route ` infers the work-type and dispatches; `/brain ` is the raw passthrough. Every command routes **through** the Brain; none replaces it. diff --git a/agents/conductors/bug/AGENTS.md b/agents/conductors/bug/AGENTS.md new file mode 100644 index 0000000..4f8f45a --- /dev/null +++ b/agents/conductors/bug/AGENTS.md @@ -0,0 +1,140 @@ +# Bug agent + +> **Tier: conductor** — a front-door agent you *drive*. The organism's +> **immune system**: it recognises a pathogen (a bug, regression, failing test or +> PyAutoHeart finding), tells it from benign self, types the threat, recalls whether +> it has met it before, and mounts a *targeted* response — neutralising the defect at +> its source without harming healthy tissue. It *consults* the read-only vitals +> faculty (`--check-health`), never queries Heart directly, and never wields the +> scalpel itself. (Organism-facing name: *Immune Agent*.) + +The **immune response** of PyAutoBrain. Where the Feature Agent grows the organism and +the Health conductor keeps it in homeostasis, the Bug Agent answers a *specific threat*: +it detects and classifies a defect, tells a real pathogen from benign self (an expected +failure, a flaky test, a mis-filed feature), consults immune **memory** (PyAutoMemory) +for prior/recurring/flaky cases, and produces a precise repair plan the `start_dev → +ship_*` workflow executes. It reasons; it does not edit source. + +``` +report / failing test / issue / PyAutoHeart finding + → Bug Agent → start_dev → start_library / ship_library + → start_workspace / ship_workspace + consults ↘ ↙ consults + vitals faculty PyAutoMemory (recurring failures, prior fixes, flaky tests) +``` + +## Fundamental principle — a precise response, no autoimmunity + +A healthy immune response neutralises the pathogen at its source and spares healthy +tissue. The most delicate tissue here is the **user-facing workspace scripts — they are +documentation.** A fix that injects test env-vars, hard-codes a path, mutates +`os.environ`, or drops a silent guard into a tutorial script is an **autoimmune +reaction** — it damages what it exists to protect. So before proposing any patch the Bug +Agent asks *where the fix belongs*, and strongly prefers a **general fix in library +source**. It edits a workspace script only when the defect truly lives there, never in a +way that reduces clarity; sanctioned knobs go through `config/build/env_vars.yaml` / +`no_run.yaml`, not inline edits. This surfaces as the `Fix locus:` field of every +decision. See [`BUG_TAXONOMY.md`](./BUG_TAXONOMY.md) for the full fix-locus rules. + +## It reasons; it does not build + +The Bug Agent never edits source, opens PRs, or runs builds — that is the existing +`start_dev` / `ship_*` workflow (and PyAutoBuild at release). It never *runs* tests or +health checks either: reproduction means **identifying** the repro command or the Heart +check, and validation is delegated to the vitals faculty. It emits a `BugDecision`; the +workflow acts on it. + +## Four modes + +| Mode | Trigger | What it does | +|------|---------|--------------| +| **specific** | a `bug/…md` path (or a report) is given | Classify (severity/scope/type/confidence), locate the owner, decide the fix locus + strategy, and produce a `start_dev`-ready plan. | +| **selection** | nothing given | Scan `bug/**`, rank **severity-first** (a bug list is a triage queue), down-rank in-flight work (`active.md`/`planned.md`), and recommend the next bug — with the reason. | +| **difficulty-constrained** | `--difficulty` / `--model` / `--budget` / `--ambitious` / `--impact` | Estimate difficulty per bug and select to match (easy/weak/limited-token → small; ambitious → large; impact → highest severity). | +| **health** | `health` subcommand | Read **two** health inputs — the live **vitals verdict** and the **filed PyAutoHeart GitHub issues** — and route real defects to `bug/health_fixes/`. | + +## Classification + +Every decision types the threat (heuristic first pass — the reasoning layer refines it): + +- **severity:** `critical | high | medium | low` +- **scope:** `single-file | single-repo | multi-repo | ecosystem` +- **type:** `test-failure | runtime-error | wrong-result | docs-error | workflow-error | config-error | release-error | flaky | unknown` +- **confidence:** `high | medium | low` + +If a `bug/` prompt is really a feature, refactor, docs or research task, the agent says +so (`rehome_suggestion`) instead of planning a fix. + +## Health mode — two inputs, one router + +The bug can come from PyAutoHeart. `bug.sh health` gathers both signals and hands them +to `_bug.py`, which emits a first-pass **category hint** per finding (real-bug / config / +flaky / expected) that the reasoning layer confirms before deciding whether the fix +belongs in the affected repo, PyAutoHeart, PyAutoBuild or PyAutoBrain: + +1. the **live vitals verdict** — via the vitals faculty (never Heart directly); +2. the **filed PyAutoHeart issues** — `gh issue list --repo PyAutoLabs/PyAutoHeart` + (`$PYAUTO_HEART_REPO` overridable), the durable findings Heart authored. + +Findings hinted as real defects become `PyAutoMind/bug/health_fixes/.md` prompts +(its README already cites Heart issue #27); flaky/expected findings are left to the +Health conductor. + +> **Boundary with the Health conductor.** The Health conductor drives the assess → +> triage → dispatch loop toward GREEN — its cut is *validation + recommend, no edit-in +> fixes*. The Bug Agent is that deferred edit-in-fix arm: Health hands it a red that is a +> genuine *code* failure, and the Bug Agent turns it into a repair plan. No duplicated +> triage, no re-implemented Heart checks. + +## BugDecision (the structured return) + +``` +Bug · Mode · Classification (severity / scope / type / confidence) · Likely owner +Reproduction (known / unknown / PyAutoHeart check) · Relevant context (PyAutoMemory) +Fix locus (library-source-first · workspace-config · workspace-script[justified] · infra) +Fix strategy (direct · investigate-first · split-into-phases · defer/re-home) +Recommended workflow (library | workspace | combined | infrastructure) +Health validation (vitals checks required before shipping) · Risks · Next action +``` + +`--json` returns the same shape (JSON-consistent with the Feature Agent's +`FeatureDecision`, plus `classification` and `fix_locus`), so a future Python +`BugAgent().decide(...)` can return it verbatim. + +## Run + +```bash +bin/pyauto-brain bug # selection mode (severity-first) +bin/pyauto-brain bug bug/autoarray/rect_adapt.md # specific mode +bin/pyauto-brain bug select --difficulty easy # easy bug for limited tokens +bin/pyauto-brain bug select --impact # highest-severity bug +bin/pyauto-brain bug health # vitals verdict + Heart issue scan +bin/pyauto-brain bug --json bug/autofit/x.md # machine-readable BugDecision +bin/pyauto-brain bug --check-health bug/autolens/x.md # also annotate with the vitals verdict +``` + +Exit codes mirror the Feature Agent: `0` produced a decision · `4` no prompts / +could-not-resolve mind · `5` bad usage. The analysis core (`_bug.py`) is stdlib-only, +does no network/Git, and never writes — `bug.sh` feeds it the verdict + Heart issues. + +## Faculties (a seam, not yet built) + +The Bug Agent ships as a **conductor only**, consulting the existing `vitals` faculty. +Its pure, side-effect-free reasoning — classify + locate + fix-locus — is the shape of a +future read-only **`diagnosis` faculty** (reusable by the Feature Agent's re-homing and +the Health conductor). Keeping the conductor set small, that split is deferred with a +clean seam, exactly as Release stayed a mode of Build. See +[`BUG_TAXONOMY.md`](./BUG_TAXONOMY.md). + +## What this agent must never do + +- Edit source, open PRs, or run builds — that is `start_dev` / `ship_*` / PyAutoBuild. +- Run tests or health checks, or re-implement a PyAutoHeart check — consult the vitals + faculty (`--check-health`) and let Heart measure. +- Query PyAutoHeart directly — only the vitals faculty talks to the Heart organ. +- **Degrade a user-facing workspace script** to mask a symptom (the autoimmune failure + mode) — prefer a general library-source fix. +- Just pick the first bug in selection mode — rank severity-first and explain the choice. + +See [`BUG_TAXONOMY.md`](./BUG_TAXONOMY.md) for the classification taxonomy, the fix-locus +rules, the two health inputs, and the reuse of the Feature difficulty heuristic. diff --git a/agents/conductors/bug/BUG_TAXONOMY.md b/agents/conductors/bug/BUG_TAXONOMY.md new file mode 100644 index 0000000..b155b1e --- /dev/null +++ b/agents/conductors/bug/BUG_TAXONOMY.md @@ -0,0 +1,116 @@ +# Bug taxonomy & surfaces known to the Bug Agent + +This audit records how the Bug Agent **classifies** a defect, decides **where the fix +belongs**, and which **health inputs** it reads. It mirrors the Feature Agent's +[`../feature/MIND_TAXONOMY.md`](../feature/MIND_TAXONOMY.md) and reuses that agent's +deterministic core by import (repo/target parsing, the difficulty heuristic, PyAutoMemory +routing, in-flight down-ranking all live in `_feature.py`). The Bug Agent adds only the +bug-specific reasoning in `_bug.py`. Sources of truth: `PyAutoMind/ROUTING.md`, +`PyAutoBrain/AGENTS.md`, `agents/faculties/vitals/`, `PyAutoMind/bug/health_fixes/`. + +## Classification (typing the threat) + +A first-pass heuristic (`classify()` in `_bug.py`), deliberately transparent so the +reasoning layer can override it — exactly as the difficulty heuristic is. + +| Axis | Values | How it is derived | +|------|--------|-------------------| +| **severity** | critical · high · medium · low | keyword signals (crash/release-block → critical; regression/wrong-result → high; typo/cosmetic → low); a ≥3-repo blast radius nudges up. | +| **scope** | single-file · single-repo · multi-repo · ecosystem | repo blast radius: ≤1 → single-repo, 2 → multi-repo, ≥3 → ecosystem. | +| **type** | test-failure · runtime-error · wrong-result · docs-error · workflow-error · config-error · release-error · flaky · unknown | first matching signal group in `TYPE_ORDER`; genuine-defect types rank above administrative ones. | +| **confidence** | high · medium · low | high when the type is clear and no ambiguity keywords fire; low when the type is `unknown` or the report is exploratory. | + +**Signal discipline.** Type signals are specific on purpose: generic words ("workflow", +"pipeline") appear in ordinary science prompts and would mis-type a real defect, so they +are omitted; a specific marker like `docstring` for `docs-error` is kept because it is +not generic. When the lists drift, edit `TYPE_SIGNALS` / `TYPE_ORDER` in `_bug.py` and +this table together — never encode them where the agent re-derives at runtime. + +## Fix locus — the targeted response (no autoimmunity) + +The immune system's core decision: *where does the fix belong?* The strong prior is a +**general fix in library source**; a user-facing workspace script is documentation and +must not be degraded to squash a symptom. `fix_locus()` returns a locus + a caution: + +| Situation | Locus | Rule | +|-----------|-------|------| +| target is an organ (PyAutoBrain/Heart/Build/Mind/Memory) | **infrastructure** | fix the organ; keep the health/exec/reasoning boundaries intact. | +| any **library** repo resolves | **library source (general fix)** | fix the class of failure at the source. If a workspace also appears, do **not** touch its scripts to mask it. | +| **workspace** only, `config-error` | **workspace config** | use `config/build/env_vars.yaml` / `no_run.yaml` — never inline edits to the script body. | +| **workspace** only, other type | **workspace source-first** | first ask whether the real defect is upstream in library source; edit the script only if the defect truly lives there, and never in a way that reduces clarity. | +| nothing resolves | **unresolved** | locate the owning repo before deciding. | + +**The autoimmune failure mode** (what agents do *without* this context, and must not): +injecting test env-vars into a script, hard-coding an output path, mutating +`os.environ`, adding a silent guard that swallows bad data, or rebaselining a tutorial +to conceal a library regression. Each trades the script's didactic clarity for a local +symptom fix — damaging the tissue the fix exists to protect. Prefer the general source +fix; when a workspace knob is genuinely needed, it goes through the sanctioned config +files, not the script. + +## Reproduction (identify, never run) + +`reproduction()` returns *known / unknown / a PyAutoHeart check* — the Bug Agent +**names** the reproduction; it never executes it (that is the vitals faculty / Heart / +smoke tests). A Heart-derived finding carries its check name; a test-failure names the +failing test to run; a traceback in the prompt is a known repro; an exploratory report +is `unknown` and forces `investigate-first`. + +## Fix strategy & workflow + +`fix_strategy()` → `direct · investigate-first · split-into-phases · defer/re-home`: +low confidence or human-judgement → investigate first; large/too-large → phase it +(prefer several small shippable fix PRs); a mis-filed prompt → defer/re-home. The +`recommended_workflow` is always a development path — `library | workspace | combined | +infrastructure` — because a bug still ships through `start_library` / `start_workspace`; +a bug is never re-homed to `research/` just because no repo resolved. + +## The two health inputs (health mode) + +PyAutoHeart measures health; the Bug Agent reasons about failures and must **not** +re-implement a Heart check. `bug.sh health` reads two complementary signals: + +1. **Live vitals verdict** — `consult_vitals_verdict` (the vitals faculty; only it talks + to Heart). "What is RED/YELLOW right now." +2. **Filed PyAutoHeart issues** — `gh issue list --repo PyAutoLabs/PyAutoHeart --state + open` (`$PYAUTO_HEART_REPO` overridable). The durable, detailed findings Heart + authored (e.g. #27 release-fidelity, #19/#7 degraded-health, #10 url-check). + +For each finding `_bug.py` emits a first-pass **category hint** (real-bug / config / +flaky / expected, from the issue title + labels); the reasoning layer confirms it and +decides where the fix belongs (affected repo, PyAutoHeart, PyAutoBuild, PyAutoBrain). +Confirmed real defects become `PyAutoMind/bug/health_fixes/.md` prompts and enter +the normal workflow; flaky / expected findings are left to the Health conductor's loop. +Validation after patching is always the vitals faculty (`pyauto-heart readiness` GREEN, +or acknowledged YELLOW), never a check re-run here. + +## Difficulty & selection (reused, severity-weighted) + +Difficulty is the Feature Agent's heuristic verbatim (`F.estimate_difficulty` — repos +affected, prompt size, scientific complexity, architectural risk, test burden, memory +context; thresholds `≤2 small · ≤5 medium · ≤9 large · >9 too-large`). Selection scans +`bug/**` (excluding `README.md`), down-ranks paths referenced in `active.md` / +`planned.md`, and ranks **severity-first** by default and under `--impact` (a bug list is +a triage queue); `--difficulty easy` / `--budget` / `--model weak` flip to smallest-first +for limited-token runs; `--ambitious` / `--model strong` prefer the largest. + +## The diagnosis-faculty seam + +The pure, side-effect-free reasoning here — `classify()` + `likely_owner()` + +`fix_locus()` — is the shape of a future read-only **`diagnosis` faculty** under +`agents/faculties/diagnosis/`, reusable by the Feature Agent's re-homing and the Health +conductor's triage. It is kept inline in the Bug conductor for v1 (keep the conductor set +small; don't multiply faculties prematurely), with this as the documented seam. + +## Boundary audit — reasoning vs. health vs. execution + +``` +intent → PyAutoMind (bug/* prompts, bug/health_fixes/, active/planned state) +reasoning → PyAutoBrain (Bug Agent — this; reuses the Feature core) +knowledge → PyAutoMemory (recurring failures / prior fixes / flaky tests; cited, not invented) +health → PyAutoHeart (via the vitals faculty + filed Heart issues; never re-implemented) +execution → PyAutoBuild (via start_dev / ship_* — never run by this agent) +``` + +No execution, health-checking, or knowledge-authoring logic lives in the Bug Agent. It +detects, classifies, locates, and plans — then hands a `BugDecision` to the workflow. diff --git a/agents/conductors/bug/_bug.py b/agents/conductors/bug/_bug.py new file mode 100755 index 0000000..38a8b91 --- /dev/null +++ b/agents/conductors/bug/_bug.py @@ -0,0 +1,521 @@ +#!/usr/bin/env python3 +"""agents/conductors/bug/_bug.py — analysis core for the PyAutoBrain Bug Agent. + +The Bug Agent is the *immune system* of PyAutoBrain: it recognises a pathogen (a +bug, regression, failing test or PyAutoHeart finding), tells it from benign self, +types the threat, recalls prior cases (PyAutoMemory), and produces a structured +BugDecision that the existing workflow (start_dev -> start_library/start_workspace +-> ship_library/ship_workspace) consumes. It does NOT implement code. + +This module is the deterministic part: it discovers bug prompts, classifies each +(severity/scope/type/confidence), locates the likely owner, reasons about the +*fix locus* (source-first, never degrade a user-facing workspace script), and — in +health mode — routes a live verdict + filed PyAutoHeart issues to bug/health_fixes/. + +It reuses the Feature Agent's core by import (minimal-refactor share): repo/target +parsing, the difficulty heuristic, PyAutoMemory routing, and in-flight down-ranking +all come from _feature.py so the two agents cannot drift. It is stdlib-only, does no +network or Git (bug.sh feeds it the verdict + Heart issues), and never writes. +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +# Reuse the Feature Agent's deterministic core rather than copying it (the user +# picked "share, minimal refactor"). Only the bug-specific reasoning lives here. +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "feature")) +import _feature as F # noqa: E402 + +# --- bug classification taxonomy (mirrors BUG_TAXONOMY.md) ------------------- +# Repos that are neither library nor workspace: fixes here are *infrastructure* +# (the organs themselves), not library-source or workspace work. +INFRA_TARGETS = { + "pyautobrain": "PyAutoBrain", "pyautoheart": "PyAutoHeart", + "pyautobuild": "PyAutoBuild", "autobuild": "PyAutoBuild", + "pyautomind": "PyAutoMind", "pyautomemory": "PyAutoMemory", +} + +# A file path referenced in the prompt (e.g. @PyAutoArray/.../inversion.py, foo.yaml). +# Used to detect single-file scope. +FILE_RE = re.compile(r"[\w./@-]+\.(?:py|md|yaml|yml|rst|ipynb|cfg|toml|sh)\b") + +# type -> word-boundary-prefix signals (matched with _feature._hits). Signals are +# deliberately specific: generic words ("workflow", "pipeline") appear in ordinary +# science prompts and would mis-type a real defect, so they are omitted (a specific +# marker like "docstring" for docs-error is kept — it is not generic). +TYPE_SIGNALS = { + "flaky": ["flaky", "intermittent", "non-determin", "race condition", "sometimes fail"], + "release-error": ["pypi", "wheel", "testpypi", "colab url", "release-block", + "release timeout", "tag_and_merge"], + "config-error": ["yaml", "env var", "environment variable", "no_run", + "config/build", "config file"], + "wrong-result": ["wrong", "incorrect", "mismatch", "offset", "inaccurate", + "misalign", "divergence", "parity", "not positive-definite", + "wrong formula", "wrong value"], + "runtime-error": ["traceback", "exception", "crash", "raise ", "segfault", + "error:", "attributeerror", "keyerror", "valueerror", + "typeerror", "nan", "overflow"], + "test-failure": ["failing test", "test fail", "pytest", "assertion", "unit test"], + "docs-error": ["docstring", "typo", "broken link", ".rst", "autosummary"], + "workflow-error": ["start_dev", "ship_", "worktree", "slash command", "skill file"], +} +# Ordered specific -> generic; first hit wins. Genuine-defect types (runtime, +# wrong-result, test-failure) rank above administrative ones (docs, workflow). +TYPE_ORDER = ["flaky", "release-error", "config-error", "runtime-error", + "wrong-result", "test-failure", "docs-error", "workflow-error"] + +SEVERITY_SIGNALS = { + "critical": ["crash", "release-block", "data loss", "all scripts", "cannot install", + "broken release", "blocks release", "timeout"], + "high": ["regression", "wrong result", "incorrect", "fails on main", "blocks", + "diverge", "not positive-definite", "nan"], + "low": ["typo", "cosmetic", "minor", "docstring", "small offset", "nit"], +} + + +def classify(p: dict, factors: dict) -> dict: + """Type the threat: severity | scope | type | confidence.""" + text = p["text"] + + # type — first signal group with a hit, else the work-type hint, else unknown. + btype = "unknown" + for t in TYPE_ORDER: + if F._hits(text, TYPE_SIGNALS[t]): + btype = t + break + + # severity — explicit signal beats the default; blast radius nudges upward. + severity = "medium" + if F._hits(text, SEVERITY_SIGNALS["critical"]): + severity = "critical" + elif F._hits(text, SEVERITY_SIGNALS["high"]): + severity = "high" + elif F._hits(text, SEVERITY_SIGNALS["low"]): + severity = "low" + if factors["repos_affected"] >= 3 and severity in ("low", "medium"): + severity = "high" + + # scope — from the repo blast radius; single-file when ≤1 repo and the prompt + # points at exactly one file. + n = factors["repos_affected"] + n_files = len({m.lstrip("@") for m in FILE_RE.findall(text)}) + if n >= 3: + scope = "ecosystem" + elif n == 2: + scope = "multi-repo" + elif n_files == 1: + scope = "single-file" + else: + scope = "single-repo" + + # confidence — high when the type is clear and a repro is implied; low when the + # report is ambiguous or the type could not be resolved. + confidence = "medium" + if btype != "unknown" and not factors["human_judgement"]: + confidence = "high" + if btype == "unknown" or factors["human_judgement"]: + confidence = "low" + + return {"severity": severity, "scope": scope, "type": btype, + "confidence": confidence} + + +def reproduction(p: dict, cls: dict, heart_check: str | None) -> str: + """known / unknown / a PyAutoHeart check — never run here, only identified.""" + if heart_check: + return f"PyAutoHeart check: {heart_check} (reproduce via the vitals faculty)" + text = p["text"].lower() + if cls["type"] == "test-failure" or "pytest" in text or "failing test" in text: + return "known: identify + run the failing test (via the vitals faculty; not here)" + if "traceback" in text or "```" in p["text"]: + return "known: from the reported traceback / error in the prompt" + if cls["confidence"] == "low": + return "unknown: establish a reliable reproduction before patching" + return "known: reproduce the reported behaviour on a clean main checkout first" + + +def likely_owner(p: dict, factors: dict) -> str: + """The repo or workflow that most likely owns the defect.""" + infra = INFRA_TARGETS.get(F.normalise_repo(p["target"])) + if infra: + return f"{infra} (infrastructure)" + libs = factors["library_repos"] + wsp = factors["workspace_repos"] + if libs: + return ", ".join(libs) + (" (+ workspace)" if wsp else "") + if wsp: + return ", ".join(wsp) + return "unresolved — locate the owning repo before planning" + + +def fix_locus(p: dict, factors: dict, cls: dict) -> dict: + """WHERE the fix belongs — the immune system's targeted-response decision. + + Strong prior: prefer a *general* library-source fix; a user-facing workspace + script is documentation and must not be degraded to squash a symptom (that is + the autoimmune failure mode). Returns {locus, note}. + """ + infra = INFRA_TARGETS.get(F.normalise_repo(p["target"])) + if infra: + return {"locus": f"infrastructure ({infra})", + "note": "fix the organ itself; keep health/exec/reasoning boundaries intact."} + libs = factors["library_repos"] + wsp = factors["workspace_repos"] + + if libs: + note = ("general fix in library source resolves the whole class of failure.") + if wsp: + note += (" Do NOT edit the workspace scripts to mask it — they are " + "user-facing documentation (no injected env-vars / hard-coded " + "paths / os.environ mutation / silent guards).") + return {"locus": "library source (general fix)", "note": note} + + if wsp: + if cls["type"] == "config-error": + return {"locus": "workspace config (config/build/*.yaml)", + "note": "use the sanctioned knobs (env_vars.yaml / no_run.yaml), " + "never inline edits to the script body."} + return {"locus": "workspace source-first (verify a library fix cannot generalise it)", + "note": "workspace scripts are documentation: only edit the script if the " + "defect truly lives there, and never in a way that reduces clarity. " + "First ask whether the real defect is upstream in library source."} + + return {"locus": "unresolved", "note": "locate the owning repo before deciding the fix locus."} + + +def fix_strategy(level: str, cls: dict, factors: dict, rehome: str | None) -> str: + """direct | investigate-first | split-into-phases | defer/re-home.""" + if rehome: + return f"defer/re-home (looks like a {rehome}/ task, not a bug)" + if cls["confidence"] == "low" or factors["human_judgement"]: + return "investigate-first (reproduce + confirm root cause before patching)" + if level in ("large", "too-large"): + return "split-into-phases (prefer several small shippable fix PRs)" + return "direct fix" + + +def recommended_workflow(p: dict, factors: dict) -> str: + """library | workspace | combined | infrastructure — from the repo blast radius. + + Unlike the Feature Agent (which passes a bug/ work-type straight through as the + label), the Bug Agent always maps to a *development path*: a bug still ships via + start_library / start_workspace. A bug with no repo resolved is investigation on + the most-likely owner, never a research re-home. + """ + if INFRA_TARGETS.get(F.normalise_repo(p["target"])): + return "infrastructure" + lib, wsp = factors["library_repos"], factors["workspace_repos"] + if lib and wsp: + return "combined" + if lib: + return "library" + if wsp: + return "workspace" + return "library" # unresolved defaults to library source — most bugs live there + + +def health_validation(workflow: str, factors: dict) -> str: + """Which health signals to confirm via the vitals faculty before shipping. + + The ship gate is GREEN, or YELLOW with explicit acknowledgement (per the + ship_* readiness policy) — never a check the Bug Agent re-runs itself. + """ + checks = ["unit tests (lib-tests)"] if workflow in ("library", "combined", "infrastructure") else [] + if workflow in ("workspace", "combined"): + checks.append("workspace/integration validation (test_run)") + checks.append("pyauto-heart readiness GREEN/YELLOW before ship") + return "Consult the vitals faculty after patching: " + ", ".join(checks) + \ + ". The Bug Agent never re-runs these itself." + + +def re_home_check(p: dict) -> str | None: + """If a bug/ prompt is really a feature/refactor/docs/research task, say so.""" + wt = p["work_type"] + if wt != "bug": + return None + text = p["text"].lower() + # A genuine defect signal keeps it a bug even if other words also fire. + defect = F._hits(text, ["bug", "regression", "crash", "traceback", "wrong", + "incorrect", "fail", "exception", "broken"]) + if F._hits(text, ["new feature", "add support for", "implement a new", "capability"]): + return "feature" + if F._hits(text, ["refactor", "restructure", "rename", "clean up", "tidy"]) and not defect: + return "refactor" + if F._hits(text, ["docstring", "tutorial", "typo", "broken link", ".rst", + "documentation"]) and not defect: + return "docs" + if F._hits(text, ["unclear", "investigate", "explore", "open question", + "not sure", "design decision", "research"]) and not defect: + return "research" + return None + + +def analyse_bug(p: dict, heart_check: str | None = None) -> dict: + level, score, factors = F.estimate_difficulty(p) + cls = classify(p, factors) + workflow = recommended_workflow(p, factors) + mishome = re_home_check(p) + locus = fix_locus(p, factors, cls) + return { + "selected_task": p["path"], + "work_type": p["work_type"], + "target": p["target"], + "repos_affected": p["repos"], + "classification": cls, + "likely_owner": likely_owner(p, factors), + "reproduction": reproduction(p, cls, heart_check), + "memory_context": F.memory_context(p), + "fix_locus": locus, + "fix_strategy": fix_strategy(level, cls, factors, mishome), + "recommended_workflow": workflow, + "rehome_suggestion": mishome, + "difficulty": level, + "difficulty_score": score, + "difficulty_factors": factors, + "health_validation": health_validation(workflow, factors), + "risks": F.risks(level, factors, workflow), + } + + +def _next_action(d: dict) -> str: + if d["rehome_suggestion"]: + return f"Re-home under {d['rehome_suggestion']}/ — this is not (only) a bug." + if d["fix_strategy"].startswith("investigate"): + return f"Reproduce {d['selected_task']} on clean main, confirm root cause, then start_dev." + if d["fix_strategy"].startswith("split"): + return "Split into small fix phases, then run start_dev on phase 1." + return f"Run start_dev on {d['selected_task']} (workflow: {d['recommended_workflow']})." + + +# --- discovery + selection (bug/**) ------------------------------------------ +def discover_bugs(mind: Path): + bug = mind / "bug" + return sorted(bug.rglob("*.md")) if bug.is_dir() else [] + + +def _referenced_bug_paths(mind: Path): + """Bug prompt paths mentioned in active.md / planned.md (in-flight work). + + The Feature Agent's `_referenced_paths` only matches `feature/…` paths, so the + Bug Agent needs its own `bug/…`-aware scan to down-rank work already moving. + """ + refs = set() + for n in ("active.md", "planned.md"): + f = mind / n + if f.is_file(): + for m in re.findall(r"[\w./-]*bug/[\w./-]+\.md", f.read_text(errors="replace")): + refs.add(m.split("PyAutoMind/")[-1].lstrip("/")) + return refs + + +SEV_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3} + + +def select_bug(mind: Path, constraint: dict, limit: int): + prompts = [p for p in discover_bugs(mind) if p.name.lower() != "readme.md"] + in_flight = _referenced_bug_paths(mind) + rows = [] + for path in prompts: + p = F.parse_prompt(path, mind) + level, score, factors = F.estimate_difficulty(p) + cls = classify(p, factors) + rows.append({ + "path": p["path"], "difficulty": level, "score": score, + "severity": cls["severity"], "type": cls["type"], + "in_flight": p["path"] in in_flight, + "factors": factors, + }) + + want = constraint.get("difficulty") + model = constraint.get("model") + budget = constraint.get("budget") + + def keyfn(r): + penalty = 100 if r["in_flight"] else 0 + # A bug list is a triage queue, so the default (and --impact) is + # severity-first — critical bugs surface before an easy medium one. + if constraint.get("ambitious"): + return (penalty, SEV_RANK[r["severity"]], -r["score"]) + # easy / weak-model / budget -> smallest first, worse severity breaking ties. + if want in ("easy", "small") or model == "weak" or budget: + return (penalty, r["score"], SEV_RANK[r["severity"]]) + return (penalty, SEV_RANK[r["severity"]], r["score"]) + + candidates = rows + if want and want != "easy": + candidates = [r for r in rows if r["difficulty"] == want] or rows + elif want == "easy": + candidates = [r for r in rows if r["difficulty"] in ("small", "medium")] or rows + if model == "weak" or budget: + candidates = [r for r in candidates if r["difficulty"] in ("small", "medium")] or candidates + + return sorted(candidates, key=keyfn)[:limit], len(prompts) + + +# --- emit --------------------------------------------------------------------- +def emit_human(mode: str, d: dict): + c = d["classification"] + print("== BugDecision ==") + print(f"Bug: {d['selected_task']}") + print(f"Mode: {mode}") + print(f"Classification: severity={c['severity']} scope={c['scope']} " + f"type={c['type']} confidence={c['confidence']}") + print(f"Likely owner: {d['likely_owner']}") + print(f"Reproduction: {d['reproduction']}") + if d["memory_context"]: + print("Relevant context (PyAutoMemory — consult, do not invent):") + for wiki, kws in d["memory_context"].items(): + print(f" - {wiki}/index.md ({', '.join(kws)})") + else: + print("Relevant context: none matched") + print(f"Fix locus: {d['fix_locus']['locus']}") + print(f" ↳ {d['fix_locus']['note']}") + print(f"Fix strategy: {d['fix_strategy']}") + print(f"Recommended workflow: {d['recommended_workflow']}", end="") + print(f" [re-home as {d['rehome_suggestion']}/]" if d["rehome_suggestion"] else "") + print(f"Difficulty: {d['difficulty']} (score {d['difficulty_score']})") + print(f"Health validation: {d['health_validation']}") + print("Risks:") + for r in d["risks"]: + print(f" - {r}") + print(f"Next action: {_next_action(d)}") + + +def hint_heart_category(issue: dict) -> str: + """A first-pass category *hint* for a filed PyAutoHeart issue. + + From the issue title + labels only (never re-running a Heart check): one of + real-bug / config / flaky / expected. It is a hint the reasoning layer confirms + before routing — not an authoritative verdict. + """ + hay = (issue.get("title", "") + " " + + " ".join(l.get("name", "") for l in issue.get("labels", []))).lower() + if F._hits(hay, ["url-check", "broken", "forbidden url"]): + return "config (URL hygiene)" + if F._hits(hay, ["flaky", "intermittent", "timeout"]): + return "flaky/timeout — confirm before treating as a defect" + if F._hits(hay, ["fail", "regression", "error", "not positive", "wrong"]): + return "likely real-bug" + if F._hits(hay, ["degraded", "health", "readiness"]): + return "expected/rollup — triage its child findings, not the umbrella issue" + return "unknown — inspect the issue before routing" + + +def emit_health(mode: str, verdict: str, issues: list, mind: Path): + print("== BugDecision (health-issue mode) ==") + print(f"Mode: {mode}") + print(f"Live vitals verdict: {verdict.upper()} (consulted via the vitals faculty)") + hf = mind / "bug" / "health_fixes" + print(f"Health-fix prompts: {'present' if hf.is_dir() else 'absent'} at PyAutoMind/bug/health_fixes/") + if not issues: + print("Filed PyAutoHeart issues: none open (or gh unavailable).") + else: + print(f"Filed PyAutoHeart issues ({len(issues)} open) — category hint per finding " + "(confirm before routing):") + for it in issues: + print(f" - #{it.get('number')} {it.get('title','').strip()}") + print(f" hint: {hint_heart_category(it)}") + print("Next action: For each finding the hint marks a real defect, write a " + "PyAutoMind/bug/health_fixes/.md prompt and run start_dev; leave " + "flaky/expected findings for the Health conductor. Confirm with the vitals " + "faculty (never query Heart directly).") + + +def main(argv=None): + ap = argparse.ArgumentParser(prog="bug", add_help=True) + ap.add_argument("--mind", required=True) + ap.add_argument("--memory", default="") + ap.add_argument("--json", action="store_true", dest="as_json") + sub = ap.add_subparsers(dest="cmd", required=True) + + sp = sub.add_parser("specific") + sp.add_argument("task") + sp.add_argument("--heart-check", default="") + + se = sub.add_parser("select") + se.add_argument("--difficulty", default="") + se.add_argument("--model", default="") + se.add_argument("--budget", action="store_true") + se.add_argument("--ambitious", action="store_true") + se.add_argument("--impact", action="store_true") + se.add_argument("--limit", type=int, default=5) + + he = sub.add_parser("health") + he.add_argument("--verdict", default="unknown") + he.add_argument("--heart-issues", default="") # path to `gh issue list --json` output + + a = ap.parse_args(argv) + mind = Path(a.mind) + + if a.cmd == "specific": + task = Path(a.task) + if not task.is_absolute(): + task = mind / a.task + if not task.is_file(): + print(f"bug agent: task not found: {task}", file=sys.stderr) + return 5 + d = analyse_bug(F.parse_prompt(task, mind), a.heart_check or None) + d["mode"] = "specific" + if a.as_json: + print(json.dumps({**d, "next_action": _next_action(d)}, indent=2)) + else: + emit_human("specific", d) + return 0 + + if a.cmd == "health": + issues = [] + if a.heart_issues and Path(a.heart_issues).is_file(): + try: + issues = json.loads(Path(a.heart_issues).read_text() or "[]") + except Exception: + issues = [] + if a.as_json: + enriched = [{"number": it.get("number"), "title": it.get("title"), + "url": it.get("url"), "category_hint": hint_heart_category(it)} + for it in issues] + print(json.dumps({"mode": "health-issue", "verdict": a.verdict, + "heart_issues": enriched}, indent=2)) + else: + emit_health("health-issue", a.verdict, issues, mind) + return 0 + + # select / difficulty-constrained + constraint = {"difficulty": a.difficulty, "model": a.model, "budget": a.budget, + "ambitious": a.ambitious, "impact": a.impact} + mode = "difficulty-constrained" if (a.difficulty or a.model or a.budget + or a.ambitious or a.impact) else "selection" + ranked, total = select_bug(mind, constraint, a.limit) + if not ranked: + print("bug agent: no bug prompts found in PyAutoMind/bug/.", file=sys.stderr) + return 4 + + if a.as_json: + top = F.parse_prompt(mind / ranked[0]["path"], mind) + d = analyse_bug(top) + d["mode"] = mode + d["shortlist"] = ranked + d["candidates_considered"] = total + print(json.dumps({**d, "next_action": _next_action(d)}, indent=2)) + return 0 + + print(f"== Bug task {mode} ({total} bug prompts considered) ==") + print("Shortlist (recommendation — severity-first, in-flight down-ranked):") + for i, r in enumerate(ranked): + flag = " [in-flight, down-ranked]" if r["in_flight"] else "" + print(f" {i+1}. {r['path']} [{r['severity']}, {r['type']}, " + f"{r['difficulty']}/score {r['score']}]{flag}") + print() + d = analyse_bug(F.parse_prompt(mind / ranked[0]["path"], mind)) + print("Recommended pick (ranked by severity then the active constraint):") + emit_human(mode, d) + print("\nWhy this bug: highest-ranked after severity weighting and down-ranking " + "in-flight work; confirm against PyAutoMind priorities and a vitals check.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/agents/conductors/bug/bug.sh b/agents/conductors/bug/bug.sh new file mode 100755 index 0000000..294dcd3 --- /dev/null +++ b/agents/conductors/bug/bug.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# agents/conductors/bug/bug.sh — the Bug Agent (a PyAutoBrain conductor). +# +# The Bug Agent is the *immune system* of PyAutoBrain. It recognises a pathogen (a +# bug, regression, failing test or PyAutoHeart finding), tells it from benign self, +# types the threat, recalls prior cases (PyAutoMemory), and mounts a *targeted* +# response — a repair plan the existing workflow executes. It does NOT edit source: +# +# report/finding -> Bug Agent -> start_dev +# -> start_library / ship_library +# -> start_workspace / ship_workspace +# +# Like the Feature Agent it consults the sibling *vitals* faculty (and only the +# vitals faculty talks to the Heart organ); it never queries Heart directly. Its +# fundamental principle is a precise response with NO AUTOIMMUNITY: user-facing +# workspace scripts are documentation, so it prefers a general library-source fix. +# +# Modes: +# specific classify a named PyAutoMind bug prompt and plan the fix. +# selection choose the best next bug (no task named) — severity-first. +# difficulty-constrained select under a constraint (--difficulty/--model/…). +# health read the live vitals verdict AND scan filed PyAutoHeart issues. +# +# Usage: +# bug.sh # specific mode +# bug.sh select [--difficulty small|medium|large|easy] +# [--model weak|strong] [--budget] [--ambitious] +# [--impact] [--limit N] +# bug.sh health # vitals verdict + Heart issue scan +# bug.sh [--json] ... # machine-readable BugDecision +# bug.sh [--check-health] ... # also annotate with the vitals verdict +# +# The analysis core lives in _bug.py (stdlib-only, never writes; reuses _feature.py). +# +# Exit codes: 0 produced a decision · 4 no prompts / could-not-resolve · 5 bad usage. + +set -uo pipefail + +HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" +source "$HERE/../../_common.sh" + +# The GitHub repo where PyAutoHeart files its health findings (health-issue mode). +HEART_ISSUES_REPO="${PYAUTO_HEART_REPO:-PyAutoLabs/PyAutoHeart}" + +check_health=0 +as_json=0 +forward=() +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) sed -n '2,45p' "${BASH_SOURCE[0]}" | sed -n 's/^# \{0,1\}//p'; exit 0 ;; + --check-health) check_health=1; shift ;; + --json) as_json=1; shift ;; + *) forward+=("$1"); shift ;; + esac +done + +mind="$(resolve_mind)" || exit 4 +memory="$(resolve_memory 2>/dev/null || true)" + +# Normalise the subcommand. Nothing given -> selection. A bare path (or any first +# token that is not a known subcommand or flag) -> specific mode on that task. +if [[ ${#forward[@]} -eq 0 ]]; then + forward=(select) +elif [[ "${forward[0]}" != "select" && "${forward[0]}" != "specific" \ + && "${forward[0]}" != "health" && "${forward[0]}" != --* ]]; then + forward=(specific "${forward[@]}") +fi + +json_flag=() +[[ "$as_json" -eq 1 ]] && json_flag=(--json) + +# --- health-issue mode: the two health inputs -------------------------------- +# (1) the live vitals verdict (via the vitals faculty, never Heart directly), and +# (2) the durable findings Heart *filed* as GitHub issues. Both feed _bug.py, which +# classifies each and routes real defects to PyAutoMind/bug/health_fixes/. +if [[ "${forward[0]}" == "health" ]]; then + # Diagnostics go to stderr so stdout stays pure (valid JSON under --json). + echo "== bug agent: health-issue mode ==" >&2 + verdict="$(consult_vitals_verdict)" + echo " live vitals verdict: $verdict" >&2 + issues_json="$(mktemp)" + trap 'rm -f "$issues_json"' EXIT + if command -v gh >/dev/null 2>&1; then + echo " scanning filed PyAutoHeart issues: https://github.com/$HEART_ISSUES_REPO/issues" >&2 + gh issue list --repo "$HEART_ISSUES_REPO" --state open --limit 50 \ + --json number,title,labels,url > "$issues_json" 2>/dev/null \ + || echo "[]" > "$issues_json" + else + echo " (gh not available — skipping the filed-issue scan)" >&2 + echo "[]" > "$issues_json" + fi + # Run (not exec) so the EXIT trap fires and the temp file is cleaned up. + python3 "$HERE/_bug.py" --mind "$mind" --memory "$memory" \ + "${json_flag[@]}" health --verdict "$verdict" --heart-issues "$issues_json" + exit $? +fi + +# Optionally annotate a specific/selection decision with the vitals verdict +# (society-of-agents). This does not gate the decision — it informs it. Diagnostics +# go to stderr so a --json run still emits valid JSON on stdout. +if [[ "$check_health" -eq 1 ]]; then + echo "== bug agent: consulting vitals faculty for tree readiness ==" >&2 + echo " readiness verdict: $(consult_vitals_verdict)" >&2 +fi + +exec python3 "$HERE/_bug.py" --mind "$mind" --memory "$memory" \ + "${json_flag[@]}" "${forward[@]}" diff --git a/bin/pyauto-brain b/bin/pyauto-brain index e6498a4..c0c2ecb 100755 --- a/bin/pyauto-brain +++ b/bin/pyauto-brain @@ -36,6 +36,7 @@ FACULTIES_DIR="$AGENTS_DIR/faculties" declare -A AGENT_SCRIPT=( [feature]="$CONDUCTORS_DIR/feature/feature.sh" + [bug]="$CONDUCTORS_DIR/bug/bug.sh" [build]="$CONDUCTORS_DIR/build/build.sh" [release]="$CONDUCTORS_DIR/release/release.sh" [health]="$CONDUCTORS_DIR/health/health.sh" @@ -43,6 +44,7 @@ declare -A AGENT_SCRIPT=( ) declare -A AGENT_DESC=( [feature]="Reason over PyAutoMind feature tasks: select, size, phase, plan for start_dev" + [bug]="The immune system: classify a bug/regression/Heart finding, locate the fix, plan the repair" [build]="Coordinate execution: consult the vitals faculty, then delegate to PyAutoBuild" [release]="Reason about readiness + run release on green; 'release rehearse'/'release validate' drive release validation" [health]="The organism's clinician: run the health loop with a human, dispatch by dispatch, toward green" @@ -50,7 +52,7 @@ declare -A AGENT_DESC=( ) # Conductors are the front doors a human drives; faculties are consulted (and # runnable read-only). Both are dispatchable, but the menu groups them by tier. -CONDUCTOR_ORDER=(feature build release health) +CONDUCTOR_ORDER=(feature bug build release health) FACULTY_ORDER=(vitals) AGENT_ORDER=("${CONDUCTOR_ORDER[@]}" "${FACULTY_ORDER[@]}") diff --git a/skills/COMMANDS.md b/skills/COMMANDS.md index cf40f6a..85d9a37 100644 --- a/skills/COMMANDS.md +++ b/skills/COMMANDS.md @@ -30,6 +30,7 @@ readiness gate, or execution — those belong to the organs. | Command | Agent | Chain | |---------|-------|-------| | `/feature` | Feature Agent | `bin/pyauto-brain feature` → `start_dev` → `ship_*` | +| `/bug` | Bug Agent | `bin/pyauto-brain bug` → `start_dev` → `ship_*` (health mode → vitals + Heart issues) | | `/build` | Build Agent | `bin/pyauto-brain build` → vitals faculty → Heart → PyAutoBuild | | `/health` | Health Agent | `bin/pyauto-brain health` loop → vitals faculty → Heart → GREEN | @@ -39,7 +40,6 @@ the Brain (via `start_dev` → Feature Agent), so nothing is bypassed: | Command | PyAutoMind work-type | Promotion follow-up | |---------|----------------------|---------------------| -| `/bug` | `bug/` | dedicated Bug conductor | | `/refactor` | `refactor/` | dedicated Refactor conductor | | `/docs` | `docs/` | dedicated Documentation conductor | | `/research` | `research/` | dedicated Research conductor | diff --git a/skills/bug/bug.md b/skills/bug/bug.md index b97efee..e867462 100644 --- a/skills/bug/bug.md +++ b/skills/bug/bug.md @@ -1,14 +1,26 @@ -# /bug — fix a regression, failing test, or wrong behaviour +# /bug — fix a regression, failing test, or wrong behaviour (via the Brain Bug Agent) -A **work-type entry** into the Brain dev-flow. No dedicated Bug conductor exists -yet, so this routes through the Feature Agent's classifier with the PyAutoMind -work-type fixed to `bug/`. (Follow-up: promote to a dedicated Bug conductor.) +Route a bug through PyAutoBrain's **Bug Agent** — the organism's *immune system* — then +hand its decision to the dev workflow. You never name the Brain; this command is the door. Shared routing context: `PyAutoBrain/skills/COMMANDS.md`. ## Do -Treat the request as PyAutoMind work-type **`bug/`**. If no prompt path exists, -first create one under `PyAutoMind/bug//.md` (original request -verbatim), then run **`/start_dev`** on it. `start_dev` routes reasoning through -the Brain — nothing bypasses it. Taxonomy: `PyAutoMind/ROUTING.md`. +1. **Specific bug** — a `bug/…md` path, issue, failing test or error: run + `bin/pyauto-brain bug `. With no target, run `bin/pyauto-brain bug` + to select the next bug (severity-first). Difficulty/importance constraints: + `bin/pyauto-brain bug select --difficulty easy | --impact | --model strong`. +2. **From PyAutoHeart** — run `bin/pyauto-brain bug health`: it reads the live vitals + verdict **and** scans the filed PyAutoHeart issues, hinting a category per finding; + confirm the real defects and file them under `PyAutoMind/bug/health_fixes/`. +3. Take the emitted `BugDecision` (classification, **fix locus**, strategy, workflow) + and continue with **`/start_dev`** on the chosen bug — that carries the branch + survey, issue creation, and registration. + +The Bug Agent **reasons; it never edits source**, and its first question is always +*where the fix belongs*: prefer a general library-source fix, and **never degrade a +user-facing workspace script** to mask a symptom (no injected env-vars, hard-coded +paths, `os.environ` mutation, or silent guards). Reproduction and validation are +identified and delegated to the vitals faculty — the Bug Agent never runs checks itself. +Do not bypass the Brain.