Skip to content

Commit 9bd7f4f

Browse files
Jammy2211claude
authored andcommitted
feat(intake): add the Intake (Conception) Agent
Turn raw input (a text-vomit idea, a bug report, an ideas.md bullet) into a formal, grouped, headed PyAutoMind prompt under <work-type>/<target>/<name>.md. New conductor at agents/conductors/intake/; it FILES a prompt, never starts dev (the step before create_issue/start_dev). Organism-facing name: Conception Agent (Conception -> Growth, pairing with the Feature Agent's Growth name). - Extract the difficulty heuristic + prompt parsing into a shared read-only sizing faculty (agents/faculties/sizing/) that both Intake and the Feature Agent consult -- so a difficulty Intake persists is the same number the Feature Agent later trusts (no drift). Feature Agent output on the library path is unchanged (regression-checked). - Resolve the organism repos (pyautomind/pyautobrain/pyautoheart/pyautobuild/ pyautomemory) as first-class targets -- fixes the Feature Agent mis-routing a pyautobrain target as "(none resolved) -> research-first". - Extend the blessed light prompt header with Difficulty/Autonomy/Priority (no YAML); register the /intake command + docs. Modes: classify (text / --file) and ideas (sweep ideas.md); writes only under --apply (dry-run default). census/dashboard are a planned follow-up PR. Issue: #21 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c98f6a6 commit 9bd7f4f

11 files changed

Lines changed: 1017 additions & 183 deletions

File tree

AGENTS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ humans invoke identically, so behaviour isn't re-derived from prose each time.
112112

113113
### Conductors
114114

115+
- **`agents/conductors/intake/`** — the **conceptive function**: turns raw input
116+
(a text-vomit idea, a bug report, an `ideas.md` bullet) into a *formal, grouped,
117+
headed* PyAutoMind prompt under `<work-type>/<target>/<name>.md`. Classifies the
118+
work-type, resolves the target (incl. the organism repos), consults the sizing
119+
faculty for difficulty and **persists** it (plus autonomy/priority) into the
120+
prompt header, and emits an `IntakeDecision`. It *files* a prompt; it never
121+
starts dev — the step *before* `create_issue`/`start_dev`. Lifecycle:
122+
**Conception → Growth**. It reasons + writes a Mind prompt (under `--apply`); it
123+
never edits source. (Organism-facing name: *Conception Agent*.)
115124
- **`agents/conductors/feature/`** — the **growth function**: reasons over
116125
PyAutoMind `feature/*` intent and decides *how the organism should grow*.
117126
Selects the next feature task (or plans a named one), estimates difficulty,
@@ -180,6 +189,8 @@ thing only reasons.
180189

181190
```bash
182191
bin/pyauto-brain help # list agents
192+
bin/pyauto-brain intake "add data cube modelling to autolens" # conceive: raw text -> a formal Mind prompt (dry-run)
193+
bin/pyauto-brain intake --apply ideas # sweep ideas.md into formal prompts
183194
bin/pyauto-brain feature # select the best next PyAutoMind feature task
184195
bin/pyauto-brain feature feature/autofit/sbi.md # plan a specific feature task
185196
bin/pyauto-brain build # consult vitals, then delegate execution to Build
@@ -204,6 +215,7 @@ to the right agent; normal usage never says "PyAutoBrain".
204215
205216
| Command | Routes to | Tier |
206217
|---------|-----------|------|
218+
| `/intake` | Intake Agent → files a PyAutoMind prompt (before `start_dev`) | real conductor |
207219
| `/feature` | Feature Agent → `start_dev` | real conductor |
208220
| `/bug` | Bug Agent → `start_dev` (health mode → vitals + Heart issues) | real conductor |
209221
| `/build` | Build Agent → vitals → Heart → PyAutoBuild | real conductor |

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ faculties multiply behind them.
4848

4949
**Conductors:**
5050

51+
- **`agents/conductors/intake/`** — the conceptive function: turns raw input (an
52+
idea, a bug report, an `ideas.md` bullet) into a formal, headed PyAutoMind
53+
prompt. Files it; never starts dev — the step before `start_dev`.
5154
- **`agents/conductors/feature/`** — the growth function: reasons over PyAutoMind
5255
`feature/*` intent and plans how the organism grows.
5356
- **`agents/conductors/bug/`** — the immune system: recognises a bug / regression /
@@ -111,7 +114,8 @@ natural language) and it routes to the right agent — normal usage never says
111114

112115
> **Users speak in short commands; PyAutoBrain performs the routing.**
113116
114-
- **Real conductors:** `/feature` → Feature Agent, `/bug` → Bug Agent, `/build`
117+
- **Real conductors:** `/intake` → Intake Agent (files a Mind prompt, before
118+
`start_dev`), `/feature` → Feature Agent, `/bug` → Bug Agent, `/build`
115119
Build Agent, `/health` → Health Agent (each → vitals/Heart/Build as needed).
116120
- **Work-type entries:** `/refactor` `/docs` `/research` route through the Brain
117121
dev-flow (`start_dev`) with their PyAutoMind work-type fixed — honest interim

agents/conductors/feature/_feature.py

Lines changed: 24 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -20,193 +20,31 @@
2020

2121
import argparse
2222
import json
23-
import os
2423
import re
2524
import sys
2625
from pathlib import Path
2726

28-
# --- the PyAutoMind taxonomy (mirrors PyAutoMind/ROUTING.md) -----------------
29-
# work-type folder -> the kind of work and the recommended re-home when a
30-
# feature prompt is mis-filed. The Feature Agent helps keep PyAutoMind organised.
31-
WORK_TYPES = {
32-
"feature": "new user-facing or scientific capability",
33-
"bug": "incorrect behaviour, crash or regression",
34-
"refactor": "internal restructuring, no behaviour change",
35-
"docs": "documentation, tutorials, notebooks, examples",
36-
"test": "test coverage, smoke tests, validation",
37-
"release": "packaging, versions, deployment, readiness",
38-
"maintenance": "dependency updates, hygiene, small tech debt",
39-
"research": "exploratory scientific/algorithmic investigation",
40-
"experiment": "prototype, spike, proof-of-concept",
41-
"triage": "classification still unclear",
42-
}
43-
44-
# Targets that are source *libraries* (work classifies as library vs workspace).
45-
LIBRARY_REPOS = {
46-
"pyautoconf", "pyautofit", "pyautoarray", "pyautogalaxy", "pyautolens",
47-
"autoconf", "autofit", "autoarray", "autogalaxy", "autolens",
48-
}
49-
# Targets / @-mentions that are workspaces, tutorials or example repos.
50-
WORKSPACE_REPOS = {
51-
"autolens_workspace", "autogalaxy_workspace", "autofit_workspace",
52-
"autolens_workspace_test", "autogalaxy_workspace_test", "autofit_workspace_test",
53-
"howtolens", "howtogalaxy", "howtofit", "autolens_assistant",
54-
"autolens_profiling", "workspaces",
55-
}
56-
# Normalise an @-mention or folder name to a canonical key.
57-
REPO_ALIASES = {
58-
"aa": "autoarray", "af": "autofit", "ag": "autogalaxy", "al": "autolens",
59-
"pyautoarray": "autoarray", "pyautofit": "autofit", "pyautoconf": "autoconf",
60-
"pyautogalaxy": "autogalaxy", "pyautolens": "autolens",
61-
}
62-
63-
# --- PyAutoMemory sub-wiki routing -------------------------------------------
64-
# Map target/keywords -> the PyAutoMemory sub-wiki that holds relevant context.
65-
# Source of truth for the sub-wiki list: PyAutoMemory/index.md.
66-
MEMORY_WIKIS = {
67-
"lensing_wiki": ["lens", "deflection", "source reconstruction", "caustic",
68-
"einstein", "subhalo", "substructure", "time delay",
69-
"cosmography", "shear", "multipole", "mass sheet", "slacs",
70-
"tdcosmo", "h0licow", "macromodel"],
71-
"smbh_wiki": ["black hole", "smbh", "binary", "recoil", "nanograv",
72-
"gravitational wave background"],
73-
"cti_wiki": ["charge transfer", "cti", "trap", "arctic", "vis calibration"],
74-
"methods_wiki": ["bayesian", "sampler", "nautilus", "dynesty", "emcee",
75-
"mcmc", "nested sampling", "likelihood", "jax", "nufft",
76-
"interpolat", "graphical model", "expectation propagation",
77-
"deep learning", "sbi", "simulation based inference",
78-
"probabilistic", "optimis", "gradient", "regularis"],
79-
"galaxies_wiki": ["galaxy formation", "bulge", "disk", "morphology", "mge",
80-
"stellar halo", "ifu", "kinematic", "elliptical", "cosmos"],
81-
}
82-
# Default sub-wiki to consult per library target when no keyword fires.
27+
# The sizing substrate (prompt parsing, the PyAutoMind taxonomy/vocabulary, and
28+
# the difficulty heuristic) is a shared read-only faculty consulted by BOTH the
29+
# Feature Agent and the Intake Agent — one definition, so a difficulty Intake
30+
# persists is the same number this agent reasons over. See
31+
# agents/faculties/sizing/_sizing.py.
32+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "faculties" / "sizing"))
33+
from _sizing import ( # noqa: E402
34+
WORK_TYPES, LIBRARY_REPOS, WORKSPACE_REPOS, ORGANISM_REPOS, REPO_ALIASES,
35+
KNOWN_REPOS, MEMORY_WIKIS, SCIENCE_KEYWORDS, RISK_KEYWORDS, AMBIGUITY_KEYWORDS,
36+
TEST_KEYWORDS, normalise_repo, parse_prompt, estimate_difficulty, _hits, _within,
37+
)
38+
39+
# Default sub-wiki to consult per library target when no keyword fires. Memory
40+
# routing is the Feature Agent's own concern, so it stays here (the shared
41+
# science *vocabulary* it keys off — MEMORY_WIKIS — lives in the sizing faculty).
8342
TARGET_DEFAULT_WIKI = {
8443
"autolens": "lensing_wiki", "autogalaxy": "galaxies_wiki",
8544
"autofit": "methods_wiki", "autoarray": "methods_wiki",
8645
"autoconf": "methods_wiki",
8746
}
8847

89-
SCIENCE_KEYWORDS = sorted({kw for kws in MEMORY_WIKIS.values() for kw in kws})
90-
RISK_KEYWORDS = ["api", "breaking", "backwards", "migrat", "deprecat",
91-
"cross-repo", "interface", "refactor", "rename", "public api"]
92-
AMBIGUITY_KEYWORDS = ["unclear", "investigate", "explore", "research", "decide",
93-
"figure out", "not sure", "tbd", "open question", "design",
94-
"proof of concept", "prototype", "spike", "?"]
95-
TEST_KEYWORDS = ["test", "smoke", "parity", "jax", "likelihood", "vmap",
96-
"validation", "regression"]
97-
98-
99-
def normalise_repo(name: str) -> str:
100-
# Take the head token before any '.' or '/': an @-mention may be an API path
101-
# (e.g. @aa.decorators.to_vector_yx -> aa) or a repo path, not just a name.
102-
key = re.split(r"[./]", name.strip().lstrip("@").lower(), 1)[0]
103-
return REPO_ALIASES.get(key, key)
104-
105-
106-
KNOWN_REPOS = LIBRARY_REPOS | WORKSPACE_REPOS
107-
108-
109-
def parse_prompt(path: Path, mind: Path):
110-
"""Read a prompt file and extract structure: work-type, target, repos, body."""
111-
text = path.read_text(encoding="utf-8", errors="replace")
112-
try:
113-
rel = path.relative_to(mind)
114-
parts = rel.parts
115-
except ValueError:
116-
parts = path.parts
117-
work_type = parts[0] if parts else "?"
118-
target = parts[1] if len(parts) > 1 else "?"
119-
120-
mentions = {normalise_repo(m) for m in re.findall(r"@[A-Za-z0-9._/-]+", text)}
121-
# Keep only mentions that resolve to a repo we know — drops project refs
122-
# (@z_projects), bare libraries (@jax) and noise, so the repo count is real.
123-
repos = {m for m in mentions if m in KNOWN_REPOS}
124-
if target not in ("?", "workspaces") and target not in WORK_TYPES:
125-
t = normalise_repo(target)
126-
if t in KNOWN_REPOS:
127-
repos.add(t)
128-
129-
return {
130-
"path": str(path.relative_to(mind)) if _within(path, mind) else str(path),
131-
"work_type": work_type,
132-
"target": target,
133-
"repos": sorted(repos),
134-
"text": text,
135-
"lines": text.count("\n") + 1,
136-
"words": len(text.split()),
137-
}
138-
139-
140-
def _within(path: Path, base: Path) -> bool:
141-
try:
142-
path.relative_to(base)
143-
return True
144-
except ValueError:
145-
return False
146-
147-
148-
def _hits(text: str, keywords) -> list:
149-
"""Keyword hits using word-boundary *prefix* matching.
150-
151-
A leading \\b stops short tokens ("cti", "api", "mge") matching inside other
152-
words ("function", "rapid"), while leaving the end open so stems still fire
153-
("interpolat" -> "interpolation", "migrat" -> "migration").
154-
"""
155-
low = text.lower()
156-
out = []
157-
for k in keywords:
158-
if re.search(r"\b" + re.escape(k), low):
159-
out.append(k)
160-
return out
161-
162-
163-
def estimate_difficulty(p: dict):
164-
"""Heuristic difficulty estimate -> (level, score, factors).
165-
166-
Considers: repos affected, prompt size, scientific complexity, architectural
167-
risk, test burden, and whether human judgement / memory context is needed.
168-
"""
169-
text = p["text"]
170-
lib = [r for r in p["repos"] if r in LIBRARY_REPOS]
171-
wsp = [r for r in p["repos"] if r in WORKSPACE_REPOS]
172-
repo_count = len(set(p["repos"]))
173-
science = _hits(text, SCIENCE_KEYWORDS)
174-
risk = _hits(text, RISK_KEYWORDS)
175-
tests = _hits(text, TEST_KEYWORDS)
176-
ambiguity = _hits(text, AMBIGUITY_KEYWORDS)
177-
178-
score = 0
179-
score += max(0, repo_count - 1) * 2 # multi-repo is the big driver
180-
score += 2 if (lib and wsp) else 0 # library+workspace coordination
181-
score += min(p["words"] // 150, 4) # size of the description
182-
score += min(len(science), 3) # scientific complexity
183-
score += min(len(risk) * 2, 4) # architectural risk
184-
score += 1 if tests else 0 # test burden
185-
score += 1 if science else 0 # memory context likely needed
186-
187-
if score <= 2:
188-
level = "small"
189-
elif score <= 5:
190-
level = "medium"
191-
elif score <= 9:
192-
level = "large"
193-
else:
194-
level = "too-large"
195-
196-
factors = {
197-
"repos_affected": repo_count,
198-
"library_repos": lib,
199-
"workspace_repos": wsp,
200-
"library_and_workspace": bool(lib and wsp),
201-
"size_words": p["words"],
202-
"scientific_complexity": science,
203-
"architectural_risk": risk,
204-
"test_burden": tests,
205-
"human_judgement": ambiguity,
206-
"memory_context_required": bool(science),
207-
}
208-
return level, score, factors
209-
21048

21149
def recommend_workflow(p: dict, factors: dict):
21250
"""Map the task to a development path / re-home suggestion."""
@@ -217,17 +55,23 @@ def recommend_workflow(p: dict, factors: dict):
21755

21856
lib = factors["library_repos"]
21957
wsp = factors["workspace_repos"]
58+
org = factors.get("organism_repos") or []
22059

22160
# Re-home suggestions for mis-filed feature prompts.
22261
rehome = None
223-
if factors["human_judgement"] and not (lib or wsp):
62+
if factors["human_judgement"] and not (lib or wsp or org):
22463
rehome = "research"
22564
if lib and wsp:
22665
return "combined", rehome
22766
if lib:
22867
return "library", rehome
22968
if wsp:
23069
return "workspace", rehome
70+
# Organism/infrastructure work (PyAutoBrain/Mind/Heart/Build/Memory) ships
71+
# like a library — worktree + PR via start_library. Resolving these stops the
72+
# old "(none resolved) -> research-first" mis-route of a `pyautobrain` target.
73+
if org:
74+
return "library", rehome
23175
# No repo resolved — most likely needs scoping first.
23276
return "research", (rehome or "research")
23377

@@ -249,7 +93,8 @@ def memory_context(p: dict):
24993

25094
def phase_decision(level: str, factors: dict, p: dict):
25195
"""direct | split-into-phases | research-first | defer, plus phase stubs."""
252-
if factors["human_judgement"] and not (factors["library_repos"] or factors["workspace_repos"]):
96+
if factors["human_judgement"] and not (factors["library_repos"]
97+
or factors["workspace_repos"] or factors.get("organism_repos")):
25398
return "research-first", []
25499
if level == "too-large":
255100
# Phase stubs live in the prompt's own target folder (mirrors the

0 commit comments

Comments
 (0)