feat(sleep): add `skillopt-sleep revert` to undo an adopt by pravit-amp · Pull Request #248 · microsoft/SkillOpt · GitHub
Skip to content

feat(sleep): add skillopt-sleep revert to undo an adopt - #248

Open
pravit-amp wants to merge 1 commit into
microsoft:mainfrom
pravit-amp:feat/sleep-revert
Open

feat(sleep): add skillopt-sleep revert to undo an adopt#248
pravit-amp wants to merge 1 commit into
microsoft:mainfrom
pravit-amp:feat/sleep-revert

Conversation

@pravit-amp

Copy link
Copy Markdown
Contributor

Fixes #247

Summary

adopt() backed up every live file it replaced, and nothing could restore that backup — there was no revert, rollback, or undo anywhere in the CLI. Adoption can also run unattended, since cmd_schedule forwards --auto-adopt straight into the installed cron entry / Scheduled Task, so a night that regressed the live skill left the user hand-copying files out of a timestamped staging directory.

  • adds skillopt-sleep revert, undoing the most recent adopt for a project
  • records in the manifest what each adopt replaced, so revert knows what to put back
  • stops adopt from destroying its own backup when the same directory is adopted twice
  • reports the revert target in status, and points at the undo after adopt

Contract

revert restores the pre-adopt state of every live path the target staging directory touched.

A live document that adopt replaced is restored from backup/. A live file that adopt created — you had no such file — is removed instead, because that is the state being returned to; leaving the proposal in place would undo nothing. adopt now records which case each live path was in, since after adoption the file exists either way and the distinction is no longer observable.

Without --staging, the target is the most recently adopted directory that has not already been reverted — deliberately not latest_staging(), whose newest entry may never have been adopted, in which case restoring its backup would undo a change the live files never received. --staging DIR reverts a specific night, so a user can walk back more than one step.

revert raises rather than reporting a successful no-op when the target was never adopted, or when a backup the manifest claims is missing from disk.

Staging directories adopted before this change carry no record, only a backup/ directory. Those are still revertible: their backup files are matched to the manifest's live paths by basename. A file such an adopt created was never backed up and is indistinguishable from one it never touched, so those are left alone rather than deleted on a guess.

The backup fix is a prerequisite, not scope creep

_backup() overwrote unconditionally. Adopting the same staging directory twice copied the already-applied proposal over backup/, losing the only copy of the user's pre-adopt document:

adopt(st); print(open(f"{st}/backup/SKILL.md").read())   # ORIGINAL hand-written skill
adopt(st); print(open(f"{st}/backup/SKILL.md").read())   # PROPOSED (regressed) skill

skillopt-sleep adopt with no --staging resolves to latest_staging(project), so running it twice in a row is easy to do by accident. Built on the old behavior, revert would restore the very regression it exists to undo. _backup() now leaves an existing backup alone, and adopt reuses the recorded entry for a live path instead of recomputing it against the file adopt itself just wrote.

This is listed separately under Fixed in the changelog and splits cleanly into its own PR if maintainers prefer — the cut is _backup() plus the recorded/prior branch in adopt().

Validation

  • 16 new tests in tests/test_sleep_revert.py — restore, remove-created, skill+memory together, idempotent revert, double-adopt backup safety, never-adopted and missing-backup refusals, legacy staging dirs, latest_adopted_staging selection, and CLI round-trip
  • full suite: 1,110 passed, 10 skipped, 130 subtests passed
  • three failures on this branch (test_parse_jsonl_ignores_excessively_nested_json, two TestOverlayIntegration cases) reproduce unchanged on main at 9c776fc and are untouched by this PR — the overlay pair is a macOS /var/private/var symlink issue in superpowers.py, the parser case is a deep-nesting guard that no longer fires on Python 3.13+
  • end-to-end through run_sleep_cycle with auto_adopt=True: revert removed both the created SKILL.md and CLAUDE.md, and a second revert exited 1 with "nothing to revert"
  • python -m mkdocs build --strict passed
  • Ruff clean on the changed files, except one pre-existing unsorted-import finding at __main__.py:554 in cmd_schedule, left untouched to keep the diff focused

Notes for review

status gained two printed lines and a latest_adopted_staging JSON key. That was edge case 4 in the issue — nothing previously distinguished an adopted proposal from a staged one — and it makes the default revert target visible before you run it. Happy to drop it if you would rather keep this PR to the command alone.

Adoption had no undo. adopt() and adopt_skills() take an immutable backup
and publish a receipt, but nothing read either one back, so a night that
regressed a live skill left the user hand-copying files out of a
timestamped staging directory. That matters most unattended: schedule
forwards --auto-adopt into the installed cron entry.

revert() and revert_skills() reverse the receipts microsoft#212 already writes,
which pin the live path, the sha256 before adoption ("" when no file
existed), the sha256 adoption wrote, and the immutable backup. A document
adoption replaced is restored from its backup; one adoption created is
removed, since that is the state being returned to. Selection mirrors
adopt: --skill, --all-skills, --legacy, --staging.

Reverting consumes the backup and drops the night's receipt rows, so the
night can be adopted again — adoption refuses to run while an immutable
backup is present.

It refuses when the live file no longer matches what adoption wrote: it
was edited since, and restoring the backup would discard that work.
Directories adoption created are left alone, because the receipt records
no created_dirs and removing a path whose ownership was not durably
recorded is what the adoption recovery path already refuses.
@pravit-amp

Copy link
Copy Markdown
Contributor Author

@Yif-Yang

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skillopt-sleep: adopt() writes a backup that nothing can restore — add a revert command

2 participants