2020
2121import argparse
2222import json
23- import os
2423import re
2524import sys
2625from 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).
8342TARGET_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
21149def 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
25094def 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