docs(plugins): mark skill mirroring plan complete

This commit is contained in:
2026-06-16 12:24:47 +08:00
parent a65e59fcb6
commit f07ce019fe

View File

@ -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 `<workspace>/.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
`<external>/<plugin-dir>/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