Skip to content

feat: wire the Spawn Drift self-heal — regenerate and propose a sync PR - #126

Merged
Jammy2211 merged 3 commits into
mainfrom
feature/spawn-drift-self-heal
Aug 4, 2026
Merged

feat: wire the Spawn Drift self-heal — regenerate and propose a sync PR#126
Jammy2211 merged 3 commits into
mainfrom
feature/spawn-drift-self-heal

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Closes #125. The last piece of the #118 arc.

Overview

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 precisely 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. This keeps the sanctioned force-push a human act while removing the "nothing regenerates" gap.

The safety interlock

--check collapsed every failure into exit 1. Now:

code meaning reaches the PR step?
0 clean
1 content drift — mechanical yes
2 UNMATCHED or canary hit — human decision no
3 unhandled exception no

Only exit 1 proposes. 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.

Two defects found by executing the workflow, not reading it

Both run: blocks were extracted and run under bash -e against a CI-shaped fixture with git push/gh stubbed. That surfaced:

  • A latent bug from fix: stamp the template's complete/index.md so its self-heal stops causing drift #120. stamp_complete_index() broke on a relative --write DIR — the child resolves the script path after chdir'ing to cwd, so --write regenerated (what a CI step naturally passes) died with "can't open file". Every invocation to date happened to use an absolute path.
  • A crash was indistinguishable from drift. Python exits 1 on an unhandled exception, so the self-heal would have read the above crash as "templates are stale" and proposed a PR from a partial tree.

Independent review (Codex) — six findings, all real, all fixed

The most important: this change broke rule 9, which it had established one PR ago. Adding the PAT-dependent step meant the template would ship secrets.PAT_PYAUTOLABS, violating the no-configured-secret condition. Confirmed against a post-merge generation.

The root cause was in the tests: the .github fixtures were hand-written miniatures, so test_no_shipped_workflow_needs_a_configured_secret was checking a toy workflow with no PAT while the real one had grown one. Fixtures now read the real workflow files; control-tested — shipping spawn_drift.yml again fails four tests.

Rule 9b accordingly revised KEEP-with-schedule-stripped → DROP (your call): the self-heal makes the workflow depend on a PAT and on published *-template repos, so every path in it is unrunnable in a fresh org. The generator and its guards still travel via scripts/ and tests/. The schedule-strip transform is retired as dead code.

Also fixed:

  • gh pr view matches merged and closed PRs, so once a sync PR was merged the reused branch would report "refreshed" forever and never open another. Now gh pr list --state open --head; both paths dry-run tested.
  • Every exit-code test compared a subprocess result with constants from the same module — swapping EXIT_DRIFT/EXIT_UNSAFE would have left them green while the workflow still auto-proposed literal exit 1. Two tests now pin the consumer against the producer by reading the real workflow. Control-tested by swapping the constants.
  • Fail-closed paths raise SystemExit("message") → exit 1, indistinguishable from drift. Those are human decisions like UNMATCHED, so they now map to EXIT_UNSAFE. The earlier crash guard only covered exceptions.
  • Added a concurrency group (queue, not cancel — a cancelled run could leave a pushed branch with no PR).
  • diff exit 2 means trouble, not differences; only exit 1 is drift now, 2 aborts rather than force-pushing over a tree it could not read.

Verification

  • 79 tests pass. Post-merge generation ships only lifecycle_drift.yml and no configured secrets.
  • Dry-run under bash -e: clean → no PR; drift → PR opened with the already-current repo skipped; open PR → refreshed, no duplicate; merged PR → new one opened; canary → job fails, code=2, nothing proposed.

API Changes

None outside this repo. spawn.py is a repo-local generator. --check gains distinct exit codes; all failure codes remain non-zero, so anything treating it as a boolean is unaffected. unscheduled_workflow_body() is removed (added and retired within this issue).

Known external dependency — cannot be verified locally

Opening a PR on the template repos needs write access there; GITHUB_TOKEN is scoped to PyAutoMind. This uses secrets.PAT_PYAUTOLABS, the org's established cross-repo token (nightly-release.yml, PyAutoHands/release.yml).

Whether that PAT grants write to the two template repos can only be established by a real run. The step fails with an explicit, actionable message rather than silently doing nothing. If it turns out the PAT is not scoped for this, the fix is a token change, not a code change.

Ship notes

Heart YELLOW (red_reasons: []), human-acknowledged for this exact set: workspace validation not passing (2 failed, cloud#30938311069, autolens interferometer notebooks) and tenant-firewall manifest drift. Neither relates to this change.

Post-merge: /spawn --apply to publish, then the first scheduled run (Monday 06:17 UTC) is the real test — the leg that has never passed on its own.

🤖 Generated with Claude Code

Jammy2211 and others added 3 commits August 4, 2026 20:45
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>
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>
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>
@Jammy2211 Jammy2211 added the pending-release Awaiting coordinated release label Aug 4, 2026
@Jammy2211
Jammy2211 merged commit 8314b59 into main Aug 4, 2026
2 checks passed
@Jammy2211
Jammy2211 deleted the feature/spawn-drift-self-heal branch August 4, 2026 20:07
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.

feat: Spawn Drift detects drift but nothing regenerates — wire the self-heal PR

1 participant