spawn: classify the three files that reddened Spawn Drift, and catch the next one at PR time - #314
Merged
Merged
Conversation
The weekly spawn_drift run failed on 2026-08-24 with exit 2 (UNSAFE):
three files on Mind main matched no MIND_RULES entry, so the partition
could not decide whether they travel into PyAutoMind-template.
✗ .github/workflows/firewall_gate.yml
✗ .github/workflows/pages_dashboard.yml
✗ dashboard.html
That is rule 9d working as designed, not a bug: there is deliberately no
.github/* catch-all, because a fallback is fail-OPEN and would ride a new
workflow into the template carrying whatever schedule and secrets it has.
The canary scan was clean, so nothing leaked — the run stopped before
proposing a sync.
All three are DROP under the fresh-repo invariant:
- firewall_gate.yml checks out PyAutoLabs/{PyAutoBrain,PyAutoHeart,
PyAutoHands} by name. That is dashboard_refresh.yml's failure mode three
times over — a fresh org has no such siblings, and owner substitution
only turns them into YOURORG/... placeholders.
- pages_dashboard.yml needs a GitHub Pages site the default token cannot
create on a fresh repo — the Hands lesson already recorded for Memory's
knowledge_board.yml — and takes pages:write + id-token:write.
- dashboard.html is the Pages twin of dashboard.md. DROP rather than
dashboard.md's EMPTY: there is no broken-link argument to answer here,
since no shipped file links it and its publisher is dropped above.
Spec tables extended first, then mirrored into spawn.py, per the error
message's own instruction. The template contract test gains both
workflows in its fixture and its DROP list; verified non-vacuous by
removing the firewall_gate rule again, which fails
test_check_exit_codes_are_the_self_heal_contract on exit != EXIT_CLEAN.
Verified: 0 of 1395 tracked files now UNMATCHED, lifecycle_drift.yml
still KEEP (rule 9 does not over-drop), 186 tests pass, lifecycle check
and index --check both OK.
Worth noting separately, not fixed here: spawn_drift's pull_request
trigger is path-filtered to scripts/spawn.py, tests/**, the spec and its
own file. A PR adding a workflow or a root file therefore never runs it,
which is why all three sat undetected until the Monday cron.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4MvhHTXxPwffptbXsP8xR
The three UNMATCHED files fixed in bac5102 reached main because nothing at PR time ever looked at the real file list. Closing that needs two changes, not one — either alone is inert. 1. Drop spawn_drift's pull_request `paths:` filter. It watched the GENERATOR — spawn.py, tests/, the spec, itself. That is the wrong input set: the partition's other input is the source tree, so a PR adding a file that matches no rule is exactly what needs checking and exactly what the filter excluded. All three offenders landed that way (two workflows and a root file), and none of them touched a watched path, so the workflow never ran on their PRs. Cheap, because the `drift` job is already skipped on pull_request: a PR runs only the hermetic `privacy` job — pytest over synthetic inputs, no clones, no secrets, no writes. 2. Add test_no_tracked_file_is_unmatched_by_mind_rules. Widening the trigger alone would have caught nothing. Every other test in this file builds a synthetic tree, so it only covers file classes somebody remembered to add to the fixture — a new real workflow is invisible to all of them. This one reads `git ls-files` and asserts every tracked path resolves to a MIND_RULES entry: the same condition the drift job reports as UNMATCHED / exit 2, minus the clones. MEMORY_RULES cannot be checked from here (PyAutoMemory is not a sibling in this checkout), so it stays covered by the weekly run. Verified non-vacuous: adding a tracked .github/workflows/zzz_probe.yml fails the new test naming the file and both places to fix it; removing it passes. 187 tests pass. Workflow YAML re-parsed — `pull_request:` with no value fires on all PRs, and the drift job's skip gate is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4MvhHTXxPwffptbXsP8xR
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.
The failure
The weekly
spawn_driftrun failed on 2026-08-24 (run 32701028244) with exit 2 (UNSAFE). Three files onmainmatched noMIND_RULESentry, so the partition could not decide whether they travel intoPyAutoMind-template:That is rule 9d working as designed, not a bug: there is deliberately no
.github/*catch-all, because a fallback is fail-OPEN and would ride a new workflow into the template carrying whatever schedule and secrets it has. The canary scan was clean, so nothing leaked — the run stopped before proposing a sync.Everything else in Mind CI was and is green; this was the only red workflow.
Part 1 — classify the three files (
bac5102)All three are DROP under the spec's fresh-repo invariant:
firewall_gate.ymlPyAutoLabs/{PyAutoBrain,PyAutoHeart,PyAutoHands}by name —dashboard_refresh.yml's failure mode three times over. A fresh org has no such siblings, and owner substitution only turns them intoYOURORG/...placeholderspages_dashboard.ymlknowledge_board.yml— and takespages: write+id-token: writedashboard.htmldashboard.md. DROP rather than 7b's EMPTY: there is no broken-link argument to answer, since no shipped file links it and its publisher is dropped aboveSpec tables extended first, then mirrored into
spawn.py, per the error message's own instruction. Also fixed a stray "The rest They" in the 9c cell being rewritten.Part 2 — catch the next one at PR time (
661be2b)The three files reached
mainbecause nothing at PR time ever looked at the real file list. Closing that needs two changes; either alone is inert.Dropped
spawn_drift'spull_request:paths filter. It watched the generator —spawn.py,tests/, the spec, itself. That is the wrong input set: the partition's other input is the source tree, so a PR adding a file that matches no rule is exactly what needs checking and exactly what the filter excluded. None of the three offenders touched a watched path, so the workflow never ran on their PRs. This is cheap rather than heavy, because thedriftjob is already skipped onpull_request— a PR runs only the hermeticprivacyjob (pytest, synthetic inputs, no clones, no secrets, no writes).Added
test_no_tracked_file_is_unmatched_by_mind_rules. Widening the trigger alone would have caught nothing: every other test in that file builds a synthetic tree, so it only covers file classes somebody remembered to add to the fixture. The new test readsgit ls-filesand asserts every tracked path resolves to aMIND_RULESentry — the same condition the drift job reports as UNMATCHED/exit 2, minus the clones.MEMORY_RULEScannot be checked from this checkout (PyAutoMemory is not a sibling), so it stays covered by the weekly run.Verification
lifecycle_drift.ymlstill resolves to KEEP, so rule 9 has not over-droppedlifecycle checkandlifecycle index --checkboth OKpull_request:with no value fires on all PRs, and the drift job's skip gate is unchangedfirewall_gaterule again failstest_check_exit_codes_are_the_self_heal_contractonexit != EXIT_CLEAN; adding a tracked.github/workflows/zzz_probe.ymlfails the new test, naming the file and both places to fix itExpected follow-on
This converts exit 2 (UNSAFE, hard stop) into exit 1 (DRIFT). Once on
main, the next scheduled or dispatched run takes the self-heal path atspawn_drift.yml:92and proposes a sync PR against the published template repos — the designed happy path, but it does mean a follow-up PR will appear.Generated by Claude Code