fix: stop spawn stamping live registry entries into the public templates - #119
Merged
Conversation
`empty_body()` implemented the spec's EMPTY action as "keep line 1 of the live file" rather than "keep the header line". `planned.md` and `ideas.md` carry no H1 at all, so their first line is a registry entry — and spawn stamped it into the public fresh-slate templates, violating the privacy invariant in docs/pyautobrain/spawn_spec.md. The `ideas.md` line reached PyAutoMind-template in the 2026-07-27 sync (3424dba1); `planned.md`'s `## rhayes-...` slug would have shipped on the next regenerate. The canary scan reported clean throughout because CANARY_TOKENS held dataset names only — the spec's own example list names `Nightingale`, but no name token was ever implemented. - empty_body() now generates its header and never opens the source, so no future edit to a live registry file can change what a template ships. Named files use EMPTY_TITLES; glob-matched bibliography files get a generated comment header (spec rule 2). An unmapped EMPTY file raises, same doctrine as UNMATCHED. - CANARY_TOKENS gains `nightingale` and `rhayes`, with a narrow per-file, per-token CANARY_EXEMPT for LICENSE (copyright attribution) and for the two files that define/exercise the tokens. - New tests/ + spec rule 1b (KEEP) so the privacy test travels with the generator it guards. - spawn_drift.yml runs the invariant on PRs touching spawn; the drift job is skipped on PRs since it diffs published templates against main. A heading-shape test would not have caught this: `## rhayes-audit-validation-phases-2-4` is a valid `##` heading. Refs #118 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Independent review (Codex) caught that the first pass reintroduced the exact
leak it removed, through a different door.
`tests/**` and `scripts/spawn.py` are both KEEP-copied verbatim into the
public template. The test file quoted the real live `planned.md` slug and
`ideas.md` line as fixtures, and a spawn.py comment quoted the slug too — so
the generated template still shipped that content. The canary scan reported
clean only because the first pass had exempted those two files wholesale.
Verified against a generated tree: the strings were present in
gen/PyAutoMind-template/{tests/test_spawn_privacy.py,scripts/spawn.py}.
- Every fixture is now fictional, and every canary token is derived from
CANARY_TOKENS at run time instead of spelled out, so the test file scans
clean on its own merits. Its wholesale exemption is removed; only
scripts/spawn.py keeps one, because the token list has to live somewhere.
- New test_only_spawn_py_is_exempt_wholesale + test_this_test_file_is_not_exempt
so the exemption map cannot quietly grow that hole back.
- New end-to-end tests drive generate_all() over a synthetic Mind/Memory and
assert no live marker reaches the tree. The old suite unit-tested
empty_body() alone, so flipping a rule to KEEP or swapping the dispatcher
for copy2 would have stayed green — which is exactly how the above slipped
through.
- EMPTY_TITLES is keyed by repo-relative path, not basename, so a glob-matched
file sharing a root ledger's name (e.g. bibliography/active.md) takes the
unmapped-file SystemExit instead of inheriting that ledger's title.
- test_every_empty_rule_is_covered checked suffix coverage instead of
`continue`-ing past every glob, which made it vacuous.
Refs #118
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jammy2211
pushed a commit
that referenced
this pull request
Aug 4, 2026
This was referenced Aug 4, 2026
Jammy2211
added a commit
that referenced
this pull request
Aug 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #118.
Overview
scripts/spawn.py::empty_body()implemented the spec'sEMPTYaction as "keep line 1 of the live file" rather than "keep the header line". Registry ledgers with no H1 therefore had a live registry entry copied into the public fresh-slate templates, violating the hard privacy invariant atdocs/pyautobrain/spawn_spec.md:61.One leak had already shipped:
PyAutoMind-templatecommit3424dba1(2026-07-27) carries a liveideas.mdline. A second would have shipped on the next regenerate. The canary scan reportedcleanthroughout becauseCANARY_TOKENSheld dataset names only — the spec's own example list namesNightingale, but no name token was ever implemented, and the spec-mandated privacy test did not exist.What was wrong
empty_body()returnedsplitlines()[0]. Neitherplanned.mdnorideas.mdcontains an H1 anywhere, so that is never a header for them:active.md/parked.md/condemned.mdqueue.md# Pytree variant queueplanned.md##ideas.mdA heading-shape test would not have helped: a task slug written as
##is a structurally valid heading. Only an explicit map separates a title from a registry entry.Changes
empty_body()never opens the source. Named ledgers take a generated title fromEMPTY_TITLES; glob-matched bibliography files take a generated comment header (spec rule 2). An unmappedEMPTYfile raises, same doctrine asUNMATCHED. Keyed by repo-relative path, so a glob-matched file sharing a root ledger's name (bibliography/active.md) cannot inherit its title.CANARY_TOKENSgainsnightingaleandrhayes, with a narrow per-file, per-tokenCANARY_EXEMPTforLICENSE(copyright attribution is the point of a licence).tests/+ spec rule 1b (KEEP) so the privacy test travels with the generator it guards.spawn_drift.ymlruns the invariant on PRs touching spawn. Thedriftjob is skipped on PRs, since it diffs the published templates againstmainand would otherwise report unrelated pre-existing drift.Review round 2 — the fix reintroduced the leak, then hid it
An independent review (Codex) caught that the first pass republished the exact same strings through a different door.
tests/**andscripts/spawn.pyare bothKEEP-copied verbatim into the public template; the test quoted the real slug and idea line as fixtures, a spawn.py comment quoted the slug, and the first pass had exempted both files wholesale from the canary scan. Confirmed against a generated tree before fixing.Fixed by removing the cause rather than widening the exemption:
CANARY_TOKENSat run time, so the test file scans clean on its own merits and needs no exemption;scripts/spawn.pystays exempt — the token list has to live somewhere — andtest_only_spawn_py_is_exempt_wholesalefails if that hole ever grows back;generate_all()over a synthetic Mind/Memory and assert no live marker reaches the tree. The original suite unit-testedempty_body()alone, which is precisely why it stayed green while the template leaked.Verification
spawn.py(not merely at import), including the two decisive leak tests.unmatched: none.--checkshows 13 expected drifts and nothing else: 3 leak fixes (ideas/planned/queue), 2 bibliography header changes, the newtests/dir, the 2 files changed here, and 5 pre-existing source drifts.API Changes
None.
spawn.pyis a repo-local generator script with no importers outside this repo; no PyAuto library API is touched.empty_body()gained an optional second parameter and is not called anywhere else in the workspace.Follow-ups filed (not in this PR)
draft/maintenance/pyautomind/spawn_drift_has_no_generator.md—Spawn Driftdetects drift but nothing regenerates. Every green run in its history was a manual dispatch fired seconds after a human ran--apply(16 s and 19 s gaps), so that leg cannot fail and is not an independent check.draft/bug/pyautomind/spawn_keep_rules_export_instance_state.md— two further pre-existing holes surfaced by the same review:.github/**exports live incident history through owner-only substitution, andSPECIAL:autonomy_logstill parses live bytes (the same hazard fixed here).Ship notes
Heart was YELLOW at ship time on three reasons, all unrelated to this change (workspace validation, tenant-firewall manifest drift, stale release rehearsal); acknowledged by the human.
Do not run
/spawn --applyuntil this merges — regenerating from livemainbeforehand runs the old, leaking generator.🤖 Generated with Claude Code