feat(sleep): add skillopt-sleep revert to undo an adopt - #248
Conversation
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.
4d427d6 to
a3e2cf4
Compare
|
Rebased onto What changed. The first revision predated #212 and carried its own adoption bookkeeping in Consequences:
Two deliberate limits, both documented in Directories adoption created are left in place. The receipt records no Revert is not WAL-journaled. It takes On the test suite. 23 new tests in They are all one cause. I have not touched it here. Happy to open a separate issue if that would be useful. Also verified: end-to-end through One reviewer question: |
Fixes #247
Summary
adopt()backed up every live file it replaced, and nothing could restore that backup — there was norevert,rollback, or undo anywhere in the CLI. Adoption can also run unattended, sincecmd_scheduleforwards--auto-adoptstraight 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.skillopt-sleep revert, undoing the most recentadoptfor a projectadoptfrom destroying its own backup when the same directory is adopted twicestatus, and points at the undo afteradoptContract
revertrestores the pre-adopt state of every live path the target staging directory touched.A live document that
adoptreplaced is restored frombackup/. A live file thatadoptcreated — you had no such file — is removed instead, because that is the state being returned to; leaving the proposal in place would undo nothing.adoptnow 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 notlatest_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 DIRreverts a specific night, so a user can walk back more than one step.revertraises 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 overbackup/, losing the only copy of the user's pre-adopt document:skillopt-sleep adoptwith no--stagingresolves tolatest_staging(project), so running it twice in a row is easy to do by accident. Built on the old behavior,revertwould restore the very regression it exists to undo._backup()now leaves an existing backup alone, andadoptreuses 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 therecorded/priorbranch inadopt().Validation
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_stagingselection, and CLI round-triptest_parse_jsonl_ignores_excessively_nested_json, twoTestOverlayIntegrationcases) reproduce unchanged onmainat9c776fcand are untouched by this PR — the overlay pair is a macOS/var→/private/varsymlink issue insuperpowers.py, the parser case is a deep-nesting guard that no longer fires on Python 3.13+run_sleep_cyclewithauto_adopt=True: revert removed both the createdSKILL.mdandCLAUDE.md, and a second revert exited 1 with "nothing to revert"python -m mkdocs build --strictpassed__main__.py:554incmd_schedule, left untouched to keep the diff focusedNotes for review
statusgained two printed lines and alatest_adopted_stagingJSON 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.