diff --git a/docs/pyautobrain/spawn_spec.md b/docs/pyautobrain/spawn_spec.md index 0393f780e..33c580bd4 100644 --- a/docs/pyautobrain/spawn_spec.md +++ b/docs/pyautobrain/spawn_spec.md @@ -74,7 +74,7 @@ not. A new organism adopts delivery deliberately after configuring its manifest. | 5 | `index.md` | SUBSTITUTE → skeleton: intro line + a table with the single `wiki/example/` row + "add sub-wikis following the same schema" | | 6 | `reading-queue.md` | EMPTY → header + section-format comment | | 7 | `README.md` | GENERATE → a template README asset held inside spawn (text surgery on the live README is brittle across edits; the asset keeps `--check` round-trips stable) | -| 8 | `.github/workflows/validate.yml` | KEEP with owner substitution — self-contained (no schedule, no secrets, no sibling repos), so it clears the fresh-repo invariant. `.github/workflows/knowledge_board.yml` | DROP — it needs a GitHub Pages site the default token cannot create on a fresh repo, plus a schedule; `scripts/board.py` itself ships via rule 1, so an adopter re-adds the publisher deliberately. `.github/workflows/queue_actions.yml` | DROP — it mutates the instance reading queue from `queue-read` issues and needs the repo's labels; `scripts/queue_mark_done.py` ships via rule 1, so an adopter re-adds the processor deliberately. `.github/workflows/queue_filing.yml` | DROP — the claude-action filing workflow needs the instance's Claude OAuth secret, labels and reading queue. `.github/workflows/arxiv_refs.yml` and `.github/workflows/queue_sweep.yml` | DROP — both run on a schedule (`queue_sweep.yml` also dispatches its queue siblings with `actions: write`), which breaks the fresh-repo invariant; their scripts ship via rule 1. As in the Mind table's rule 9d there is **no `.github/**` catch-all**: a new Memory workflow is UNMATCHED and needs an explicit decision. `logo.png` (instance branding) | DROP. The old legacy-family DROP rules (root `*.bib`, PDFs, `CTI/` etc.) are retired — those files are gone from the live repo and PyAutoMemory's structure lint (`make validate-structure`, in CI) prevents their return at the source | +| 8 | `.github/workflows/validate.yml` | GENERATE a standalone content-validation workflow: check out only this repo, run `make validate`, report wikilinks, and run the three `test_validate_*` suites. The live workflow also runs board tests and checks out Brain for its theme; copying it with owner substitution produces an unusable `YOURORG/PyAutoBrain` dependency. The template retains content CI without requiring siblings or configured secrets; full board tests remain an adopter integration check once Brain is installed. `.github/workflows/knowledge_board.yml` | DROP — it needs a GitHub Pages site the default token cannot create on a fresh repo, plus a schedule; `scripts/board.py` itself ships via rule 1, so an adopter re-adds the publisher deliberately. `.github/workflows/queue_actions.yml` | DROP — it mutates the instance reading queue from `queue-read` issues and needs the repo's labels; `scripts/queue_mark_done.py` ships via rule 1, so an adopter re-adds the processor deliberately. `.github/workflows/queue_filing.yml` | DROP — the claude-action filing workflow needs the instance's Claude OAuth secret, labels and reading queue. `.github/workflows/arxiv_refs.yml` and `.github/workflows/queue_sweep.yml` | DROP — both run on a schedule (`queue_sweep.yml` also dispatches its queue siblings with `actions: write`), which breaks the fresh-repo invariant; their scripts ship via rule 1. As in the Mind table's rule 9d there is **no `.github/**` catch-all**: a new Memory workflow is UNMATCHED and needs an explicit decision. `logo.png` (instance branding) | DROP. The old legacy-family DROP rules (root `*.bib`, PDFs, `CTI/` etc.) are retired — those files are gone from the live repo and PyAutoMemory's structure lint (`make validate-structure`, in CI) prevents their return at the source | | 10 | `.claude/**`, `.codex/**` | DROP — installed hook copies and agent-discovery symlinks (recreated by the PyAutoBrain installer and session-hook propagation), not source content; mirrors the Mind table's rule 10 | **Fresh-repo invariant (hard rule, rule 9):** no workflow shipped into a diff --git a/scripts/spawn.py b/scripts/spawn.py index 5abedfd9c..9af164757 100644 --- a/scripts/spawn.py +++ b/scripts/spawn.py @@ -255,11 +255,10 @@ # The canary scan still grades every kept file (PyAutoMind#484). ("skills/*", "KEEP"), ("bibliography/*", "EMPTY"), - # Same fail-closed discipline as MIND_RULES (spec rule 9d). validate.yml is - # self-contained — no schedule, no secrets, no sibling repos — so it clears - # the fresh-repo invariant and ships. No catch-all: a new Memory workflow is - # UNMATCHED and gets an explicit decision. - (".github/workflows/validate.yml", "KEEP_SUB"), + # Memory rule 8: the live full-suite workflow needs Brain's board theme. + # Generate standalone content validation instead of copying that sibling + # checkout into a fresh repo. New workflows still fail closed as UNMATCHED. + (".github/workflows/validate.yml", "SPECIAL:memory_validate"), # DROP: the knowledge-board publisher needs a GitHub Pages site the default # token cannot create on a fresh repo (the Hands lesson) and a schedule — # both break the fresh-repo invariant. scripts/board.py itself SHIPS via @@ -446,6 +445,41 @@ # category: assistant """ +MEMORY_VALIDATE_TEMPLATE = """\ +# Generated by spawn.py: content validation needs no sibling repositories. +# Full board tests require an adopted Brain checkout and are not run here. +name: validate + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install pytest + - run: make validate + - name: Wikilink report + if: always() + run: python scripts/validate_wikilinks.py --list + - name: Content validator tests + run: >- + python -m pytest -q + tests/test_validate_literature_citations.py + tests/test_validate_structure.py + tests/test_validate_wikilinks.py +""" + + MEMORY_INDEX_TEMPLATE = """\ # PyAutoMemory — index @@ -854,6 +888,8 @@ def generate_memory(memory_root, out_dir): dest.write_text(substitute_owner(src.read_text(errors="replace"))) elif action == "EMPTY": dest.write_text(empty_body(src, rel)) + elif action == "SPECIAL:memory_validate": + dest.write_text(MEMORY_VALIDATE_TEMPLATE) elif action == "SPECIAL:memory_index": dest.write_text(MEMORY_INDEX_TEMPLATE) elif action == "SPECIAL:memory_readme": diff --git a/tests/test_spawn_template_contract.py b/tests/test_spawn_template_contract.py index 9030b2a4c..366d374f6 100644 --- a/tests/test_spawn_template_contract.py +++ b/tests/test_spawn_template_contract.py @@ -305,6 +305,51 @@ def test_memory_github_is_also_fail_closed(tmp_path): assert (out / ".github" / "workflows" / "validate.yml").exists() +def test_memory_validation_is_standalone_even_when_live_ci_needs_brain(tmp_path): + """The live board suite must not impose a sibling on a fresh template.""" + mem = tmp_path / "PyAutoMemory" + files = dict(MINIMAL_MEMORY) + files[".github/workflows/validate.yml"] = """\ +name: validate +on: [push, pull_request] +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: YOURORG/PyAutoBrain + path: PyAutoBrain + - run: make validate + - run: make test +""" + _fake_repo(mem, files) + out = tmp_path / "out" + assert spawn.generate_memory(mem, out) == [] + text = (out / ".github/workflows/validate.yml").read_text() + workflow = yaml.safe_load(text) + triggers = workflow.get("on", workflow.get(True)) + assert set(triggers) == {"push", "pull_request"} + assert workflow["permissions"] == {"contents": "read"} + assert "secrets." not in text + steps = workflow["jobs"]["validate"]["steps"] + checkouts = [s for s in steps if s.get("uses", "").startswith("actions/checkout@")] + assert len(checkouts) == 1 + assert "repository" not in checkouts[0].get("with", {}) + commands = [s["run"] for s in steps if "run" in s] + assert "make validate" in commands + assert "python scripts/validate_wikilinks.py --list" in commands + test_cmd = next(s["run"] for s in steps if s.get("name") == "Content validator tests") + assert test_cmd.split() == [ + "python", "-m", "pytest", "-q", + "tests/test_validate_literature_citations.py", + "tests/test_validate_structure.py", + "tests/test_validate_wikilinks.py", + ] + assert "make test" not in commands + + def test_spawn_stamps_the_templates_complete_index(tmp_path): """spawn must run the GENERATED tree's own lifecycle.py, not the live one.""" mind = tmp_path / "PyAutoMind"