Skip to content

fix: stop spawn stamping live registry entries into the public templates - #119

Merged
Jammy2211 merged 2 commits into
mainfrom
feature/spawn-empty-body-privacy-fix
Aug 4, 2026
Merged

fix: stop spawn stamping live registry entries into the public templates#119
Jammy2211 merged 2 commits into
mainfrom
feature/spawn-empty-body-privacy-fix

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Closes #118.

Overview

scripts/spawn.py::empty_body() implemented the spec's EMPTY action 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 at docs/pyautobrain/spawn_spec.md:61.

One leak had already shipped: PyAutoMind-template commit 3424dba1 (2026-07-27) carries a live ideas.md line. A second 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, and the spec-mandated privacy test did not exist.

What was wrong

empty_body() returned splitlines()[0]. Neither planned.md nor ideas.md contains an H1 anywhere, so that is never a header for them:

EMPTY-ruled file live line 1 verdict
active.md / parked.md / condemned.md a real H1 fine
queue.md # Pytree variant queue H1, but instance-flavoured
planned.md a live task slug written as ## leak
ideas.md a raw idea bullet leak

A 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 from EMPTY_TITLES; glob-matched bibliography files take a generated comment header (spec rule 2). An unmapped EMPTY file raises, same doctrine as UNMATCHED. Keyed by repo-relative path, so a glob-matched file sharing a root ledger's name (bibliography/active.md) cannot inherit its title.
  • CANARY_TOKENS gains nightingale and rhayes, with a narrow per-file, per-token CANARY_EXEMPT for LICENSE (copyright attribution is the point of a licence).
  • 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 the published templates against main and 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/** and scripts/spawn.py are both KEEP-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:

  • every fixture is fictional, and every canary token is derived from CANARY_TOKENS at run time, so the test file scans clean on its own merits and needs no exemption;
  • only scripts/spawn.py stays exempt — the token list has to live somewhere — and test_only_spawn_py_is_exempt_wholesale fails if that hole ever grows back;
  • end-to-end tests now drive generate_all() over a synthetic Mind/Memory and assert no live marker reaches the tree. The original suite unit-tested empty_body() alone, which is precisely why it stayed green while the template leaked.

Verification

  • 52 tests pass; the suite fails behaviourally against the pre-fix spawn.py (not merely at import), including the two decisive leak tests.
  • Regenerated against live sources: the live slug and idea line are absent from the generated tree, canary clean, unmatched: none.
  • Post-merge --check shows 13 expected drifts and nothing else: 3 leak fixes (ideas/planned/queue), 2 bibliography header changes, the new tests/ dir, the 2 files changed here, and 5 pre-existing source drifts.

API Changes

None. spawn.py is 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.mdSpawn Drift detects 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, and SPECIAL:autonomy_log still 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 --apply until this merges — regenerating from live main beforehand runs the old, leaking generator.

🤖 Generated with Claude Code

Jammy2211 and others added 2 commits August 4, 2026 18:07
`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 Jammy2211 added the pending-release Awaiting coordinated release label Aug 4, 2026
@Jammy2211
Jammy2211 merged commit ebd60f3 into main Aug 4, 2026
2 checks passed
@Jammy2211
Jammy2211 deleted the feature/spawn-empty-body-privacy-fix branch August 4, 2026 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pending-release Awaiting coordinated release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: spawn EMPTY leaks live registry entries into public templates

1 participant