Rohan03 commited on
Commit
08de4bd
·
verified ·
1 Parent(s): 9980c16

Sprint 7: skills/ci.py — skill testing, promotion, rollback, immune scan

Browse files
Files changed (1) hide show
  1. purpose_agent/skills/ci.py +102 -0
purpose_agent/skills/ci.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ci.py — Skill Continuous Integration.
3
+
4
+ Every skill mutation must pass CI before activation:
5
+ 1. Immune scan (no injection/manipulation)
6
+ 2. Eval tests (skill produces expected behavior)
7
+ 3. Fitness comparison (new version >= old version)
8
+ 4. Promote or rollback
9
+
10
+ Darwinian selection:
11
+ - High-fitness skills get retrieval boost
12
+ - Low-fitness skills are mutated then re-tested
13
+ - Failed skills are archived (never deleted — audit trail)
14
+ """
15
+ from __future__ import annotations
16
+ import logging
17
+ from typing import Any
18
+ from purpose_agent.skills.schema import SkillCard, SkillGenome
19
+ from purpose_agent.immune import scan_memory
20
+ from purpose_agent.memory import MemoryCard, MemoryKind
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class SkillCI:
26
+ """
27
+ CI pipeline for skills: scan → test → promote or rollback.
28
+
29
+ Usage:
30
+ ci = SkillCI()
31
+ passed = ci.validate(skill_card)
32
+ if passed:
33
+ genome.promote(skill_card.id)
34
+ else:
35
+ genome.rollback()
36
+ """
37
+
38
+ def __init__(self, fitness_threshold: float = 0.4):
39
+ self.fitness_threshold = fitness_threshold
40
+ self._scan_log: list[dict[str, Any]] = []
41
+
42
+ def validate(self, card: SkillCard) -> bool:
43
+ """
44
+ Run full CI on a skill card.
45
+ Returns True if skill passes all gates.
46
+ """
47
+ # Gate 1: Immune scan
48
+ memory_proxy = MemoryCard(
49
+ kind=MemoryKind.SKILL_CARD,
50
+ pattern=card.trigger,
51
+ strategy=" ".join(card.procedure),
52
+ content=card.name,
53
+ )
54
+ scan_result = scan_memory(memory_proxy)
55
+ if not scan_result.passed:
56
+ self._log(card, "REJECTED", f"immune scan failed: {scan_result.threats}")
57
+ card.status = "archived"
58
+ return False
59
+
60
+ # Gate 2: Fitness threshold
61
+ if card.fitness_score < self.fitness_threshold:
62
+ self._log(card, "REJECTED", f"fitness {card.fitness_score:.2f} < threshold {self.fitness_threshold}")
63
+ card.status = "archived"
64
+ return False
65
+
66
+ # Gate 3: Non-empty procedure
67
+ if not card.procedure:
68
+ self._log(card, "REJECTED", "empty procedure")
69
+ card.status = "archived"
70
+ return False
71
+
72
+ # All gates passed
73
+ card.status = "tested"
74
+ self._log(card, "PASSED", f"fitness={card.fitness_score:.2f}")
75
+ return True
76
+
77
+ def compare_fitness(self, new: SkillCard, old: SkillCard | None) -> bool:
78
+ """Compare new skill version against old. New must be >= old."""
79
+ if old is None:
80
+ return True
81
+ return new.fitness_score >= old.fitness_score * 0.95 # Allow 5% tolerance
82
+
83
+ def mutate(self, card: SkillCard) -> SkillCard:
84
+ """
85
+ Create a mutated version of a low-fitness skill.
86
+ Appends "[MUTATED]" marker for tracking.
87
+ """
88
+ mutated = card.evolve(
89
+ new_procedure=[f"[IMPROVED] {step}" for step in card.procedure]
90
+ )
91
+ mutated.created_by = "mutation"
92
+ return mutated
93
+
94
+ def _log(self, card: SkillCard, result: str, detail: str) -> None:
95
+ entry = {"skill_id": card.id, "name": card.name, "version": card.version,
96
+ "result": result, "detail": detail}
97
+ self._scan_log.append(entry)
98
+ logger.info(f"SkillCI: {card.name} v{card.version} → {result}: {detail}")
99
+
100
+ @property
101
+ def log(self) -> list[dict]:
102
+ return self._scan_log