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
2 changes: 1 addition & 1 deletion agents/conductors/bug/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ belongs in the affected repo, PyAutoHeart, PyAutoHands or PyAutoBrain:
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/<name>.md` prompts
Findings hinted as real defects become `PyAutoMind/draft/bug/health_fixes/<name>.md` prompts
(its README already cites Heart issue #27); flaky/expected findings are left to the
Health conductor.

Expand Down
6 changes: 3 additions & 3 deletions agents/conductors/bug/BUG_TAXONOMY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ belongs**, and which **health inputs** it reads. It mirrors the Feature 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/`.
`PyAutoBrain/AGENTS.md`, `agents/faculties/vitals/`, `PyAutoMind/draft/bug/health_fixes/`.

## Classification (typing the threat)

Expand Down Expand Up @@ -79,7 +79,7 @@ re-implement a Heart check. `bug.sh health` reads two complementary signals:
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, PyAutoHands, PyAutoBrain).
Confirmed real defects become `PyAutoMind/bug/health_fixes/<name>.md` prompts and enter
Confirmed real defects become `PyAutoMind/draft/bug/health_fixes/<name>.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.
Expand All @@ -105,7 +105,7 @@ 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)
intent → PyAutoMind (draft/bug/* prompts, draft/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)
Expand Down
13 changes: 6 additions & 7 deletions agents/conductors/bug/_bug.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,7 @@ def _next_action(d: dict) -> str:

# --- discovery + selection (bug/**) ------------------------------------------
def discover_bugs(mind: Path):
bug = mind / "bug"
return sorted(bug.rglob("*.md")) if bug.is_dir() else []
return F.discover_prompts(mind, "bug")


def _referenced_bug_paths(mind: Path):
Expand All @@ -325,7 +324,7 @@ def _referenced_bug_paths(mind: Path):


def select_bug(mind: Path, constraint: dict, limit: int):
prompts = [p for p in discover_bugs(mind) if p.name.lower() != "readme.md"]
prompts = discover_bugs(mind)
in_flight = _referenced_bug_paths(mind)
rows = []
for path in prompts:
Expand Down Expand Up @@ -418,8 +417,8 @@ 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/")
hf = mind / "draft" / "bug" / "health_fixes"
print(f"Health-fix prompts: {'present' if hf.is_dir() else 'absent'} at PyAutoMind/draft/bug/health_fixes/")
if not issues:
print("Filed PyAutoHeart issues: none open (or gh unavailable).")
else:
Expand All @@ -429,7 +428,7 @@ def emit_health(mode: str, verdict: str, issues: list, mind: Path):
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/<name>.md prompt and run start_dev; leave "
"PyAutoMind/draft/bug/health_fixes/<name>.md prompt and run start_dev; leave "
"flaky/expected findings for the Health conductor. Confirm with the vitals "
"faculty (never query Heart directly).")

Expand Down Expand Up @@ -499,7 +498,7 @@ def main(argv=None):
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)
print("bug agent: no bug prompts found in PyAutoMind/draft/bug/.", file=sys.stderr)
return 4

if a.as_json:
Expand Down
2 changes: 1 addition & 1 deletion agents/conductors/bug/bug.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ json_flag=()
# --- 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/.
# classifies each and routes real defects to PyAutoMind/draft/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
Expand Down
2 changes: 1 addition & 1 deletion agents/conductors/feature/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ it reasons over and the PyAutoMemory routing it uses.
| Mode | Trigger | What it does |
|------|---------|--------------|
| **specific** | a task path is given | Read the named prompt, classify repos, consult memory, size it, decide phasing, and produce a `start_dev`-ready plan. |
| **selection** | no task given | Scan `feature/**`, rank candidates, and recommend the best next task — **not** merely the first in a list; down-ranks in-flight work (from `active.md` / `planned.md`). |
| **selection** | no task given | Scan `draft/feature/**` (legacy flat `feature/**` still resolves), rank candidates, and recommend the best next task — **not** merely the first in a list; down-ranks in-flight work (from `active.md` / `planned.md`). |
| **difficulty-constrained** | `--difficulty` / `--model` / `--budget` / `--ambitious` / `--impact` | Estimate difficulty per task and select to match the constraint (easy/weak-model/limited-token → small; ambitious/strong-model → large; impact → high-leverage). |

## Difficulty & sizing
Expand Down
8 changes: 3 additions & 5 deletions agents/conductors/feature/_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
WORK_TYPES, LIBRARY_REPOS, WORKSPACE_REPOS, ORGANISM_REPOS, REPO_ALIASES,
KNOWN_REPOS, MEMORY_WIKIS, SCIENCE_KEYWORDS, RISK_KEYWORDS, AMBIGUITY_KEYWORDS,
policy as _sizing_policy,
TEST_KEYWORDS, normalise_repo, parse_prompt, estimate_difficulty, _hits, _within,
TEST_KEYWORDS, normalise_repo, parse_prompt, discover_prompts, estimate_difficulty,
_hits, _within,
)

# Default sub-wiki to consult per library target when no keyword fires. Memory
Expand Down Expand Up @@ -183,10 +184,7 @@ def analyse(p: dict):

# --- task discovery + selection ----------------------------------------------
def discover(mind: Path):
feat = mind / "feature"
if not feat.is_dir():
return []
return sorted(feat.rglob("*.md"))
return discover_prompts(mind, "feature")


def _referenced_paths(mind: Path, *names):
Expand Down
2 changes: 1 addition & 1 deletion agents/conductors/refactor/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
> behaviour-preserving by definition, so tests + the review faculty form a
> near-complete gate.

It reasons over `PyAutoMind/refactor/*` intent and produces a structured
It reasons over `PyAutoMind/draft/refactor/*` intent and produces a structured
**RefactorDecision** the standard workflow consumes:

```
Expand Down
5 changes: 2 additions & 3 deletions agents/conductors/refactor/_refactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "feature"))
from _feature import analyse # noqa: E402 (pulls _sizing onto the path too)
from _sizing import parse_prompt, policy as _sizing_policy # noqa: E402
from _sizing import discover_prompts, parse_prompt, policy as _sizing_policy # noqa: E402

# Public-API-change smells: a "refactor" prompt matching these is suspect —
# it belongs in feature/ (or bug/) and must not run at `safe`.
Expand Down Expand Up @@ -105,8 +105,7 @@ def decide(prompt_path: Path, mind: Path) -> dict:


def discover(mind: Path) -> list[Path]:
root = mind / "refactor"
return sorted(root.rglob("*.md")) if root.is_dir() else []
return discover_prompts(mind, "refactor")


def candidates(mind: Path) -> dict:
Expand Down
35 changes: 35 additions & 0 deletions agents/faculties/sizing/_sizing.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,41 @@ def _hits(text: str, keywords) -> list:
return out


def discover_prompts(mind: Path, work_type: str) -> list[Path]:
"""Every backlog prompt of one work-type, across the Mind lifecycle layout.

The discovery counterpart to `parse_prompt`, and shared for the same reason:
three conductors (feature / bug / refactor) each held a private copy rooted
at the pre-#71 `mind/<work-type>/`, so from the day the split closed
(2026-07-13) all three selection modes silently returned "no prompts found"
against a live backlog.

Covers the two regimes that hold *backlog* prompts, mirroring `parse_prompt`:

- `draft/<work-type>/<target>/*.md` — the current layout (PyAutoMind#71);
- `<work-type>/<target>/*.md` — legacy flat, pre-migration.

`active/` is deliberately NOT discovered: it is flat (so its paths carry no
work-type to filter on) and holds issued, in-flight work, whereas selection
answers "what should I start next". `complete/` — records, not backlog — is
excluded by construction, since neither root above can reach into it.
"""
seen, out = set(), []
for root in (mind / "draft" / work_type, mind / work_type):
if not root.is_dir():
continue
for p in root.rglob("*.md"):
# READMEs document a folder; they are never themselves tasks.
if p.name.lower() == "readme.md":
continue
key = p.resolve()
if key in seen:
continue
seen.add(key)
out.append(p)
return sorted(out)


def parse_prompt(path: Path, mind: Path):
"""Read a prompt file and extract structure: work-type, target, repos, body."""
text = path.read_text(encoding="utf-8", errors="replace")
Expand Down
2 changes: 1 addition & 1 deletion skills/bug/bug.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Shared routing context: `PyAutoBrain/skills/COMMANDS.md`.
`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/`.
confirm the real defects and file them under `PyAutoMind/draft/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.
Expand Down
131 changes: 131 additions & 0 deletions tests/test_conductor_discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""tests/test_conductor_discovery.py — conductor discovery vs the Mind lifecycle layout.

The companion to `test_sizing_paths.py`. That file locks how a prompt path is
*read*; this one locks how prompts are *found* — the half the 2026-07-16 sweep
missed, which left `feature` / `bug` / `refactor` selection silently returning
"no prompts found" against a live backlog for as long as the split had been in
place (all three rooted discovery at the pre-#71 `mind/<work-type>/`).

The end-to-end cases at the bottom are the real regression guard: they assert
each conductor's selection surface is non-empty on a draft/-layout Mind, which
is the exact assertion that would have caught the original bug.
"""

from __future__ import annotations

import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "agents" / "faculties" / "sizing"))
sys.path.insert(0, str(ROOT / "agents" / "conductors" / "feature"))
sys.path.insert(0, str(ROOT / "agents" / "conductors" / "bug"))
sys.path.insert(0, str(ROOT / "agents" / "conductors" / "refactor"))

from _sizing import discover_prompts # noqa: E402
import _feature # noqa: E402
import _bug # noqa: E402
import _refactor # noqa: E402


def _write(mind: Path, rel: str, work_type: str = "bug", target: str = "PyAutoHands") -> Path:
p = mind / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(
f"# A task\n\n"
f"Type: {work_type}\n"
f"Target: {target}\n"
f"Repos:\n- {target}\n"
f"Difficulty: small\nAutonomy: safe\nPriority: normal\nStatus: formalised\n\n"
f"Body mentioning @{target}.\n"
)
return p


def _rel(mind: Path, paths) -> set[str]:
return {str(p.relative_to(mind)) for p in paths}


# --- discover_prompts: the layout regimes -------------------------------------
def test_draft_layout_is_discovered(tmp_path):
_write(tmp_path, "draft/bug/pyautohands/one.md")
_write(tmp_path, "draft/bug/pyautofit/two.md")
assert _rel(tmp_path, discover_prompts(tmp_path, "bug")) == {
"draft/bug/pyautohands/one.md",
"draft/bug/pyautofit/two.md",
}


def test_legacy_flat_layout_still_resolves(tmp_path):
"""Pre-migration paths keep working, mirroring parse_prompt's third regime."""
_write(tmp_path, "bug/pyautohands/legacy.md")
assert _rel(tmp_path, discover_prompts(tmp_path, "bug")) == {"bug/pyautohands/legacy.md"}


def test_both_layouts_coexist_without_duplicates(tmp_path):
_write(tmp_path, "draft/bug/pyautohands/one.md")
_write(tmp_path, "bug/pyautohands/legacy.md")
found = discover_prompts(tmp_path, "bug")
assert _rel(tmp_path, found) == {"draft/bug/pyautohands/one.md", "bug/pyautohands/legacy.md"}
assert len(found) == len(set(found))


def test_work_types_do_not_bleed_into_each_other(tmp_path):
_write(tmp_path, "draft/bug/pyautohands/a_bug.md", work_type="bug")
_write(tmp_path, "draft/feature/pyautohands/a_feature.md", work_type="feature")
assert _rel(tmp_path, discover_prompts(tmp_path, "bug")) == {"draft/bug/pyautohands/a_bug.md"}
assert _rel(tmp_path, discover_prompts(tmp_path, "feature")) == {
"draft/feature/pyautohands/a_feature.md"
}


# --- discover_prompts: what must stay out -------------------------------------
def test_readme_is_not_a_task(tmp_path):
_write(tmp_path, "draft/bug/health_fixes/one.md")
(tmp_path / "draft/bug/health_fixes/README.md").write_text("# documents the folder\n")
assert _rel(tmp_path, discover_prompts(tmp_path, "bug")) == {"draft/bug/health_fixes/one.md"}


def test_complete_records_are_not_backlog(tmp_path):
"""complete/ holds shipped records; selection must never resurface them."""
_write(tmp_path, "draft/bug/pyautohands/live.md")
_write(tmp_path, "complete/2026/07/bug/shipped.md")
_write(tmp_path, "complete/archive/shelved/bug/old.md")
assert _rel(tmp_path, discover_prompts(tmp_path, "bug")) == {"draft/bug/pyautohands/live.md"}


def test_active_is_not_discovered(tmp_path):
"""active/ is issued, in-flight work; selection answers 'what next'."""
_write(tmp_path, "draft/bug/pyautohands/live.md")
_write(tmp_path, "active/issued.md")
assert _rel(tmp_path, discover_prompts(tmp_path, "bug")) == {"draft/bug/pyautohands/live.md"}


def test_missing_roots_are_not_an_error(tmp_path):
assert discover_prompts(tmp_path, "refactor") == []


# --- end-to-end: the assertion that would have caught the original bug --------
def _populated(tmp_path) -> Path:
_write(tmp_path, "draft/feature/pyautohands/feat.md", work_type="feature")
_write(tmp_path, "draft/bug/pyautohands/bug.md", work_type="bug")
_write(tmp_path, "draft/refactor/pyautohands/ref.md", work_type="refactor")
return tmp_path


def test_feature_selection_is_non_empty_on_draft_layout(tmp_path):
ranked, total = _feature.select(_populated(tmp_path), {}, 5)
assert total == 1
assert [r["path"] for r in ranked] == ["draft/feature/pyautohands/feat.md"]


def test_bug_selection_is_non_empty_on_draft_layout(tmp_path):
ranked, total = _bug.select_bug(_populated(tmp_path), {}, 5)
assert total == 1
assert [r["path"] for r in ranked] == ["draft/bug/pyautohands/bug.md"]


def test_refactor_backlog_is_non_empty_on_draft_layout(tmp_path):
assert _refactor.candidates(_populated(tmp_path))["backlog"] == [
"draft/refactor/pyautohands/ref.md"
]
Loading