feat(skill-learning): merge plugin skill updates

This commit is contained in:
2026-06-16 11:55:55 +08:00
parent c9e6c37b5c
commit a34b1219bc
15 changed files with 860 additions and 5 deletions

View File

@ -35,6 +35,7 @@ class SkillLearningPipelineService:
publisher: SkillPublisher,
safety_checker: SkillDraftSafetyChecker | None = None,
evaluator: SkillDraftEvaluator | None = None,
publish_observer: Callable[[SkillDraft, SkillVersion | SkillSpec], None] | None = None,
) -> None:
self.learning_store = learning_store
self.learning_service = learning_service
@ -43,6 +44,7 @@ class SkillLearningPipelineService:
self.publisher = publisher
self.safety_checker = safety_checker or SkillDraftSafetyChecker()
self.evaluator = evaluator
self.publish_observer = publish_observer
def list_candidates(self, status: str | None = None) -> list[SkillLearningCandidate]:
return self.learning_store.list_learning_candidates(status=status)
@ -238,6 +240,16 @@ class SkillLearningPipelineService:
else:
result = self.publisher.publish(skill_name, draft_id, publisher=publisher, notes=notes)
self._mark_candidate_by_draft(skill_name, draft_id, "published", "published")
if self.publish_observer is not None:
try:
self.publish_observer(draft, result)
except Exception as exc: # noqa: BLE001 - observer is best effort after successful publish.
candidate = self._candidate_by_draft(skill_name, draft_id)
self.learning_store.append_audit_event(
candidate.candidate_id if candidate is not None else f"draft:{draft_id}",
"plugin_publish_ack_failed",
{"error": str(exc), "skill_name": skill_name, "draft_id": draft_id},
)
return result
def rollback(
@ -391,6 +403,14 @@ class SkillLearningPipelineService:
preservation = eval_report.preservation_report or {}
if preservation.get("passed") is False:
raise ValueError("Draft preservation check did not pass")
if draft.proposal_kind == "plugin_skill_update":
if draft.provenance.get("merge_mode") == "three_way" and preservation.get("mode") != "plugin_three_way":
raise ValueError("Plugin update requires a three-way preservation report")
if preservation.get("unresolved_conflicts"):
raise ValueError("Plugin update has unresolved merge conflicts")
supporting_plan = draft.provenance.get("supporting_file_plan")
if isinstance(supporting_plan, dict) and supporting_plan.get("conflicts"):
raise ValueError("Plugin update has unresolved supporting-file conflicts")
def _mark_candidate_by_draft(
self,