Overview
scripts/spawn.py::empty_body() implements the spec's EMPTY action as "keep line 1 of the live file" rather than "keep the header line". For registry files with no H1, line 1 is instance content, so spawn stamps live registry entries into the fresh-slate template repos — violating the hard privacy invariant at docs/pyautobrain/spawn_spec.md:61.
One leak is already published in PyAutoLabs/PyAutoMind-template (commit 3424dba1, 2026-07-27). A second would ship on the next regenerate. The canary scan reports clean throughout because CANARY_TOKENS holds dataset names only, and the spec-mandated privacy test was never written.
This also explains the chronic Spawn Drift red: keying on line 1 couples the template to files that ordinary daily work rewrites.
Plan
- Make
EMPTY never copy source bytes. Named registry files get a deterministic title from an EMPTY_TITLES map; glob-matched bibliography files get a generated header comment (spec rule 2 already specifies this).
- Treat an
EMPTY-ruled file with no mapped title as a hard failure, same doctrine as UNMATCHED — never classify ad hoc.
- Widen
CANARY_TOKENS with the name tokens the spec already names (nightingale, rhayes).
- Add the privacy test required by
spawn_spec.md:62, which does not currently exist.
- Regenerate and force-sync both templates, clearing the published leak and the five legitimate source drifts.
- Do not edit
spawn_spec.md — the spec is correct; the implementation never mirrored it.
Detailed implementation plan
Affected Repositories
Branch Survey
| Repository |
Current Branch |
Dirty? |
| ./PyAutoMind |
main |
clean |
Suggested branch: feature/spawn-empty-body-privacy-fix
Evidence
Neither planned.md nor ideas.md contains an H1 anywhere (grep "^# " finds nothing in either), so empty_body() can never yield a header for them:
| EMPTY-ruled file |
live line 1 |
verdict |
active.md |
# Active Tasks |
header — fine |
parked.md |
# Parked tasks |
header — fine |
condemned.md |
# Condemned material |
header — fine |
queue.md |
# Pytree variant queue |
header, but instance-flavoured |
planned.md |
## rhayes-audit-validation-phases-2-4 |
registry entry — leak |
ideas.md |
- lens_calc_zero_contour_jax autolens workspace guide. |
registry entry — leak |
Implementation Steps
scripts/spawn.py — add EMPTY_TITLES beside CANARY_TOKENS:
active.md→# Active Tasks, planned.md→# Planned, parked.md→# Parked tasks,
condemned.md→# Condemned material, ideas.md→# Ideas, queue.md→# Queue,
reading-queue.md→# Reading queue.
scripts/spawn.py:338 — rewrite empty_body(src) to key on src.name: mapped title if
present; else for .bib/.yaml/.yml emit a generated %/# header comment; else
raise SystemExit with an UNMATCHED-style message. Delete the splitlines()[0] read
outright rather than documenting it.
scripts/spawn.py — CANARY_TOKENS += ("nightingale", "rhayes").
- New
tests/test_spawn_privacy.py (no tests/ dir exists yet): generate both trees into
tmp_path; assert every EMPTY output equals exactly its mapped title + marker (no byte
derived from its source); assert canary_scan() returns empty; assert an unmapped EMPTY
file raises.
A heading-shape test is deliberately not used: ## rhayes-audit-validation-phases-2-4 is a
valid ## heading, so shape cannot separate a title from a registry entry. Only an explicit
map can.
Expected drift after the fix
The fix itself changes ideas.md, planned.md, queue.md, bibkey_aliases.yaml and
pyautomemory.bib in the generated tree — all resolved by the same --apply, alongside the
five pre-existing source drifts (lifecycle.py, repos_sync.py, lifecycle_drift.yml,
Memory validate.yml, Memory validate_structure.py).
Key Files
scripts/spawn.py — empty_body() (line 338), CANARY_TOKENS (line 102), canary_scan() (line 430)
docs/pyautobrain/spawn_spec.md — rules 5 and 2, privacy invariant at line 61 (read-only)
tests/test_spawn_privacy.py — new
Out of scope (follow-up)
Spawn Drift detects drift but nothing regenerates. Every green run to date was a manual
dispatch fired seconds after a human ran spawn --apply (2026-07-13 sync 11:08:46Z →
dispatch 11:09:02Z; 2026-07-27 sync 19:30:29Z → dispatch 19:30:48Z), so that leg cannot
fail and is not an independent check. The workflow has no trigger-dependent behaviour — no
actions/checkout, no github.ref, no github.event_name, no if: — both legs run identical
code. A separate prompt will propose the lifecycle_drift.yml self-heal pattern (#116),
opening a PR rather than pushing, since these are force-synced generated views.
Original Prompt
Click to expand starting prompt
The PyAutoMind spawn_drift scheduled workflow has failed on every
scheduled run since 2026-07-20 and passes only on manual dispatch.
Latest failure: PyAutoMind run 30804113655 (2026-08-03T10:05Z), step
"Regenerate + diff", exit 1, with 6 drifts:
PyAutoMind-template: planned.md
.github/workflows/lifecycle_drift.yml
scripts/lifecycle.py
scripts/repos_sync.py
PyAutoMemory-template: .github/workflows/validate.yml
scripts/validate_structure.py
Canary scans were clean and "unmatched: none" on both. Regenerate the
templates from their live sources so the drift check goes green, and say
why the scheduled leg diverges from the manual-dispatch leg — a check
that only passes when a human pushes the button isn't a check.
Overview
scripts/spawn.py::empty_body()implements the spec'sEMPTYaction as "keep line 1 of the live file" rather than "keep the header line". For registry files with no H1, line 1 is instance content, so spawn stamps live registry entries into the fresh-slate template repos — violating the hard privacy invariant atdocs/pyautobrain/spawn_spec.md:61.One leak is already published in
PyAutoLabs/PyAutoMind-template(commit3424dba1, 2026-07-27). A second would ship on the next regenerate. The canary scan reportscleanthroughout becauseCANARY_TOKENSholds dataset names only, and the spec-mandated privacy test was never written.This also explains the chronic
Spawn Driftred: keying on line 1 couples the template to files that ordinary daily work rewrites.Plan
EMPTYnever copy source bytes. Named registry files get a deterministic title from anEMPTY_TITLESmap; glob-matched bibliography files get a generated header comment (spec rule 2 already specifies this).EMPTY-ruled file with no mapped title as a hard failure, same doctrine asUNMATCHED— never classify ad hoc.CANARY_TOKENSwith the name tokens the spec already names (nightingale,rhayes).spawn_spec.md:62, which does not currently exist.spawn_spec.md— the spec is correct; the implementation never mirrored it.Detailed implementation plan
Affected Repositories
Branch Survey
Suggested branch:
feature/spawn-empty-body-privacy-fixEvidence
Neither
planned.mdnorideas.mdcontains an H1 anywhere (grep "^# "finds nothing in either), soempty_body()can never yield a header for them:active.md# Active Tasksparked.md# Parked taskscondemned.md# Condemned materialqueue.md# Pytree variant queueplanned.md## rhayes-audit-validation-phases-2-4ideas.md- lens_calc_zero_contour_jax autolens workspace guide.Implementation Steps
scripts/spawn.py— addEMPTY_TITLESbesideCANARY_TOKENS:active.md→# Active Tasks,planned.md→# Planned,parked.md→# Parked tasks,condemned.md→# Condemned material,ideas.md→# Ideas,queue.md→# Queue,reading-queue.md→# Reading queue.scripts/spawn.py:338— rewriteempty_body(src)to key onsrc.name: mapped title ifpresent; else for
.bib/.yaml/.ymlemit a generated%/#header comment; elseraise SystemExitwith an UNMATCHED-style message. Delete thesplitlines()[0]readoutright rather than documenting it.
scripts/spawn.py—CANARY_TOKENS+=("nightingale", "rhayes").tests/test_spawn_privacy.py(notests/dir exists yet): generate both trees intotmp_path; assert everyEMPTYoutput equals exactly its mapped title + marker (no bytederived from its source); assert
canary_scan()returns empty; assert an unmappedEMPTYfile raises.
A heading-shape test is deliberately not used:
## rhayes-audit-validation-phases-2-4is avalid
##heading, so shape cannot separate a title from a registry entry. Only an explicitmap can.
Expected drift after the fix
The fix itself changes
ideas.md,planned.md,queue.md,bibkey_aliases.yamlandpyautomemory.bibin the generated tree — all resolved by the same--apply, alongside thefive pre-existing source drifts (
lifecycle.py,repos_sync.py,lifecycle_drift.yml,Memory
validate.yml, Memoryvalidate_structure.py).Key Files
scripts/spawn.py—empty_body()(line 338),CANARY_TOKENS(line 102),canary_scan()(line 430)docs/pyautobrain/spawn_spec.md— rules 5 and 2, privacy invariant at line 61 (read-only)tests/test_spawn_privacy.py— newOut of scope (follow-up)
Spawn Driftdetects drift but nothing regenerates. Every green run to date was a manualdispatch fired seconds after a human ran
spawn --apply(2026-07-13 sync11:08:46Z→dispatch
11:09:02Z; 2026-07-27 sync19:30:29Z→ dispatch19:30:48Z), so that leg cannotfail and is not an independent check. The workflow has no trigger-dependent behaviour — no
actions/checkout, nogithub.ref, nogithub.event_name, noif:— both legs run identicalcode. A separate prompt will propose the
lifecycle_drift.ymlself-heal pattern (#116),opening a PR rather than pushing, since these are force-synced generated views.
Original Prompt
Click to expand starting prompt