Skip to content

feat(sleep): adopt a reviewed subset of staged skills with rollback - #189

Closed
bogdanbaciu21 wants to merge 2 commits into
microsoft:mainfrom
bogdanbaciu21:skoc-008-adopt-reviewed-skill-subset
Closed

feat(sleep): adopt a reviewed subset of staged skills with rollback#189
bogdanbaciu21 wants to merge 2 commits into
microsoft:mainfrom
bogdanbaciu21:skoc-008-adopt-reviewed-skill-subset

Conversation

@bogdanbaciu21

Copy link
Copy Markdown
Contributor

Summary

Final review-sized slice of #120: adopt an explicitly reviewed subset of a
multi-skill night's staged proposals, with receipts and all-or-nothing rollback.

In skillopt_sleep/staging.py:

  • staged_skills(staging_dir) reads the per-skill manifest rows;
  • adopt_skills(staging_dir, skill_names=None) adopts the selection —
    None = every staged skill, [] = nothing. Unknown or repeated names, unsafe
    manifest rows, and missing proposal files raise StagingError before any
    write; skills outside the selection are never touched;
  • each live file is backed up under backup/skills/<skill>/ and written
    atomically, and if any write fails the whole selection is restored (files that
    did not exist before are removed), so no partial adoption survives;
  • AdoptedSkill receipts carry skill_name, live_skill_path, sha256_before
    (empty when there was no live file), sha256_after, and backup_path, and are
    also written to adopted_skills.json beside the report.

adopt() and the legacy single-proposal path are unchanged: a night that stages
no per-skill proposals has no "skills" rows and adopt_skills() returns [].
Adoption still never happens implicitly. docs/sleep/multi-skill-staging.md
documents the layout, the subset adoption API, and what consumers of
manifest.json / report.json need to know to migrate; it is linked from
docs/sleep/README.md.

Stacked on #188 (skoc-007-stage-per-skill-proposals), which adds the staging
fan-out this adopts from.

Tests

  • python -m pytest -q tests/test_sleep_adopt_skill_subset.py → 14 passed
  • python -m pytest -q → 584 passed, 7 skipped (base main at 8304e6c:
    556 passed, 7 skipped)
  • python -m ruff check skillopt_sleep/staging.py tests/test_sleep_adopt_skill_subset.py
    → All checks passed

Covered by an end-to-end two-skill staged-night fixture: reading manifest rows,
legacy nights adopting nothing, one skill adopted while the other is untouched,
before/after hashes and backup contents, persisted receipts, empty and full
selections, a first-time live file reporting an empty before-hash, refusal of
unknown/repeated selections, missing proposals and unsafe rows, rollback of the
whole selection after a failed write (including removing newly created files), and
no adoption without an explicit call.

Refs #120

Add SkillProposal, skill_proposal_rows, and write_skill_proposals: validate skill
names, live target paths, and collisions before writing, then write each skill's
proposal atomically. write_staging gains an optional skill_proposals fan-out and
keeps the legacy single-proposal layout when it is unused.

Refs microsoft#120
Add staged_skills and adopt_skills: validate the selection first, back up and
atomically write each live skill, roll the whole selection back on failure, and
return/persist sha256 before/after receipts. Legacy single-proposal adoption is
unchanged, and multi-skill staging plus migration notes are documented.

Refs microsoft#120
Copilot AI review requested due to automatic review settings July 28, 2026 16:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends SkillOpt-Sleep’s staging/adoption pipeline to support multi-skill nights by staging one proposal per skill and enabling explicit adoption of a reviewed subset with backups, hashes, and rollback.

Changes:

  • Add per-skill proposal staging (SkillProposal, manifest "skills" rows, and atomic proposal file writes).
  • Add subset adoption API (staged_skills(), adopt_skills()) with receipts persisted to adopted_skills.json and all-or-nothing rollback.
  • Document the multi-skill staging layout and subset adoption flow and link it from the Sleep docs index.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
skillopt_sleep/staging.py Implements multi-skill proposal staging and subset adoption with backups/receipts/rollback.
tests/test_sleep_staging_fanout.py Adds hermetic unittest coverage for per-skill staging fan-out validation and file layout.
tests/test_sleep_adopt_skill_subset.py Adds end-to-end hermetic tests for subset adoption, receipts, refusal cases, and rollback behavior.
docs/sleep/README.md Links to the new multi-skill staging documentation.
docs/sleep/multi-skill-staging.md Documents multi-skill staging layout, subset adoption API, and migration notes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread skillopt_sleep/staging.py
Comment on lines +523 to +526
staged = os.path.join(staging_dir, str(row.get("proposed_file") or ""))
if not os.path.isfile(staged):
raise StagingError(f"staged proposal missing for {name!r}: {staged}")
plan.append((name, live, staged))
Comment thread skillopt_sleep/staging.py
Comment on lines +471 to +472
rows = manifest.get("skills") or []
return [row for row in rows if isinstance(row, dict)]
Comment thread skillopt_sleep/staging.py
Comment on lines +293 to +305
directory = os.path.dirname(path) or "."
os.makedirs(directory, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=directory, prefix=".tmp-", suffix=".md")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(text)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, path)
except BaseException:
if os.path.exists(tmp):
os.unlink(tmp)
raise
Comment thread skillopt_sleep/staging.py
os.makedirs(skill_backup, exist_ok=True)
backup_path = os.path.join(skill_backup, os.path.basename(live))
shutil.copy2(live, backup_path)
before = _sha256_file(live)
@bogdanbaciu21

Copy link
Copy Markdown
Contributor Author

Thanks for the welcome on #120. To keep review load bounded and follow the incremental shape Yif-Yang suggested, I'm closing this PR for now and will reopen it once #182 (the harvesting slice) lands or the maintainer asks for the next slice. The branch stays on my fork so this can be re-opened as-is. Happy to restructure if a different cadence is preferred.

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.

2 participants