fix: stamp the template's complete/index.md so its self-heal stops causing drift - #120
Merged
Merged
Conversation
…using drift The template is not inert. It ships `.github/workflows/lifecycle_drift.yml`, whose self-heal (#116) regenerates `complete/index.md` on every push to the template's own `main` — but `MIND_RULES` DROPs `complete/*`, so spawn never produced that file. Every sync was therefore followed within seconds by a bot commit creating a file the next `--check` reported as drift, permanently. Observed on 2026-08-04: sync 51f5ae58 at 17:28:51Z, bot commit 79864dde at 17:29:12Z, and the next dispatch (run 30934170549) failed with `only in published: complete/index.md`. The self-heal only reached the template in that same sync — it was one of the five source drifts outstanding since 2026-07-27 — which is why this surfaced now rather than earlier. spawn now runs the GENERATED tree's own `scripts/lifecycle.py index --apply` after writing (lifecycle.py resolves its root from `__file__`, and rule 1 already KEEPs it). Byte-identical to the self-heal's output by construction, verified against the bot's commit. Deliberately not a constant asset here: lifecycle.py owns the index format and a second copy would drift from it. The live `complete/index.md` is still DROPped by rule 7 — a fresh template gets an empty-archive index, not the live one's wall of task slugs. Adds spec rule 6c and tests/test_spawn_template_contract.py, which fails if the stamp is removed. Refs #118 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The privacy job named tests/test_spawn_privacy.py explicitly, so tests/test_spawn_template_contract.py added in the previous commit never ran in CI — the run reported 52 passed while the suite is 55. Naming a suite by filename means anything added beside it is silently uncovered. Also widens the pull_request paths filter to tests/** so a PR touching only a test file still triggers the job. Refs #118 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jammy2211
added a commit
that referenced
this pull request
Aug 4, 2026
…PR (#126) * feat: wire the Spawn Drift self-heal — regenerate and propose a sync PR Spawn Drift detected drift but nothing regenerated. The templates are declared a generated view, yet the only thing that ever regenerated them was a human typing `/spawn --apply` — so the scheduled leg went red, stayed red, and was cleared by hand. Every green run in its history was a manual dispatch fired 16-19 seconds after such a sync, which is why it could not fail. On schedule/dispatch the workflow now regenerates and opens (or refreshes) a sync PR on each drifted template repo. Deliberately a PR, not a bot push. These repos are force-synced generated views, so an automated push would be a force-push to a published `main`; #118 — a leak that sat public for eight days — is the argument for a human seeing what gets published. That keeps the sanctioned force-push a human act while removing the "nothing regenerates" gap. ## The safety interlock `--check` collapsed every failure into exit 1. Split into: 0 CLEAN published matches the regenerated tree 1 DRIFT content differs — mechanical, safe to PROPOSE 2 UNSAFE UNMATCHED file class or canary hit — a HUMAN DECISION Only exit 1 reaches the PR path. Exit 2 fails the job and opens nothing: a canary hit means the regenerated tree carries live instance content, so a sync PR would be proposing to publish a leak — #118 with a robot doing it. A test asserts UNSAFE outranks DRIFT when both are present. ## A latent bug this surfaced Running the workflow's shell rather than only reading it found that `stamp_complete_index()` (from #120) breaks on a RELATIVE `--write DIR`: the child resolves the script path after chdir'ing to `cwd`, so `--write regenerated` — exactly what a CI step naturally passes — died with "can't open file". Every invocation to date happened to use an absolute path, so it stayed latent. Fixed by resolving, with a regression test that fails without it. ## Verified by executing it, not just reading it Both `run:` blocks were extracted and run under `bash -e` (as Actions runs them) against a CI-shaped fixture, with `git push`/`gh` stubbed: * clean -> no PR * drift -> PR opened; the already-current repo correctly skipped * drift -> existing PR REFRESHED, no duplicate (stable force-updated branch) * canary -> job fails, code=2, PR step's `if` is false, nothing proposed ## Known external dependency Opening a PR on the template repos needs write access there; GITHUB_TOKEN is scoped to PyAutoMind. Uses `secrets.PAT_PYAUTOLABS`, the org's established cross-repo token. Whether that PAT grants write to the two template repos CANNOT be verified from a local session — only a real run reveals it. The step therefore fails with an explicit, actionable message rather than silently doing nothing. Refs #125 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: a spawn crash must not be reported as drift Python exits 1 on an unhandled exception — the same code as EXIT_DRIFT. The self-heal would therefore read a crash as "the templates are stale" and try to propose a sync PR from whatever partial tree the crash left behind. Not hypothetical: stamp_complete_index() crashing on a relative --write path (fixed in the previous commit) produced exactly this exit-1-that-means-crash, and that is how it was noticed. Unhandled exceptions now exit 3, which the workflow's catch-all rejects. The traceback is still printed, so nothing is hidden. Refs #125 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: address independent review of the self-heal (6 findings) 1. HIGH — the change broke rule 9, which it had just established one PR ago. Adding the PAT-dependent proposal step to spawn_drift.yml meant the template would ship `secrets.PAT_PYAUTOLABS`, violating the no-configured-secret condition. Confirmed against a post-merge generation. Root cause in the TESTS, not just the rules: the .github fixtures were hand-written miniatures, so `test_no_shipped_workflow_needs_a_configured_secret` was checking a toy workflow that had no PAT while the real one had grown one. The fixtures now READ THE REAL WORKFLOW FILES. Control-tested: shipping spawn_drift.yml again now fails four tests, including the secret check. Rule 9b revised KEEP-with-schedule-stripped -> DROP. The self-heal makes the workflow depend on a PAT and on published *-template repos; a fresh org has neither, so every path in it is unrunnable there. "When in doubt DROP" — the generator and its guards still travel via scripts/ and tests/. The schedule-stripping transform is retired as dead code with its tests. 2. HIGH — `gh pr view` matches merged and closed PRs, so once a sync PR was merged the reused branch would report "refreshed" forever and silently never open another. Now `gh pr list --state open --head`. Both paths dry-run tested: open -> refresh, merged -> opens a new PR. 3. HIGH — every exit-code test compared a subprocess result with constants from the same module, so swapping EXIT_DRIFT and EXIT_UNSAFE would have left them green while the workflow still auto-proposed literal exit 1. Two tests now read the real workflow and pin the CONSUMER against the producer. Control-tested by swapping the constants. 4. MEDIUM — the interlock was incomplete. Fail-closed paths raise SystemExit("message"), which Python turns into exit 1: indistinguishable from EXIT_DRIFT. Those are human decisions like UNMATCHED, so they now map to EXIT_UNSAFE. The earlier crash guard only covered exceptions. 5. MEDIUM — concurrent runs were last-writer-wins on the shared proposal branch. Added a `concurrency` group that queues rather than cancels; a cancelled run could leave a pushed branch with no PR. 6. LOW — `diff` exit 2 means trouble, not differences, but every nonzero status entered the drift path, so an unreadable tree would trigger replacement and force-push. Only exit 1 is drift now; 2 aborts. Refs #125 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Jammy2211 <JNightingale2211@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Jammy2211
added a commit
that referenced
this pull request
Aug 14, 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.
Follow-on to #119 / #118, found by dispatching
Spawn Driftafter publishing rather than trusting a local--check.The problem
The template is not inert. It ships
.github/workflows/lifecycle_drift.yml, whose self-heal (#116) regeneratescomplete/index.mdon every push to the template's ownmain. ButMIND_RULESmapscomplete/*→DROP, so spawn never produced that file.Result: a self-sustaining drift loop. Every sync is followed within seconds by a bot commit creating a file spawn does not generate, which the next
--checkreports as drift — forever.Observed immediately:
and the next dispatch (run
30934170549) failed withonly in published: complete/index.md.This surfaced now because the self-heal itself only reached the template in that same sync — it was one of the five source drifts outstanding since 2026-07-27.
The fix
spawn runs the generated tree's own
scripts/lifecycle.py index --applyafter writing the tree.lifecycle.pyresolves its root from__file__, and rule 1 alreadyKEEPs it, so the template stamps its own index with the same code, over the same empty archive, as its self-heal would.Verified byte-identical against the bot's actual commit.
Deliberately not a constant asset inside spawn:
lifecycle.pyowns the index format, and a second copy of that text would drift from it the next time the format changes.The live
complete/index.mdis stillDROPped by rule 7 — a fresh-slate template gets an empty-archive index, not the live one's wall of instance task slugs.test_live_complete_index_is_never_copiedpins that.Verification
tests/test_spawn_template_contract.pyfails (2 of 3 tests) when the stamp is removed — control-tested, not assumed.--checkagainst the live published repos now reports thecomplete/index.mddrift gone; the only remaining drift isscripts/spawn.pyitself, which clears when this merges and the templates are re-synced.API Changes
None.
spawn.pyis a repo-local generator with no importers outside this repo. New module-level helperstamp_complete_index(); no signature changes to existing functions.Post-merge
Re-run
/spawn --applyto publish this, then the next scheduledSpawn Driftshould be green without a human touching it — which is the actual point of the exercise.🤖 Generated with Claude Code