skillpool 4.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skillpool/__init__.py +74 -0
- skillpool/__main__.py +6 -0
- skillpool/adapters/__init__.py +8 -0
- skillpool/adapters/base.py +41 -0
- skillpool/adapters/claude_adapter.py +36 -0
- skillpool/adapters/codex_adapter.py +92 -0
- skillpool/adapters/hermes_adapter.py +38 -0
- skillpool/audit/__init__.py +651 -0
- skillpool/bridge/__init__.py +16 -0
- skillpool/bridge/freeze_detector.py +134 -0
- skillpool/bridge/maintenance.py +119 -0
- skillpool/bridge/wal_manager.py +136 -0
- skillpool/clawmem_client.py +176 -0
- skillpool/cli.py +700 -0
- skillpool/combiner/__init__.py +31 -0
- skillpool/combiner/lifecycle.py +453 -0
- skillpool/combiner/models.py +99 -0
- skillpool/config.py +34 -0
- skillpool/cost/__init__.py +111 -0
- skillpool/cost/audit_hash.py +51 -0
- skillpool/cost/budget_tracker.py +66 -0
- skillpool/cost/dashboard.py +189 -0
- skillpool/cost/models.py +129 -0
- skillpool/cost/token_governor.py +264 -0
- skillpool/cost/trace_ceiling.py +38 -0
- skillpool/csdf.py +126 -0
- skillpool/evolver/__init__.py +978 -0
- skillpool/gain/__init__.py +285 -0
- skillpool/gate.py +282 -0
- skillpool/gate_policy/__init__.py +31 -0
- skillpool/gate_policy/incremental.py +157 -0
- skillpool/gate_policy/parser.py +258 -0
- skillpool/gate_policy/state_machine.py +432 -0
- skillpool/graph/__init__.py +14 -0
- skillpool/graph/ppr.py +279 -0
- skillpool/health/__init__.py +73 -0
- skillpool/health/check.py +85 -0
- skillpool/health/degradation.py +90 -0
- skillpool/health/models.py +43 -0
- skillpool/hooks/__init__.py +4 -0
- skillpool/hooks/security_scanner.py +288 -0
- skillpool/lifecycle.py +150 -0
- skillpool/materializer/__init__.py +124 -0
- skillpool/materializer/budget_cropper.py +178 -0
- skillpool/materializer/csdf_loader.py +114 -0
- skillpool/materializer/lazy_loader.py +265 -0
- skillpool/materializer/lifecycle_filter.py +93 -0
- skillpool/materializer/mapper.py +178 -0
- skillpool/materializer/models.py +66 -0
- skillpool/mcp_server.py +2005 -0
- skillpool/monitor/__init__.py +576 -0
- skillpool/monitor/bug_collector.py +392 -0
- skillpool/monitor/defect_classifier.py +218 -0
- skillpool/monitor/self_healing.py +530 -0
- skillpool/monitor/telemetry_bridge.py +197 -0
- skillpool/paradigm/__init__.py +312 -0
- skillpool/paradigm/override.py +285 -0
- skillpool/profile.py +94 -0
- skillpool/quality.py +254 -0
- skillpool/registry/__init__.py +509 -0
- skillpool/registry/models.py +98 -0
- skillpool/resolver/__init__.py +320 -0
- skillpool/resolver/cache.py +103 -0
- skillpool/resolver/circuit_breaker.py +103 -0
- skillpool/resolver/conflict_detector.py +111 -0
- skillpool/resolver/health_filter.py +38 -0
- skillpool/resolver/models.py +154 -0
- skillpool/resolver/rate_limiter.py +48 -0
- skillpool/resolver/skill_graph.py +183 -0
- skillpool/review/__init__.py +242 -0
- skillpool/review/async_queue.py +96 -0
- skillpool/review/checkpoint_runner.py +345 -0
- skillpool/review/models.py +164 -0
- skillpool/review/suspect_marker.py +39 -0
- skillpool/review/veto_evaluator.py +94 -0
- skillpool/router/__init__.py +481 -0
- skillpool/schemas.py +119 -0
- skillpool/synergy/__init__.py +240 -0
- skillpool/synergy/detector.py +5 -0
- skillpool/telemetry.py +126 -0
- skillpool/utils/__init__.py +21 -0
- skillpool/utils/changelog.py +218 -0
- skillpool/utils/logger.py +273 -0
- skillpool/utils/runtime_audit.py +163 -0
- skillpool/utils/time_utils.py +13 -0
- skillpool-4.3.0.dist-info/METADATA +21 -0
- skillpool-4.3.0.dist-info/RECORD +90 -0
- skillpool-4.3.0.dist-info/WHEEL +5 -0
- skillpool-4.3.0.dist-info/entry_points.txt +3 -0
- skillpool-4.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
"""SelfHealingLoop — BugCollector → Evolver → Skill upgrade → BDD verify → auto-rollback.
|
|
2
|
+
|
|
3
|
+
Connects the BugCollector's defect data to the Evolver's evolution pipeline,
|
|
4
|
+
adding BDD verification and automatic rollback on failure.
|
|
5
|
+
|
|
6
|
+
Trigger logic (from CLAUDE.md Section 8.5):
|
|
7
|
+
- >=3 P2 bugs of same defect_type in same skill -> PATCH (auto)
|
|
8
|
+
- >=5 P2 or >=1 P1 -> MINOR (auto + notify)
|
|
9
|
+
- >=1 P0 -> MAJOR (must NOT auto-execute, return needs_human)
|
|
10
|
+
|
|
11
|
+
Part of SkillPool — independent infrastructure, shared by all agents.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"HealingAction",
|
|
18
|
+
"HealingProposal",
|
|
19
|
+
"HealingStatus",
|
|
20
|
+
"SelfHealingLoop",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from enum import StrEnum
|
|
25
|
+
import logging
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
import yaml
|
|
30
|
+
|
|
31
|
+
from skillpool.config import get_data_dir
|
|
32
|
+
from skillpool.evolver import (
|
|
33
|
+
EvolverLayer,
|
|
34
|
+
EvolutionProposal,
|
|
35
|
+
VerificationStatus,
|
|
36
|
+
)
|
|
37
|
+
from skillpool.monitor.bug_collector import BugCollector, BugSeverity, DefectType
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class HealingAction(StrEnum):
|
|
43
|
+
"""Action taken by the healing loop."""
|
|
44
|
+
|
|
45
|
+
PATCH = "PATCH"
|
|
46
|
+
MINOR = "MINOR"
|
|
47
|
+
MAJOR = "MAJOR"
|
|
48
|
+
NEEDS_HUMAN = "needs_human"
|
|
49
|
+
SKIPPED = "skipped"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class HealingStatus(StrEnum):
|
|
53
|
+
"""Status of a healing proposal execution."""
|
|
54
|
+
|
|
55
|
+
PROPOSED = "proposed"
|
|
56
|
+
EXECUTING = "executing"
|
|
57
|
+
VERIFIED = "verified"
|
|
58
|
+
ROLLED_BACK = "rolled_back"
|
|
59
|
+
NEEDS_HUMAN = "needs_human"
|
|
60
|
+
BLOCKED = "blocked"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class HealingProposal:
|
|
65
|
+
"""A healing proposal generated from bug pattern analysis."""
|
|
66
|
+
|
|
67
|
+
proposal_id: str
|
|
68
|
+
skill_id: str
|
|
69
|
+
defect_type: DefectType
|
|
70
|
+
upgrade_type: HealingAction
|
|
71
|
+
bug_count: int
|
|
72
|
+
bug_severity: BugSeverity
|
|
73
|
+
evolver_proposal: EvolutionProposal | None = None
|
|
74
|
+
status: HealingStatus = HealingStatus.PROPOSED
|
|
75
|
+
verification_result: dict[str, Any] = field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SelfHealingLoop:
|
|
79
|
+
"""Self-healing feedback loop: BugCollector → Evolver → upgrade → verify → rollback.
|
|
80
|
+
|
|
81
|
+
Scans BugCollector for recurring defect patterns, groups by skill_id +
|
|
82
|
+
defect_type, proposes evolutions via Evolver, and verifies results with
|
|
83
|
+
BDD-style checks. Auto-rolls back on verification failure.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
bug_collector: BugCollector instance to scan for defects.
|
|
87
|
+
evolver: EvolverLayer instance to create/execute evolutions.
|
|
88
|
+
audit_layer: Optional AuditLayer for audit trail.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
bug_collector: BugCollector,
|
|
94
|
+
evolver: EvolverLayer,
|
|
95
|
+
audit_layer: Any | None = None,
|
|
96
|
+
skills_dir: Path | None = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
self._bug_collector = bug_collector
|
|
99
|
+
self._evolver = evolver
|
|
100
|
+
self._audit = audit_layer
|
|
101
|
+
self._skills_dir = skills_dir or get_data_dir() / "skills"
|
|
102
|
+
self._proposals: dict[str, HealingProposal] = {}
|
|
103
|
+
self._proposal_counter: int = 0
|
|
104
|
+
# Pre-evolution YAML snapshots for disk-level rollback
|
|
105
|
+
self._yaml_snapshots: dict[str, str] = {}
|
|
106
|
+
|
|
107
|
+
def scan_and_propose(self) -> list[dict[str, Any]]:
|
|
108
|
+
"""Scan BugCollector for recurring defects and propose evolutions.
|
|
109
|
+
|
|
110
|
+
Groups bugs by (skill_id, defect_type), counts by severity, and
|
|
111
|
+
applies trigger thresholds to determine upgrade type.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List of dicts with proposal details (proposal_id, skill_id,
|
|
115
|
+
defect_type, upgrade_type, bug_count, status).
|
|
116
|
+
"""
|
|
117
|
+
all_bugs = self._bug_collector.get_bugs()
|
|
118
|
+
if not all_bugs:
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
# Group by (skill_id, defect_type) -> severity counts
|
|
122
|
+
groups: dict[tuple[str, str], dict[str, int]] = {}
|
|
123
|
+
for bug in all_bugs:
|
|
124
|
+
key = (bug.skill_id, bug.defect_type.value)
|
|
125
|
+
if key not in groups:
|
|
126
|
+
groups[key] = {"P0": 0, "P1": 0, "P2": 0}
|
|
127
|
+
groups[key][bug.severity.value] += 1
|
|
128
|
+
|
|
129
|
+
proposals: list[dict[str, Any]] = []
|
|
130
|
+
|
|
131
|
+
for (skill_id, defect_type_str), counts in groups.items():
|
|
132
|
+
action = self._determine_action(counts)
|
|
133
|
+
if action == HealingAction.SKIPPED:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# Dedup: skip if a PROPOSED proposal already exists for this key
|
|
137
|
+
existing = any(
|
|
138
|
+
p.skill_id == skill_id and p.defect_type.value == defect_type_str and p.status == HealingStatus.PROPOSED
|
|
139
|
+
for p in self._proposals.values()
|
|
140
|
+
)
|
|
141
|
+
if existing:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
defect_type = DefectType(defect_type_str)
|
|
145
|
+
total = counts["P0"] + counts["P1"] + counts["P2"]
|
|
146
|
+
dominant_severity = self._dominant_severity(counts)
|
|
147
|
+
|
|
148
|
+
self._proposal_counter += 1
|
|
149
|
+
proposal_id = f"heal-{self._proposal_counter}"
|
|
150
|
+
|
|
151
|
+
# Create evolver proposal
|
|
152
|
+
evolver_proposal = self._evolver.create_proposal(
|
|
153
|
+
context={
|
|
154
|
+
"skill_id": skill_id,
|
|
155
|
+
"defect_type": defect_type_str,
|
|
156
|
+
"bug_counts": counts,
|
|
157
|
+
"trigger_source": "self_healing_loop",
|
|
158
|
+
},
|
|
159
|
+
risk="high" if action == HealingAction.MAJOR else "medium",
|
|
160
|
+
upgrade_type=action.value if action != HealingAction.NEEDS_HUMAN else "MAJOR",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
healing = HealingProposal(
|
|
164
|
+
proposal_id=proposal_id,
|
|
165
|
+
skill_id=skill_id,
|
|
166
|
+
defect_type=defect_type,
|
|
167
|
+
upgrade_type=action,
|
|
168
|
+
bug_count=total,
|
|
169
|
+
bug_severity=dominant_severity,
|
|
170
|
+
evolver_proposal=evolver_proposal,
|
|
171
|
+
status=HealingStatus.NEEDS_HUMAN if action == HealingAction.NEEDS_HUMAN else HealingStatus.PROPOSED,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
self._proposals[proposal_id] = healing
|
|
175
|
+
|
|
176
|
+
proposals.append(
|
|
177
|
+
{
|
|
178
|
+
"proposal_id": proposal_id,
|
|
179
|
+
"skill_id": skill_id,
|
|
180
|
+
"defect_type": defect_type_str,
|
|
181
|
+
"upgrade_type": action.value,
|
|
182
|
+
"bug_count": total,
|
|
183
|
+
"bug_counts": counts,
|
|
184
|
+
"status": healing.status.value,
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if self._audit:
|
|
189
|
+
self._audit.append(
|
|
190
|
+
action="self_healing_scan",
|
|
191
|
+
result=f"proposed_{len(proposals)}",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return proposals
|
|
195
|
+
|
|
196
|
+
def execute_healing(self, proposal_id: str) -> dict[str, Any]:
|
|
197
|
+
"""Execute a proposed healing evolution with BDD verification.
|
|
198
|
+
|
|
199
|
+
Steps:
|
|
200
|
+
1. Validate proposal exists and is in PROPOSED state
|
|
201
|
+
2. MAJOR proposals require human approval (return needs_human)
|
|
202
|
+
3. Mark as EXECUTING
|
|
203
|
+
4. Record pre-evolution bug snapshot
|
|
204
|
+
5. Save snapshot via Evolver for rollback support
|
|
205
|
+
6. Run BDD verification (check bug count decreased)
|
|
206
|
+
7. If BDD fails -> call Evolver.verify_evolution() to trigger rollback
|
|
207
|
+
8. If BDD passes -> call Evolver.verify_evolution() with passing results
|
|
208
|
+
9. Return execution result
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
proposal_id: The healing proposal ID to execute.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dict with execution result details.
|
|
215
|
+
"""
|
|
216
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
217
|
+
healing = self._proposals.get(proposal_id)
|
|
218
|
+
if healing is None:
|
|
219
|
+
return {
|
|
220
|
+
"proposal_id": proposal_id,
|
|
221
|
+
"status": "not_found",
|
|
222
|
+
"error": f"No healing proposal with id {proposal_id}",
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# MAJOR upgrades must NOT auto-execute
|
|
226
|
+
if healing.upgrade_type == HealingAction.NEEDS_HUMAN:
|
|
227
|
+
healing.status = HealingStatus.NEEDS_HUMAN
|
|
228
|
+
return {
|
|
229
|
+
"proposal_id": proposal_id,
|
|
230
|
+
"status": HealingStatus.NEEDS_HUMAN.value,
|
|
231
|
+
"reason": "MAJOR upgrade requires human approval",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if healing.status != HealingStatus.PROPOSED:
|
|
235
|
+
return {
|
|
236
|
+
"proposal_id": proposal_id,
|
|
237
|
+
"status": healing.status.value,
|
|
238
|
+
"reason": f"Proposal not in PROPOSED state (current: {healing.status.value})",
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Mark executing
|
|
242
|
+
healing.status = HealingStatus.EXECUTING
|
|
243
|
+
|
|
244
|
+
# Read and snapshot the CSDF YAML before evolution
|
|
245
|
+
yaml_path = self._find_skill_yaml(healing.skill_id)
|
|
246
|
+
if yaml_path and yaml_path.exists():
|
|
247
|
+
self._yaml_snapshots[proposal_id] = yaml_path.read_text(encoding="utf-8")
|
|
248
|
+
|
|
249
|
+
# Capture pre-evolution bug count for this skill+defect_type
|
|
250
|
+
bugs_before = self._bug_collector.get_bugs(
|
|
251
|
+
skill_id=healing.skill_id,
|
|
252
|
+
defect_type=healing.defect_type,
|
|
253
|
+
)
|
|
254
|
+
count_before = len(bugs_before)
|
|
255
|
+
|
|
256
|
+
# Save snapshot via evolver for rollback support
|
|
257
|
+
if healing.evolver_proposal:
|
|
258
|
+
self._evolver.save_snapshot(
|
|
259
|
+
healing.evolver_proposal.proposal_id,
|
|
260
|
+
{"bug_count_before": count_before, "skill_id": healing.skill_id},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# BDD verification: check that no new bugs of same type appeared
|
|
264
|
+
bdd_passed = self._bdd_verify(healing.skill_id, healing.defect_type, count_before)
|
|
265
|
+
|
|
266
|
+
if bdd_passed:
|
|
267
|
+
# Run evolver's verify_evolution with passing BDD results
|
|
268
|
+
if healing.evolver_proposal:
|
|
269
|
+
report = self._evolver.verify_evolution(
|
|
270
|
+
proposal_id=healing.evolver_proposal.proposal_id,
|
|
271
|
+
bdd_results={"bug_count_decreased": True},
|
|
272
|
+
scores_before={},
|
|
273
|
+
scores_after={},
|
|
274
|
+
)
|
|
275
|
+
if report.status == VerificationStatus.ROLLED_BACK:
|
|
276
|
+
healing.status = HealingStatus.ROLLED_BACK
|
|
277
|
+
healing.verification_result = {
|
|
278
|
+
"bdd_passed": False,
|
|
279
|
+
"evolver_rollback": True,
|
|
280
|
+
"veto_triggered": report.veto_triggered,
|
|
281
|
+
"veto_details": report.veto_details,
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
"proposal_id": proposal_id,
|
|
285
|
+
"status": HealingStatus.ROLLED_BACK.value,
|
|
286
|
+
"reason": "Evolver verification triggered rollback",
|
|
287
|
+
"verification": healing.verification_result,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
healing.status = HealingStatus.VERIFIED
|
|
291
|
+
healing.verification_result = {
|
|
292
|
+
"bdd_passed": True,
|
|
293
|
+
"bug_count_before": count_before,
|
|
294
|
+
"bug_count_after": len(
|
|
295
|
+
self._bug_collector.get_bugs(
|
|
296
|
+
skill_id=healing.skill_id,
|
|
297
|
+
defect_type=healing.defect_type,
|
|
298
|
+
)
|
|
299
|
+
),
|
|
300
|
+
}
|
|
301
|
+
# Persist evolution to CSDF YAML on disk
|
|
302
|
+
self._persist_evolution(healing.skill_id, healing)
|
|
303
|
+
else:
|
|
304
|
+
# BDD failed — trigger evolver rollback via verify_evolution
|
|
305
|
+
if healing.evolver_proposal:
|
|
306
|
+
self._evolver.verify_evolution(
|
|
307
|
+
proposal_id=healing.evolver_proposal.proposal_id,
|
|
308
|
+
bdd_results={"bug_count_decreased": False},
|
|
309
|
+
scores_before={},
|
|
310
|
+
scores_after={},
|
|
311
|
+
veto_details=["BDD verification failed: bug count did not decrease"],
|
|
312
|
+
)
|
|
313
|
+
# Restore from snapshot
|
|
314
|
+
snapshot = None
|
|
315
|
+
if healing.evolver_proposal:
|
|
316
|
+
snapshot = self._evolver.get_snapshot(healing.evolver_proposal.proposal_id)
|
|
317
|
+
|
|
318
|
+
# Restore original YAML from disk snapshot
|
|
319
|
+
yaml_restored = self._restore_yaml_snapshot(proposal_id, healing.skill_id)
|
|
320
|
+
|
|
321
|
+
healing.status = HealingStatus.ROLLED_BACK
|
|
322
|
+
healing.verification_result = {
|
|
323
|
+
"bdd_passed": False,
|
|
324
|
+
"bug_count_before": count_before,
|
|
325
|
+
"reason": "BDD verification failed: bug count did not decrease",
|
|
326
|
+
"snapshot_restored": snapshot is not None,
|
|
327
|
+
"yaml_restored": yaml_restored,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if self._audit:
|
|
331
|
+
self._audit.append(
|
|
332
|
+
action="self_healing_execute",
|
|
333
|
+
object_id=proposal_id,
|
|
334
|
+
result=healing.status.value,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
"proposal_id": proposal_id,
|
|
339
|
+
"status": healing.status.value,
|
|
340
|
+
"verification": healing.verification_result,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
def get_healing_status(self) -> dict[str, Any]:
|
|
344
|
+
"""Return current healing loop status.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Dict with total proposals, counts by status, and proposal summaries.
|
|
348
|
+
"""
|
|
349
|
+
by_status: dict[str, int] = {}
|
|
350
|
+
summaries: list[dict[str, Any]] = []
|
|
351
|
+
|
|
352
|
+
for healing in self._proposals.values():
|
|
353
|
+
status_key = healing.status.value
|
|
354
|
+
by_status[status_key] = by_status.get(status_key, 0) + 1
|
|
355
|
+
summaries.append(
|
|
356
|
+
{
|
|
357
|
+
"proposal_id": healing.proposal_id,
|
|
358
|
+
"skill_id": healing.skill_id,
|
|
359
|
+
"defect_type": healing.defect_type.value,
|
|
360
|
+
"upgrade_type": healing.upgrade_type.value,
|
|
361
|
+
"bug_count": healing.bug_count,
|
|
362
|
+
"status": healing.status.value,
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
"total_proposals": len(self._proposals),
|
|
368
|
+
"by_status": by_status,
|
|
369
|
+
"proposals": summaries,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
# ── Internal ──
|
|
373
|
+
|
|
374
|
+
@staticmethod
|
|
375
|
+
def _determine_action(counts: dict[str, int]) -> HealingAction:
|
|
376
|
+
"""Determine healing action from bug severity counts.
|
|
377
|
+
|
|
378
|
+
Thresholds (CLAUDE.md Section 8.5):
|
|
379
|
+
- >=1 P0 -> MAJOR (needs_human)
|
|
380
|
+
- >=1 P1 or >=5 P2 -> MINOR
|
|
381
|
+
- >=3 P2 -> PATCH
|
|
382
|
+
- else -> SKIPPED
|
|
383
|
+
"""
|
|
384
|
+
p0 = counts.get("P0", 0)
|
|
385
|
+
p1 = counts.get("P1", 0)
|
|
386
|
+
p2 = counts.get("P2", 0)
|
|
387
|
+
|
|
388
|
+
if p0 >= 1:
|
|
389
|
+
return HealingAction.NEEDS_HUMAN
|
|
390
|
+
if p1 >= 1 or p2 >= 5:
|
|
391
|
+
return HealingAction.MINOR
|
|
392
|
+
if p2 >= 3:
|
|
393
|
+
return HealingAction.PATCH
|
|
394
|
+
return HealingAction.SKIPPED
|
|
395
|
+
|
|
396
|
+
@staticmethod
|
|
397
|
+
def _dominant_severity(counts: dict[str, int]) -> BugSeverity:
|
|
398
|
+
"""Return the highest severity with non-zero count."""
|
|
399
|
+
if counts.get("P0", 0) > 0:
|
|
400
|
+
return BugSeverity.P0
|
|
401
|
+
if counts.get("P1", 0) > 0:
|
|
402
|
+
return BugSeverity.P1
|
|
403
|
+
return BugSeverity.P2
|
|
404
|
+
|
|
405
|
+
def _bdd_verify(
|
|
406
|
+
self,
|
|
407
|
+
skill_id: str,
|
|
408
|
+
defect_type: DefectType,
|
|
409
|
+
count_before: int,
|
|
410
|
+
) -> bool:
|
|
411
|
+
"""BDD-style verification: bug count check + optional review checkpoint.
|
|
412
|
+
|
|
413
|
+
1. Compares pre/post bug counts (tolerates ≤1 concurrent bug)
|
|
414
|
+
2. Optionally calls `skillpool review --checkpoint L3` for real regression
|
|
415
|
+
check when the environment supports it
|
|
416
|
+
"""
|
|
417
|
+
current_bugs = self._bug_collector.get_bugs(
|
|
418
|
+
skill_id=skill_id,
|
|
419
|
+
defect_type=defect_type,
|
|
420
|
+
)
|
|
421
|
+
count_after = len(current_bugs)
|
|
422
|
+
|
|
423
|
+
# If no new bugs appeared, verification passes
|
|
424
|
+
if count_after <= count_before:
|
|
425
|
+
return True
|
|
426
|
+
|
|
427
|
+
# New bugs appeared — check if they're genuinely new
|
|
428
|
+
# (not just the same bugs we counted before)
|
|
429
|
+
new_bug_count = count_after - count_before
|
|
430
|
+
# If more than 1 new bug of the same type appeared, pattern is worsening
|
|
431
|
+
if new_bug_count > 1:
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
# Exactly 1 new bug — tolerable as concurrent, but run
|
|
435
|
+
# checkpoint L3 if available for deeper verification
|
|
436
|
+
review_ok = self._run_review_checkpoint(skill_id)
|
|
437
|
+
return review_ok
|
|
438
|
+
|
|
439
|
+
def _run_review_checkpoint(self, skill_id: str) -> bool:
|
|
440
|
+
"""Run review checkpoint L3 for a skill. Returns True if review passes.
|
|
441
|
+
|
|
442
|
+
Falls back to True (pass) if review infrastructure is unavailable.
|
|
443
|
+
"""
|
|
444
|
+
try:
|
|
445
|
+
from skillpool.review import ReviewManager
|
|
446
|
+
from skillpool.review.models import (
|
|
447
|
+
CheckpointLevel,
|
|
448
|
+
ReviewTrigger,
|
|
449
|
+
ReviewTriggerRequest,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
rm = ReviewManager()
|
|
453
|
+
request = ReviewTriggerRequest(
|
|
454
|
+
trigger=ReviewTrigger.L3_REGRESSION_FAIL,
|
|
455
|
+
checkpoint=CheckpointLevel.L3,
|
|
456
|
+
affected_skills=[skill_id],
|
|
457
|
+
)
|
|
458
|
+
result = rm.trigger(request)
|
|
459
|
+
# If no veto triggered, review passes
|
|
460
|
+
return not result.veto_triggered
|
|
461
|
+
except Exception as e:
|
|
462
|
+
logger.warning("Review infrastructure unavailable for %s, defaulting pass: %s", skill_id, e)
|
|
463
|
+
# Review infrastructure unavailable — default pass
|
|
464
|
+
return True
|
|
465
|
+
|
|
466
|
+
def _find_skill_yaml(self, skill_id: str) -> Path | None:
|
|
467
|
+
"""Find the CSDF YAML file for a skill ID."""
|
|
468
|
+
if not self._skills_dir.exists():
|
|
469
|
+
return None
|
|
470
|
+
# Exact match
|
|
471
|
+
exact = self._skills_dir / f"{skill_id}.yaml"
|
|
472
|
+
if exact.exists():
|
|
473
|
+
return exact
|
|
474
|
+
# Prefix match
|
|
475
|
+
for p in self._skills_dir.iterdir():
|
|
476
|
+
if p.name.startswith(f"{skill_id}-") and p.suffix == ".yaml":
|
|
477
|
+
return p
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
def _persist_evolution(self, skill_id: str, healing: HealingProposal) -> bool:
|
|
481
|
+
"""Write evolution results back to the CSDF YAML file.
|
|
482
|
+
|
|
483
|
+
Updates the YAML with healing metadata (last_healed, upgrade_type,
|
|
484
|
+
defect_type) so the change is durable across restarts.
|
|
485
|
+
"""
|
|
486
|
+
yaml_path = self._find_skill_yaml(skill_id)
|
|
487
|
+
if yaml_path is None:
|
|
488
|
+
return False
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
|
|
492
|
+
except (yaml.YAMLError, OSError):
|
|
493
|
+
return False
|
|
494
|
+
|
|
495
|
+
if not isinstance(data, dict):
|
|
496
|
+
return False
|
|
497
|
+
|
|
498
|
+
# Apply healing metadata
|
|
499
|
+
from skillpool.utils.time_utils import utc_now
|
|
500
|
+
|
|
501
|
+
data["last_healed"] = utc_now().isoformat()
|
|
502
|
+
data["last_healing_type"] = healing.upgrade_type.value
|
|
503
|
+
data["last_healing_defect"] = healing.defect_type.value
|
|
504
|
+
|
|
505
|
+
# Write back
|
|
506
|
+
try:
|
|
507
|
+
yaml_path.write_text(
|
|
508
|
+
yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False),
|
|
509
|
+
encoding="utf-8",
|
|
510
|
+
)
|
|
511
|
+
return True
|
|
512
|
+
except OSError:
|
|
513
|
+
return False
|
|
514
|
+
|
|
515
|
+
def _restore_yaml_snapshot(self, proposal_id: str, skill_id: str) -> bool:
|
|
516
|
+
"""Restore original YAML from pre-evolution snapshot."""
|
|
517
|
+
original = self._yaml_snapshots.get(proposal_id)
|
|
518
|
+
if original is None:
|
|
519
|
+
return False
|
|
520
|
+
|
|
521
|
+
yaml_path = self._find_skill_yaml(skill_id)
|
|
522
|
+
if yaml_path is None:
|
|
523
|
+
return False
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
yaml_path.write_text(original, encoding="utf-8")
|
|
527
|
+
del self._yaml_snapshots[proposal_id]
|
|
528
|
+
return True
|
|
529
|
+
except OSError:
|
|
530
|
+
return False
|