From c4a373bbd5a3c3b310f05a2022f2233f7e8cdbd1 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 11:11:27 +0100 Subject: [PATCH] Add repos.yaml body map and repos_sync.py generator/drift-check repos.yaml is the single source of repo IDENTITY (GitHub home, category, one-line role) for every repo in the workspace. Per-organ POLICY stays where it belongs (PyAutoHeart/config/repos.yaml, PyAutoBuild/pre_build.sh). scripts/repos_sync.py --write regenerates the routing table in the workspace-root AGENTS.md and the owner map in PyAutoBrain/skills/WORKFLOW.md between repos_sync markers; --check validates Heart config, pre_build.sh, ensure_workspace_labels.sh and every local checkout's origin remote against the manifest. Replaces four hand-maintained repo lists that had drifted (WORKFLOW.md and the labels script still pointed at pre-org-migration rhayes777/Jammy2211 owners). Co-Authored-By: Claude Fable 5 --- AGENTS.md | 6 ++ ROUTING.md | 3 + repos.yaml | 150 ++++++++++++++++++++++++++ scripts/repos_sync.py | 246 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 405 insertions(+) create mode 100644 repos.yaml create mode 100644 scripts/repos_sync.py diff --git a/AGENTS.md b/AGENTS.md index 4d8f25e..ebe6f9f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,12 @@ For the full workflow narrative, conventions, and registry schemas, read `parked.md` holds tasks that were started or scoped but are not currently in flight (e.g. work parked in a stash, orphan worktrees); move back to `active.md` (or `planned.md` if re-scoping) when resuming. +- **Body map** — `repos.yaml` is the single source of repo *identity* (GitHub + home, category, one-line role) for every repo in the workspace. The routing + table in the workspace-root `AGENTS.md` and the owner map in + `PyAutoBrain/skills/WORKFLOW.md` are generated from it, and the repo lists in + Heart/Build/admin scripts are drift-checked against it: + `python3 scripts/repos_sync.py --write`. - **Skills** — `skills//` are Claude Code skills/commands tightly coupled to the registry. They source `scripts/prompt_sync.sh` for commit/push. - **Scripts** — `scripts/status.sh` (inventory), `scripts/prompt_sync.sh` diff --git a/ROUTING.md b/ROUTING.md index 1cff9aa..dadc616 100644 --- a/ROUTING.md +++ b/ROUTING.md @@ -29,6 +29,9 @@ domain is affected. ## Targets (second folder) +The canonical list of repos (with GitHub home, category and role) is +`repos.yaml` in this repo — the organism's body map. + The second folder names the affected repo or domain, e.g. `autoarray`, `autofit`, `autogalaxy`, `autolens`, `autolens_assistant`, `autolens_profiling`, `autolens_workspace_developer`, `autobuild`, `pyautobrain`; the workspace bucket diff --git a/repos.yaml b/repos.yaml new file mode 100644 index 0000000..4e430f3 --- /dev/null +++ b/repos.yaml @@ -0,0 +1,150 @@ +# repos.yaml — the organism's body map: the single source of repo IDENTITY. +# +# Identity only: GitHub home, category, one-line role. Per-organ POLICY stays +# with the organ that owns it — what Heart polls/gates lives in +# PyAutoHeart/config/repos.yaml; what pre_build formats/generates lives in +# PyAutoBuild/pre_build.sh. Those files must agree with this one on identity; +# `scripts/repos_sync.py --check` enforces it (including against the actual +# `git remote` of every local checkout). +# +# After editing this file: +# +# python3 PyAutoMind/scripts/repos_sync.py --write +# +# regenerates the routing table in /AGENTS.md and the owner +# map in PyAutoBrain/skills/WORKFLOW.md, then re-runs the drift checks. +# +# Categories with `collapse: true` render as ONE row (label + role) in the +# routing table, to keep it token-lean; other categories render one row per +# repo using the repo's own `role:`. + +categories: + organ: {} + library: {} + workspace: + collapse: true + label: "autofit/autogalaxy/autolens_workspace" + role: "User-facing example scripts, tutorials, configs for each library." + workspace_test: + collapse: true + label: "\\*_workspace_test" + role: "Regression, smoke and parity scripts (code-heavy, doc-light)." + workspace_developer: + collapse: true + label: "\\*_workspace_developer" + role: "Developer / profiling / experiment scripts (code-heavy, doc-light)." + howto: + collapse: true + label: "HowToFit / HowToGalaxy / HowToLens" + role: "Narrative teaching tutorials." + assistant: + collapse: true + label: "autolens_assistant / euclid_assistant" + role: "Science-assistant workspaces (see lensing pointer below)." + pipeline: {} + project: {} + admin: {} + +repos: + # --- organs ----------------------------------------------------------- + PyAutoMind: + github: PyAutoLabs/PyAutoMind + category: organ + role: "Intent, goals, priorities, workflow state; every task starts as a markdown prompt here." + PyAutoBrain: + github: PyAutoLabs/PyAutoBrain + category: organ + role: "Reasoning/orchestration layer; how work is decomposed and routed; the specialist agents." + PyAutoBuild: + github: PyAutoLabs/PyAutoBuild + category: organ + role: "Packaging, tagging, notebook generation, PyPI release execution." + PyAutoHeart: + github: PyAutoLabs/PyAutoHeart + category: organ + role: "Health/readiness — the authoritative \"is it safe to release?\" verdict." + PyAutoMemory: + github: PyAutoLabs/PyAutoMemory + category: organ + role: "Long-term scientific/software/project knowledge (see science pointer below)." + + # --- scientific libraries ---------------------------------------------- + PyAutoConf: + github: PyAutoLabs/PyAutoConf + category: library + role: "Shared config + shared utilities used across the stack." + PyAutoFit: + github: PyAutoLabs/PyAutoFit + category: library + role: "Model-fitting / non-linear search / aggregator library." + PyAutoArray: + github: PyAutoLabs/PyAutoArray + category: library + role: "Data structures (arrays, grids, masks), inversions, interferometry." + PyAutoGalaxy: + github: PyAutoLabs/PyAutoGalaxy + category: library + role: "Galaxy light/mass profiles and galaxy modelling." + PyAutoLens: + github: PyAutoLabs/PyAutoLens + category: library + role: "Strong-lensing modelling built on the libraries above." + + # --- workspaces --------------------------------------------------------- + autofit_workspace: + github: PyAutoLabs/autofit_workspace + category: workspace + autogalaxy_workspace: + github: PyAutoLabs/autogalaxy_workspace + category: workspace + autolens_workspace: + github: PyAutoLabs/autolens_workspace + category: workspace + autofit_workspace_test: + github: PyAutoLabs/autofit_workspace_test + category: workspace_test + autogalaxy_workspace_test: + github: PyAutoLabs/autogalaxy_workspace_test + category: workspace_test + autolens_workspace_test: + github: PyAutoLabs/autolens_workspace_test + category: workspace_test + autofit_workspace_developer: + github: Jammy2211/autofit_workspace_developer + category: workspace_developer + autolens_workspace_developer: + github: PyAutoLabs/autolens_workspace_developer + category: workspace_developer + + # --- tutorials ---------------------------------------------------------- + HowToFit: + github: PyAutoLabs/HowToFit + category: howto + HowToGalaxy: + github: PyAutoLabs/HowToGalaxy + category: howto + HowToLens: + github: PyAutoLabs/HowToLens + category: howto + + # --- assistants / pipelines / projects ---------------------------------- + autolens_assistant: + github: PyAutoLabs/autolens_assistant + category: assistant + euclid_assistant: + github: Jammy2211/euclid_assistant + category: assistant + euclid_strong_lens_modeling_pipeline: + github: PyAutoLabs/euclid_strong_lens_modeling_pipeline + category: pipeline + role: "Euclid pipeline glue (library/pipeline changes go here, not z_projects/euclid)." + autolens_profiling: + github: PyAutoLabs/autolens_profiling + category: project + role: "JAX likelihood-profiling runs and results." + + # --- admin --------------------------------------------------------------- + admin_jammy: + github: Jammy2211/admin_jammy + category: admin + role: "Local tooling, worktree helpers, some skills." diff --git a/scripts/repos_sync.py b/scripts/repos_sync.py new file mode 100644 index 0000000..e40e5d5 --- /dev/null +++ b/scripts/repos_sync.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +"""Sync + drift-check the organism's body map (PyAutoMind/repos.yaml). + +repos.yaml is the single source of repo IDENTITY (GitHub home, category, +one-line role). This script keeps the generated doc blocks in step with it and +checks every other hand-maintained repo list against it. + +Usage: + python3 repos_sync.py [--check] # drift checks only (default) + python3 repos_sync.py --write # regenerate doc blocks, then check + python3 repos_sync.py --root # override the workspace root + +--write regenerates the blocks between `` / +`` markers in: + + * /AGENTS.md — the repo routing table + * /PyAutoBrain/skills/WORKFLOW.md — the GitHub owner map + +--check (always run) verifies, against the manifest: + + * PyAutoHeart/config/repos.yaml — polled repos exist, owners match + * PyAutoBuild/pre_build.sh — run_workspace repos exist + * admin_jammy/software/ensure_workspace_labels.sh — owner/name pairs match + * the `origin` remote of every local checkout — manifest matches reality + +Exit code 0 = no drift; 1 = drift found (each mismatch printed). +""" + +import argparse +import re +import subprocess +import sys +from pathlib import Path + +import yaml + +MARK_BEGIN = "" +MARK_END = "" + + +def load_manifest(mind_root): + data = yaml.safe_load((mind_root / "repos.yaml").read_text()) + return data["categories"], data["repos"] + + +def owner_of(repo_spec): + return repo_spec["github"].split("/")[0] + + +# -------------------------------------------------------------------------- +# Generated blocks +# -------------------------------------------------------------------------- + +def routing_table(categories, repos): + lines = [ + "| Repo | Role — go here when the task is about… |", + "|------|----------------------------------------|", + ] + for cat, spec in categories.items(): + members = {n: r for n, r in repos.items() if r["category"] == cat} + if not members: + continue + if spec and spec.get("collapse"): + lines.append(f"| **{spec['label']}** | {spec['role']} |") + else: + for name, repo in members.items(): + lines.append(f"| **{name}** | {repo['role']} |") + provenance = ( + "Generated from `PyAutoMind/repos.yaml` (the body map — the single " + "source of repo identity). Edit that file, then run " + "`python3 PyAutoMind/scripts/repos_sync.py --write`." + ) + return "\n".join(lines) + "\n\n" + provenance + + +def owner_map(categories, repos): + owners = {} + for name, repo in repos.items(): + owners.setdefault(owner_of(repo), []).append(name) + majority = max(owners, key=lambda o: len(owners[o])) + exceptions = [ + f"`{repo['github']}`" + for name, repo in repos.items() + if owner_of(repo) != majority + ] + libraries = [n for n, r in repos.items() if r["category"] == "library"] + ws_cats = ("workspace", "workspace_test", "howto", "pipeline") + workspaces = [n for n, r in repos.items() if r["category"] in ws_cats] + lines = [ + f"All repos live at `{majority}/` on GitHub, except: " + + ", ".join(exceptions) + + ".", + "", + "**Library repos:** " + ", ".join(libraries) + ".", + "**Workspace repos:** " + ", ".join(workspaces) + ".", + "", + "Generated from `PyAutoMind/repos.yaml`; edit there, then run " + "`python3 PyAutoMind/scripts/repos_sync.py --write`.", + ] + return "\n".join(lines) + + +def replace_block(path, content): + text = path.read_text() + if MARK_BEGIN not in text or MARK_END not in text: + raise SystemExit(f"repos_sync: no marker block in {path}") + pattern = re.compile( + re.escape(MARK_BEGIN) + r".*?" + re.escape(MARK_END), re.DOTALL + ) + new = pattern.sub(f"{MARK_BEGIN}\n{content}\n{MARK_END}", text, count=1) + changed = new != text + if changed: + path.write_text(new) + return changed + + +# -------------------------------------------------------------------------- +# Drift checks +# -------------------------------------------------------------------------- + +def check_heart(root, repos): + problems = [] + heart_yaml = root / "PyAutoHeart/config/repos.yaml" + if not heart_yaml.exists(): + return [f"missing {heart_yaml} (skipped)"] if False else [] + data = yaml.safe_load(heart_yaml.read_text()) + for group, entries in data.get("repos", {}).items(): + for entry in entries: + name, owner = entry["name"], entry["owner"] + if name not in repos: + problems.append( + f"Heart polls '{name}' ({group}) — not in the manifest" + ) + elif owner != owner_of(repos[name]): + problems.append( + f"Heart owner for '{name}' is '{owner}', manifest says " + f"'{owner_of(repos[name])}'" + ) + return problems + + +def check_pre_build(root, repos): + script = root / "PyAutoBuild/pre_build.sh" + if not script.exists(): + return [] + names = re.findall(r'^run_workspace "([^"]+)"', script.read_text(), re.M) + return [ + f"pre_build.sh runs '{n}' — not in the manifest" + for n in names + if n not in repos + ] + + +def check_labels(root, repos): + script = root / "admin_jammy/software/ensure_workspace_labels.sh" + if not script.exists(): + return [] + block = re.search(r"REPOS=\((.*?)\)", script.read_text(), re.DOTALL) + problems = [] + for slug in block.group(1).split(): + owner, _, name = slug.partition("/") + if name not in repos: + problems.append( + f"ensure_workspace_labels targets '{slug}' — '{name}' not in the manifest" + ) + elif slug != repos[name]["github"]: + problems.append( + f"ensure_workspace_labels targets '{slug}', manifest says " + f"'{repos[name]['github']}'" + ) + return problems + + +def normalize_remote(url): + url = url.strip().removesuffix(".git") + m = re.match(r"git@github\.com:(.+)", url) + if m: + return m.group(1) + m = re.match(r"https://github\.com/(.+)", url) + if m: + return m.group(1) + return url + + +def check_origins(root, repos): + problems = [] + for name, repo in repos.items(): + checkout = root / name + if not (checkout / ".git").exists(): + continue # not checked out in this environment + result = subprocess.run( + ["git", "-C", str(checkout), "remote", "get-url", "origin"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + problems.append(f"'{name}': cannot read origin remote") + continue + actual = normalize_remote(result.stdout) + if actual != repo["github"]: + problems.append( + f"'{name}': origin is '{actual}', manifest says '{repo['github']}'" + ) + return problems + + +# -------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--write", action="store_true") + parser.add_argument("--check", action="store_true") + parser.add_argument("--root", type=Path, default=None) + args = parser.parse_args() + + mind_root = Path(__file__).resolve().parents[1] + root = args.root or mind_root.parent + categories, repos = load_manifest(mind_root) + + if args.write: + targets = { + root / "AGENTS.md": routing_table(categories, repos), + root / "PyAutoBrain/skills/WORKFLOW.md": owner_map(categories, repos), + } + for path, content in targets.items(): + changed = replace_block(path, content) + print(f"{'updated' if changed else 'unchanged'}: {path}") + + checks = { + "PyAutoHeart/config/repos.yaml": check_heart(root, repos), + "PyAutoBuild/pre_build.sh": check_pre_build(root, repos), + "ensure_workspace_labels.sh": check_labels(root, repos), + "local checkout origins": check_origins(root, repos), + } + drift = False + for label, problems in checks.items(): + status = "OK" if not problems else f"{len(problems)} mismatch(es)" + print(f"check {label}: {status}") + for p in problems: + drift = True + print(f" ✗ {p}") + sys.exit(1 if drift else 0) + + +if __name__ == "__main__": + main()