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 docs/pyautobrain/spawn_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 41 additions & 5 deletions scripts/spawn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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":
Expand Down
45 changes: 45 additions & 0 deletions tests/test_spawn_template_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading