diff --git a/docs/superpowers/plans/2026-06-15-plugin-skill-mirroring.md b/docs/superpowers/plans/2026-06-15-plugin-skill-mirroring.md index c984448..14bd395 100644 --- a/docs/superpowers/plans/2026-06-15-plugin-skill-mirroring.md +++ b/docs/superpowers/plans/2026-06-15-plugin-skill-mirroring.md @@ -82,7 +82,7 @@ Add tests: - Test: `app-instance/backend/tests/unit/test_plugin_hashing.py` - Test: `app-instance/backend/tests/unit/test_config_loader.py` -- [ ] **Step 1: Write failing manifest validation tests** +- [x] **Step 1: Write failing manifest validation tests** Create tests covering: @@ -156,7 +156,7 @@ def test_skill_tree_hash_changes_when_supporting_file_changes(tmp_path: Path) -> Also verify path changes and executable-bit changes affect `skill_tree_hash`, while mtime and non-executable permission changes do not. -- [ ] **Step 2: Run tests and verify failure** +- [x] **Step 2: Run tests and verify failure** Run: @@ -167,7 +167,7 @@ pytest tests/unit/test_plugin_manifest.py tests/unit/test_plugin_hashing.py test Expected: FAIL because `beaver.plugins` and `PluginsConfig` do not exist. -- [ ] **Step 3: Implement immutable plugin models and config** +- [x] **Step 3: Implement immutable plugin models and config** Put plugin package models in `beaver/plugins/models.py`: @@ -229,7 +229,7 @@ def _parse_plugins(raw: Any) -> PluginsConfig: ) ``` -- [ ] **Step 4: Implement strict JSON manifest loading** +- [x] **Step 4: Implement strict JSON manifest loading** `load_plugin_manifest()` must: @@ -242,7 +242,7 @@ def _parse_plugins(raw: Any) -> PluginsConfig: 7. initialize `display_path` without exposing an absolute path; 8. return frozen dataclasses. -- [ ] **Step 5: Implement deterministic dual hashing** +- [x] **Step 5: Implement deterministic dual hashing** `hash_plugin_skill_tree(root)` must: @@ -258,7 +258,7 @@ def _parse_plugins(raw: Any) -> PluginsConfig: Use length-prefixed binary fields in the digest input instead of ambiguous string concatenation. -- [ ] **Step 6: Run focused tests** +- [x] **Step 6: Run focused tests** ```bash cd app-instance/backend @@ -267,7 +267,7 @@ pytest tests/unit/test_plugin_manifest.py tests/unit/test_plugin_hashing.py test Expected: PASS. -- [ ] **Step 7: Commit** +- [x] **Step 7: Commit** ```bash git add app-instance/backend/beaver/plugins app-instance/backend/beaver/foundation/config app-instance/backend/tests/unit/test_plugin_manifest.py app-instance/backend/tests/unit/test_plugin_hashing.py app-instance/backend/tests/unit/test_config_loader.py @@ -287,7 +287,7 @@ git commit -m "feat(plugins): add declarative skill manifest" - Test: `app-instance/backend/tests/unit/test_plugin_state.py` - Test: `app-instance/backend/tests/unit/test_workspace_write_lock.py` -- [ ] **Step 1: Write failing discovery and state tests** +- [x] **Step 1: Write failing discovery and state tests** Cover workspace discovery, configured search paths, duplicate plugin IDs, malformed manifests reported as errors instead of crashing the full scan, and state round trips: @@ -321,7 +321,7 @@ Add a multiprocess lock test in which two processes enter the same workspace loc assert their critical sections never overlap. Add a reentrancy test in which nested acquisitions in one process complete without deadlock. -- [ ] **Step 2: Run tests and verify failure** +- [x] **Step 2: Run tests and verify failure** ```bash cd app-instance/backend @@ -330,7 +330,7 @@ pytest tests/unit/test_plugin_state.py tests/unit/test_workspace_write_lock.py - Expected: FAIL because discovery and state stores are missing. -- [ ] **Step 3: Implement state dataclasses** +- [x] **Step 3: Implement state dataclasses** Add backward-compatible `to_dict()` and `from_dict()` methods for: @@ -358,7 +358,7 @@ class PluginState: skills: dict[str, PluginSkillBinding] = field(default_factory=dict) ``` -- [ ] **Step 4: Implement atomic state persistence** +- [x] **Step 4: Implement atomic state persistence** Store data at `/.beaver/plugins/state.json`. Write a complete JSON document to `state.json.tmp`, flush it, then replace `state.json`. Public methods: @@ -371,7 +371,7 @@ upsert_plugin(plugin_state) update_skill_binding(plugin_id, skill_name, binding) ``` -- [ ] **Step 5: Implement the shared workspace write lock** +- [x] **Step 5: Implement the shared workspace write lock** Add: @@ -395,7 +395,7 @@ Requirements: - raise `WorkspaceWriteLockBusy` on timeout/contention; - keep the lock file separate from atomically replaced data files. -- [ ] **Step 6: Implement discovery** +- [x] **Step 6: Implement discovery** Scan: @@ -409,7 +409,7 @@ manifest display path when possible and a redacted `//beaver.plugin.json` path otherwise; absolute paths remain internal. -- [ ] **Step 7: Run focused tests** +- [x] **Step 7: Run focused tests** ```bash cd app-instance/backend @@ -418,7 +418,7 @@ pytest tests/unit/test_plugin_state.py tests/unit/test_workspace_write_lock.py t Expected: PASS. -- [ ] **Step 8: Commit** +- [x] **Step 8: Commit** ```bash git add app-instance/backend/beaver/plugins app-instance/backend/beaver/foundation/utils/file_lock.py app-instance/backend/tests/unit/test_plugin_state.py app-instance/backend/tests/unit/test_workspace_write_lock.py @@ -436,7 +436,7 @@ git commit -m "feat(plugins): discover packages and persist state" - Modify: `app-instance/backend/beaver/skills/specs/__init__.py` - Test: `app-instance/backend/tests/unit/test_plugin_skill_storage.py` -- [ ] **Step 1: Write failing snapshot storage tests** +- [x] **Step 1: Write failing snapshot storage tests** Test exact content, supporting files, idempotence, symlink rejection, and source immutability: @@ -478,7 +478,7 @@ Also test: - promoting a staged snapshot uses `os.replace()` and is idempotent; - a failed metadata write leaves no current pointer to the staged version. -- [ ] **Step 2: Run test and verify failure** +- [x] **Step 2: Run test and verify failure** ```bash cd app-instance/backend @@ -487,7 +487,7 @@ pytest tests/unit/test_plugin_skill_storage.py -q Expected: FAIL because upstream snapshot APIs do not exist. -- [ ] **Step 3: Add upstream snapshot models** +- [x] **Step 3: Add upstream snapshot models** Add: @@ -510,7 +510,7 @@ Add `LoadedSkillUpstreamSnapshot(snapshot, content, root)` for storage reads. Ex complete version-tree hash, while `read_published_skill()` derives it for legacy metadata that lacks the field. -- [ ] **Step 4: Add safe tree-copy helper** +- [x] **Step 4: Add safe tree-copy helper** Refactor a private `SkillSpecStore._copy_regular_tree(source_root, target_root)` that: @@ -522,7 +522,7 @@ Refactor a private `SkillSpecStore._copy_regular_tree(source_root, target_root)` Use it for transaction staging now; Task 4 will reuse it for mirrored versions. -- [ ] **Step 5: Implement same-filesystem staging and promotion** +- [x] **Step 5: Implement same-filesystem staging and promotion** `PluginSkillTransaction` creates: @@ -542,7 +542,7 @@ cleanup() `promote_directory()` uses `os.replace()` and never replaces an existing non-identical immutable directory. Cleanup removes only the transaction's staging root. -- [ ] **Step 6: Implement snapshot APIs** +- [x] **Step 6: Implement snapshot APIs** Write snapshots to: @@ -561,14 +561,14 @@ promote_upstream_snapshot(transaction, snapshot) read_upstream_snapshot(skill_name, source_id, skill_tree_hash) ``` -- [ ] **Step 7: Make JSON/current/index writes atomic** +- [x] **Step 7: Make JSON/current/index writes atomic** Change `SkillSpecStore._write_json()` and current/index pointer writes to create a temporary file in the target directory, flush and `fsync`, then `os.replace()`. Immutable version directories are promoted first; runtime visibility changes only when `current.json`, `skill.json`, and the published index are atomically replaced under the workspace lock. -- [ ] **Step 8: Run focused and existing storage tests** +- [x] **Step 8: Run focused and existing storage tests** ```bash cd app-instance/backend @@ -577,7 +577,7 @@ pytest tests/unit/test_plugin_skill_storage.py tests/unit/test_phase5_skills_run Expected: PASS. -- [ ] **Step 9: Commit** +- [x] **Step 9: Commit** ```bash git add app-instance/backend/beaver/plugins/transaction.py app-instance/backend/beaver/skills/specs app-instance/backend/tests/unit/test_plugin_skill_storage.py @@ -595,7 +595,7 @@ git commit -m "feat(skills): store immutable plugin upstream snapshots" - Modify: `app-instance/backend/beaver/skills/specs/storage.py` - Test: `app-instance/backend/tests/unit/test_plugin_skill_sync.py` -- [ ] **Step 1: Write failing initial mirror tests** +- [x] **Step 1: Write failing initial mirror tests** Cover: @@ -626,7 +626,7 @@ assert loaded.version.provenance["upstream_skill_content_hash"] assert loaded.version.provenance["upstream_skill_tree_hash"] ``` -- [ ] **Step 2: Run tests and verify failure** +- [x] **Step 2: Run tests and verify failure** ```bash cd app-instance/backend @@ -635,7 +635,7 @@ pytest tests/unit/test_plugin_skill_sync.py -q Expected: FAIL because `PluginManager` does not exist. -- [ ] **Step 3: Implement `PluginManager` constructor and discovery view** +- [x] **Step 3: Implement `PluginManager` constructor and discovery view** Constructor dependencies: @@ -659,7 +659,7 @@ class PluginManager: Keep all filesystem and lifecycle dependencies injectable for tests. -- [ ] **Step 4: Implement exact initial mirror publication** +- [x] **Step 4: Implement exact initial mirror publication** Acquire the workspace write lock before reading state, allocating versions, or writing candidates. For each declared skill: @@ -689,7 +689,7 @@ Use provenance: } ``` -- [ ] **Step 5: Promote the complete staged transaction** +- [x] **Step 5: Promote the complete staged transaction** After every declared skill passes validation: @@ -704,7 +704,7 @@ metadata write fails, those directories remain unreferenced and harmless; the pr current pointers remain authoritative. Add startup cleanup for staging directories older than 24 hours. -- [ ] **Step 6: Run focused and loader tests** +- [x] **Step 6: Run focused and loader tests** ```bash cd app-instance/backend @@ -713,7 +713,7 @@ pytest tests/unit/test_plugin_skill_sync.py tests/unit/test_phase5_skills_runtim Expected: PASS. -- [ ] **Step 7: Commit** +- [x] **Step 7: Commit** ```bash git add app-instance/backend/beaver/plugins app-instance/backend/beaver/skills/specs/storage.py app-instance/backend/tests/unit/test_plugin_skill_sync.py @@ -731,7 +731,7 @@ git commit -m "feat(plugins): mirror enabled plugin skills" - Test: `app-instance/backend/tests/unit/test_plugin_skill_sync.py` - Test: `app-instance/backend/tests/unit/test_skill_learning_candidate_state.py` -- [ ] **Step 1: Write failing upgrade classification tests** +- [x] **Step 1: Write failing upgrade classification tests** Create four tree-hash fixtures representing `B`, `L`, and `U`: @@ -758,7 +758,7 @@ Also test: - legacy candidate payloads still parse. - two processes syncing the same update append only one candidate record. -- [ ] **Step 2: Run tests and verify failure** +- [x] **Step 2: Run tests and verify failure** ```bash cd app-instance/backend @@ -767,7 +767,7 @@ pytest tests/unit/test_plugin_skill_sync.py tests/unit/test_skill_learning_candi Expected: FAIL because update classification and candidate kind are missing. -- [ ] **Step 3: Add `plugin_skill_update` candidate support** +- [x] **Step 3: Add `plugin_skill_update` candidate support** Do not add a special status. Existing candidate statuses remain sufficient. Ensure `SkillLearningCandidate.from_dict()` accepts the new `kind` without changing legacy @@ -789,7 +789,7 @@ Use evidence: Set `priority=10`, `confidence=1.0`, `trigger_reason="plugin_update"`. -- [ ] **Step 4: Implement update classification and candidate creation** +- [x] **Step 4: Implement update classification and candidate creation** Use canonical hashes and deterministic IDs: @@ -804,7 +804,7 @@ For `already_applied`, advance state without a candidate. For `fast_forward` and `three_way`, record an open candidate. If the same ID exists in any status, do not append another JSONL record. -- [ ] **Step 5: Make candidate mutation atomic under the shared lock** +- [x] **Step 5: Make candidate mutation atomic under the shared lock** Add an optional `WorkspaceWriteLock` to `SkillLearningStore`; EngineLoader supplies the shared workspace instance, while isolated unit-test construction falls back to a @@ -818,7 +818,7 @@ Inside one lock acquisition, read current candidates, check the deterministic ID atomically rewrite or append the JSONL record. Apply the same lock to candidate update and transition methods. Nested calls from `PluginManager` reuse the reentrant lock. -- [ ] **Step 6: Supersede stale pending updates** +- [x] **Step 6: Supersede stale pending updates** When a different pending candidate exists for the same plugin skill: @@ -831,7 +831,7 @@ learning_store.transition_learning_candidate( ) ``` -- [ ] **Step 7: Run focused tests** +- [x] **Step 7: Run focused tests** ```bash cd app-instance/backend @@ -840,7 +840,7 @@ pytest tests/unit/test_plugin_skill_sync.py tests/unit/test_skill_learning_candi Expected: PASS. -- [ ] **Step 8: Commit** +- [x] **Step 8: Commit** ```bash git add app-instance/backend/beaver/plugins/skills.py app-instance/backend/beaver/memory/skills/models.py app-instance/backend/beaver/memory/skills/store.py app-instance/backend/tests/unit/test_plugin_skill_sync.py app-instance/backend/tests/unit/test_skill_learning_candidate_state.py @@ -859,7 +859,7 @@ git commit -m "feat(plugins): enqueue skill upgrade candidates" - Test: `app-instance/backend/tests/unit/test_plugin_skill_learning.py` - Test: `app-instance/backend/tests/unit/test_skill_learning_pipeline.py` -- [ ] **Step 1: Write failing model and fast-forward tests** +- [x] **Step 1: Write failing model and fast-forward tests** Test backward-compatible draft parsing and exact upstream fast-forward: @@ -877,7 +877,7 @@ assert provider.calls == [] After publish, assert the new version contains the new upstream supporting files even when `SKILL.md` did not change. -- [ ] **Step 2: Run tests and verify failure** +- [x] **Step 2: Run tests and verify failure** ```bash cd app-instance/backend @@ -887,7 +887,7 @@ pytest tests/unit/test_plugin_skill_learning.py tests/unit/test_skill_learning_p Expected: FAIL because drafts have no provenance and the learning service has no plugin update branch. -- [ ] **Step 3: Add backward-compatible draft provenance** +- [x] **Step 3: Add backward-compatible draft provenance** Extend `SkillDraft`: @@ -897,7 +897,7 @@ provenance: dict[str, Any] = field(default_factory=dict) Include it in `to_dict()` and parse missing values as `{}` in `from_dict()`. -- [ ] **Step 4: Add a focused draft constructor** +- [x] **Step 4: Add a focused draft constructor** Add: @@ -918,7 +918,7 @@ def create_plugin_update_draft( It writes `proposal_kind="plugin_skill_update"`. -- [ ] **Step 5: Implement fast-forward synthesis** +- [x] **Step 5: Implement fast-forward synthesis** In `SkillLearningService.synthesize_draft()`, branch before ordinary revision: @@ -930,7 +930,7 @@ if candidate.kind == "plugin_skill_update": For `merge_mode == "fast_forward"`, load `U` from `SkillSpecStore`, parse its frontmatter/body, and create a draft exactly equal to `U`. Do not call the provider. -- [ ] **Step 6: Serialize all skill publication** +- [x] **Step 6: Serialize all skill publication** Add an optional `WorkspaceWriteLock` to `SkillPublisher`; EngineLoader supplies the shared workspace instance and isolated tests use a publisher-local fallback. Hold it across @@ -938,14 +938,14 @@ workspace instance and isolated tests use a publisher-local fallback. Hold it ac and disable. This protects ordinary learned skills as well as plugin-origin skills from racing with boot or explicit plugin sync. -- [ ] **Step 7: Materialize referenced supporting files during publish** +- [x] **Step 7: Materialize referenced supporting files during publish** For `proposal_kind="plugin_skill_update"`, resolve the snapshot and supporting-file plan from draft provenance. Stage the complete next version directory, including `SKILL.md` and supporting files, before promoting it. Reject missing snapshots, path conflicts, or tree-hash mismatches. Ordinary skill publication keeps its current behavior. -- [ ] **Step 8: Preserve draft provenance on publish** +- [x] **Step 8: Preserve draft provenance on publish** Change `SkillPublisher.publish()` provenance construction to: @@ -959,7 +959,7 @@ provenance={ } ``` -- [ ] **Step 9: Run focused tests** +- [x] **Step 9: Run focused tests** ```bash cd app-instance/backend @@ -968,7 +968,7 @@ pytest tests/unit/test_plugin_skill_learning.py tests/unit/test_skill_learning_p Expected: PASS. -- [ ] **Step 10: Commit** +- [x] **Step 10: Commit** ```bash git add app-instance/backend/beaver/skills app-instance/backend/tests/unit/test_plugin_skill_learning.py app-instance/backend/tests/unit/test_skill_learning_pipeline.py @@ -986,7 +986,7 @@ git commit -m "feat(skill-learning): create plugin update drafts" - Test: `app-instance/backend/tests/unit/test_plugin_skill_learning.py` - Test: `app-instance/backend/tests/unit/test_skill_learning_synthesizer_preservation.py` -- [ ] **Step 1: Write failing three-way prompt and parse tests** +- [x] **Step 1: Write failing three-way prompt and parse tests** Assert the prompt contains labeled `OLD UPSTREAM`, `CURRENT LOCAL`, and `NEW UPSTREAM` sections and does not confuse the current local version with the merge base. @@ -1019,7 +1019,7 @@ def test_supporting_file_merge_blocks_divergent_edits() -> None: assert plan.conflicts[0].path == "a.txt" ``` -- [ ] **Step 2: Run tests and verify failure** +- [x] **Step 2: Run tests and verify failure** ```bash cd app-instance/backend @@ -1028,7 +1028,7 @@ pytest tests/unit/test_plugin_skill_learning.py tests/unit/test_skill_learning_s Expected: FAIL because three-way synthesis does not exist. -- [ ] **Step 3: Add `synthesize_plugin_update()`** +- [x] **Step 3: Add `synthesize_plugin_update()`** Signature: @@ -1054,7 +1054,7 @@ The system message must require JSON only and state: - list every intentional drop; - leave `resolved_conflicts` empty only when no semantic conflict exists. -- [ ] **Step 4: Load all three snapshots in the learning service** +- [x] **Step 4: Load all three snapshots in the learning service** Resolve: @@ -1065,7 +1065,7 @@ Resolve: Raise a specific `ValueError` when any referenced snapshot/version is missing. Do not fallback to a two-way merge. -- [ ] **Step 5: Build the deterministic supporting-file merge plan** +- [x] **Step 5: Build the deterministic supporting-file merge plan** Compare files by path and content/executable digest: @@ -1078,7 +1078,7 @@ Compare files by path and content/executable digest: Exclude `SKILL.md` because the synthesizer handles it. Store selected source references and conflict records in draft provenance; do not duplicate file bytes in JSON. -- [ ] **Step 6: Create the plugin update draft** +- [x] **Step 6: Create the plugin update draft** Store merge decisions in draft provenance: @@ -1097,7 +1097,7 @@ Store merge decisions in draft provenance: If the supporting-file plan contains conflicts, the draft may be inspected but cannot be published. V1 does not ask the LLM to merge arbitrary or binary files. -- [ ] **Step 7: Run focused tests** +- [x] **Step 7: Run focused tests** ```bash cd app-instance/backend @@ -1106,7 +1106,7 @@ pytest tests/unit/test_plugin_skill_learning.py tests/unit/test_skill_learning_s Expected: PASS. -- [ ] **Step 8: Commit** +- [x] **Step 8: Commit** ```bash git add app-instance/backend/beaver/plugins/tree_merge.py app-instance/backend/beaver/skills/learning app-instance/backend/tests/unit/test_plugin_skill_learning.py app-instance/backend/tests/unit/test_skill_learning_synthesizer_preservation.py @@ -1125,7 +1125,7 @@ git commit -m "feat(skill-learning): synthesize three-way plugin updates" - Test: `app-instance/backend/tests/unit/test_skill_learning_eval.py` - Test: `app-instance/backend/tests/unit/test_skill_learning_pipeline.py` -- [ ] **Step 1: Write failing plugin merge preservation tests** +- [x] **Step 1: Write failing plugin merge preservation tests** Cover: @@ -1148,7 +1148,7 @@ assert report.preservation_report == { } ``` -- [ ] **Step 2: Run tests and verify failure** +- [x] **Step 2: Run tests and verify failure** ```bash cd app-instance/backend @@ -1157,7 +1157,7 @@ pytest tests/unit/test_skill_learning_preservation.py tests/unit/test_skill_lear Expected: FAIL because preservation only checks one base skill. -- [ ] **Step 3: Add plugin merge preservation helper** +- [x] **Step 3: Add plugin merge preservation helper** Add: @@ -1174,13 +1174,13 @@ def check_plugin_merge_preservation( It calls existing `check_preservation()` for local and upstream content, gives Safety and Required Tools sections blocking weight, and reports unresolved conflicts separately. -- [ ] **Step 4: Use current local as replay baseline** +- [x] **Step 4: Use current local as replay baseline** When `draft.proposal_kind == "plugin_skill_update"`, load `draft.base_version` as the baseline skill. Continue to run the candidate arm with the draft context. Do not use raw upstream `B` or `U` as the replay baseline. -- [ ] **Step 5: Tighten publish gate** +- [x] **Step 5: Tighten publish gate** Add: @@ -1197,7 +1197,7 @@ if draft.proposal_kind == "plugin_skill_update": The existing `passed is False` gate remains active. -- [ ] **Step 6: Run focused tests** +- [x] **Step 6: Run focused tests** ```bash cd app-instance/backend @@ -1206,7 +1206,7 @@ pytest tests/unit/test_skill_learning_preservation.py tests/unit/test_skill_lear Expected: PASS. -- [ ] **Step 7: Commit** +- [x] **Step 7: Commit** ```bash git add app-instance/backend/beaver/skills/learning app-instance/backend/tests/unit/test_skill_learning_preservation.py app-instance/backend/tests/unit/test_skill_learning_eval.py app-instance/backend/tests/unit/test_skill_learning_pipeline.py @@ -1224,7 +1224,7 @@ git commit -m "feat(skill-learning): gate plugin merge preservation" - Test: `app-instance/backend/tests/unit/test_plugin_skill_sync.py` - Test: `app-instance/backend/tests/unit/test_skill_learning_pipeline.py` -- [ ] **Step 1: Write failing lifecycle tests** +- [x] **Step 1: Write failing lifecycle tests** Test: @@ -1242,7 +1242,7 @@ Test: active; - adopt changes `source_kind` to `managed`, removes binding, and keeps the skill active. -- [ ] **Step 2: Run tests and verify failure** +- [x] **Step 2: Run tests and verify failure** ```bash cd app-instance/backend @@ -1251,7 +1251,7 @@ pytest tests/unit/test_plugin_skill_sync.py tests/unit/test_skill_learning_pipel Expected: FAIL because publication has no plugin acknowledgement callback. -- [ ] **Step 3: Add a narrow publication observer** +- [x] **Step 3: Add a narrow publication observer** Extend pipeline construction with: @@ -1265,7 +1265,7 @@ or turn the publish API response into a failure. Mark the learning candidate pub before invoking the best-effort observer so clients do not retry a successful publish. The next sync is responsible for reconciliation. -- [ ] **Step 4: Implement `PluginManager.on_skill_published()`** +- [x] **Step 4: Implement `PluginManager.on_skill_published()`** For `proposal_kind="plugin_skill_update"`: @@ -1277,7 +1277,7 @@ For `proposal_kind="plugin_skill_update"`: 6. clear `pending_candidate_id`; 7. set status `synced`. -- [ ] **Step 5: Implement sync-time reconciliation** +- [x] **Step 5: Implement sync-time reconciliation** At the beginning of `sync_enabled()`, inspect each linked skill's current published version. When provenance contains: @@ -1294,7 +1294,7 @@ and the referenced upstream snapshot exists, advance state only if the current v number is newer than `accepted_beaver_version`. Clear only the matching pending candidate. Never regress state when the runtime current pointer was rolled back to an older version. -- [ ] **Step 6: Implement pause, resume, disable, missing, and adopt** +- [x] **Step 6: Implement pause, resume, disable, missing, and adopt** `pause(plugin_id)` sets `updates_paused=True` and leaves linked skills unchanged. `resume(plugin_id)` clears the flag and performs reconciliation/sync. @@ -1313,7 +1313,7 @@ When discovery cannot find a previously known plugin, set status `missing`, pres `enabled` and `updates_paused`, skip update generation, and do not disable any linked skill. -- [ ] **Step 7: Run focused tests** +- [x] **Step 7: Run focused tests** ```bash cd app-instance/backend @@ -1322,7 +1322,7 @@ pytest tests/unit/test_plugin_skill_sync.py tests/unit/test_skill_learning_pipel Expected: PASS. -- [ ] **Step 8: Commit** +- [x] **Step 8: Commit** ```bash git add app-instance/backend/beaver/plugins/skills.py app-instance/backend/beaver/skills/learning/pipeline.py app-instance/backend/beaver/skills/publisher/service.py app-instance/backend/tests/unit/test_plugin_skill_sync.py app-instance/backend/tests/unit/test_skill_learning_pipeline.py @@ -1339,7 +1339,7 @@ git commit -m "feat(plugins): track published updates and ownership" - Test: `app-instance/backend/tests/unit/test_plugin_runtime.py` - Test: `app-instance/backend/tests/unit/test_phase5_skills_runtime.py` -- [ ] **Step 1: Write failing runtime assembly tests** +- [x] **Step 1: Write failing runtime assembly tests** Test: @@ -1352,7 +1352,7 @@ Test: workspace lock; - `EngineLoadResult.plugin_manager` and plugin summaries are available. -- [ ] **Step 2: Run tests and verify failure** +- [x] **Step 2: Run tests and verify failure** ```bash cd app-instance/backend @@ -1361,7 +1361,7 @@ pytest tests/unit/test_plugin_runtime.py tests/unit/test_phase5_skills_runtime.p Expected: FAIL because `EngineLoader` does not assemble plugin services. -- [ ] **Step 3: Extend `EngineLoadResult` and loader injection** +- [x] **Step 3: Extend `EngineLoadResult` and loader injection** Add: @@ -1372,7 +1372,7 @@ plugins: list[dict] = field(default_factory=list) Allow `plugin_manager` injection in `EngineLoader.__init__()` for tests. -- [ ] **Step 4: Assemble in dependency order** +- [x] **Step 4: Assemble in dependency order** Required order: @@ -1390,7 +1390,7 @@ Do not use `SkillsLoader.extra_dirs` for plugin skills. Explicit API enable/sync bounded blocking lock timeout; Engine boot uses a non-blocking attempt and proceeds with the current published skill set if another writer owns the lock. -- [ ] **Step 5: Run runtime tests** +- [x] **Step 5: Run runtime tests** ```bash cd app-instance/backend @@ -1399,7 +1399,7 @@ pytest tests/unit/test_plugin_runtime.py tests/unit/test_phase5_skills_runtime.p Expected: PASS. -- [ ] **Step 6: Commit** +- [x] **Step 6: Commit** ```bash git add app-instance/backend/beaver/engine/loader.py app-instance/backend/beaver/plugins app-instance/backend/tests/unit/test_plugin_runtime.py app-instance/backend/tests/unit/test_phase5_skills_runtime.py @@ -1414,7 +1414,7 @@ git commit -m "feat(runtime): sync declarative plugins at boot" - Modify: `app-instance/backend/beaver/interfaces/web/app.py` - Test: `app-instance/backend/tests/unit/test_plugin_web_api.py` -- [ ] **Step 1: Write failing API tests** +- [x] **Step 1: Write failing API tests** Cover: @@ -1433,7 +1433,7 @@ manifest/sync errors. Assert lock timeout maps to `409 plugin_write_busy`. Asser payload contains the real absolute workspace or external search-root path. Assert disable without `{"disable_linked_skills": true}` is rejected. -- [ ] **Step 2: Run tests and verify failure** +- [x] **Step 2: Run tests and verify failure** ```bash cd app-instance/backend @@ -1442,7 +1442,7 @@ pytest tests/unit/test_plugin_web_api.py -q Expected: FAIL with missing routes. -- [ ] **Step 3: Add normalized plugin payload helper** +- [x] **Step 3: Add normalized plugin payload helper** Return: @@ -1473,12 +1473,12 @@ Return: Never return arbitrary plugin file content, secrets, or absolute server paths. -- [ ] **Step 4: Implement routes** +- [x] **Step 4: Implement routes** Each mutating endpoint boots one runtime, invokes its `plugin_manager`, and returns the updated plugin payload. Map `ValueError` messages to stable HTTP status codes. -- [ ] **Step 5: Run focused and existing web tests** +- [x] **Step 5: Run focused and existing web tests** ```bash cd app-instance/backend @@ -1487,7 +1487,7 @@ pytest tests/unit/test_plugin_web_api.py tests/unit/test_skill_learning_web_api. Expected: PASS. -- [ ] **Step 6: Commit** +- [x] **Step 6: Commit** ```bash git add app-instance/backend/beaver/interfaces/web/app.py app-instance/backend/tests/unit/test_plugin_web_api.py @@ -1504,12 +1504,12 @@ git commit -m "feat(api): manage declarative plugins" - Modify: `app-instance/frontend/app/(app)/skills/page.tsx` - Test: `app-instance/frontend/lib/plugin-api.test.ts` -- [ ] **Step 1: Write failing API client tests** +- [x] **Step 1: Write failing API client tests** Test URL, method, and response typing for list, sync, enable, pause, resume, disable, and adopt. -- [ ] **Step 2: Run frontend test and verify failure** +- [x] **Step 2: Run frontend test and verify failure** Run the repository's existing frontend test command targeting: @@ -1520,7 +1520,7 @@ npx vitest run lib/plugin-api.test.ts Expected: FAIL because plugin API functions do not exist. -- [ ] **Step 3: Add frontend types** +- [x] **Step 3: Add frontend types** Add: @@ -1549,7 +1549,7 @@ export interface BeaverPlugin { } ``` -- [ ] **Step 4: Add API functions** +- [x] **Step 4: Add API functions** Implement: @@ -1563,7 +1563,7 @@ disablePlugin(pluginId, { disable_linked_skills: true }) adoptPluginSkill(pluginId, skillName) ``` -- [ ] **Step 5: Add a `plugins` Skills tab** +- [x] **Step 5: Add a `plugins` Skills tab** Extend `SkillsTab` and render a compact table with: @@ -1578,7 +1578,7 @@ Extend `SkillsTab` and render a compact table with: Do not add a separate marketing-style page or nested cards. -- [ ] **Step 6: Label plugin-origin skills and update candidates** +- [x] **Step 6: Label plugin-origin skills and update candidates** In existing Published/Candidates/Drafts views: @@ -1586,7 +1586,7 @@ In existing Published/Candidates/Drafts views: - render `plugin_skill_update` as `插件升级合并 / Plugin update merge`; - show `fast_forward` or `three_way` from candidate evidence/provenance. -- [ ] **Step 7: Run frontend tests and type checks** +- [x] **Step 7: Run frontend tests and type checks** ```bash cd app-instance/frontend @@ -1597,7 +1597,7 @@ npx tsc --noEmit Expected: PASS. -- [ ] **Step 8: Commit** +- [x] **Step 8: Commit** ```bash git add app-instance/frontend/types/index.ts app-instance/frontend/lib/api.ts app-instance/frontend/lib/plugin-api.test.ts 'app-instance/frontend/app/(app)/skills/page.tsx' @@ -1613,7 +1613,7 @@ git commit -m "feat(skills-ui): manage plugin skill mirrors" - Create: `docs/plugins/skill-plugins.md` - Modify: `docs/product-discovery/beaver/README.md` -- [ ] **Step 1: Write the end-to-end lifecycle test** +- [x] **Step 1: Write the end-to-end lifecycle test** The test must: @@ -1634,7 +1634,7 @@ The test must: remains active; 15. run two sync processes and assert no duplicate version or candidate is created. -- [ ] **Step 2: Run the integration test and fix only lifecycle defects** +- [x] **Step 2: Run the integration test and fix only lifecycle defects** ```bash cd app-instance/backend @@ -1643,7 +1643,7 @@ pytest tests/integration/test_plugin_skill_lifecycle.py -v Expected: PASS. -- [ ] **Step 3: Write operator documentation** +- [x] **Step 3: Write operator documentation** Document: @@ -1658,7 +1658,7 @@ Document: - workspace locking, deferred boot sync, and publication reconciliation; - why plugin Python code is not executed in V1. -- [ ] **Step 4: Run the complete relevant backend suite** +- [x] **Step 4: Run the complete relevant backend suite** ```bash cd app-instance/backend @@ -1683,7 +1683,7 @@ pytest \ Expected: PASS. -- [ ] **Step 5: Run frontend verification** +- [x] **Step 5: Run frontend verification** ```bash cd app-instance/frontend @@ -1694,7 +1694,7 @@ npx tsc --noEmit Expected: PASS. -- [ ] **Step 6: Run a dirty-worktree-safe diff review** +- [x] **Step 6: Run a dirty-worktree-safe diff review** ```bash git status --short @@ -1708,7 +1708,7 @@ Expected: - only plugin/skill lifecycle files and planned docs/tests are included in this feature; - unrelated pre-existing user changes remain untouched. -- [ ] **Step 7: Commit** +- [x] **Step 7: Commit** ```bash git add app-instance/backend/tests/integration/test_plugin_skill_lifecycle.py docs/plugins/skill-plugins.md docs/product-discovery/beaver/README.md