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,285 @@
|
|
|
1
|
+
"""GainTracker — Four-dimension scoring and gain quantification for skill combinations.
|
|
2
|
+
|
|
3
|
+
Tracks skill execution outcomes across four dimensions:
|
|
4
|
+
1. Effectiveness — Task goal achievement degree
|
|
5
|
+
2. Efficiency — Resource consumption reasonableness
|
|
6
|
+
3. Quality — Output sustainability
|
|
7
|
+
4. Gain — Combination marginal contribution (A alone vs A+B)
|
|
8
|
+
|
|
9
|
+
Supports both implicit tracking (auto-collected from execution) and
|
|
10
|
+
explicit scoring (agent-provided ratings). Zero-burden by default.
|
|
11
|
+
|
|
12
|
+
Part of SkillPool — independent infrastructure, shared by all agents.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
|
+
|
|
24
|
+
from skillpool.config import get_data_dir
|
|
25
|
+
from skillpool.utils.time_utils import utc_now
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GainScores(BaseModel):
|
|
31
|
+
"""Four-dimension gain scores for a skill execution."""
|
|
32
|
+
|
|
33
|
+
effectiveness: float = Field(default=0.0, ge=0.0, le=10.0, description="Task goal achievement (0-10)")
|
|
34
|
+
efficiency: float = Field(default=0.0, ge=0.0, le=10.0, description="Resource consumption (0-10)")
|
|
35
|
+
quality: float = Field(default=0.0, ge=0.0, le=10.0, description="Output sustainability (0-10)")
|
|
36
|
+
gain: float = Field(default=0.0, ge=-10.0, le=10.0, description="Combination marginal contribution (-10 to +10)")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SkillExecution(BaseModel):
|
|
40
|
+
"""Record of a single skill execution with outcome data."""
|
|
41
|
+
|
|
42
|
+
execution_id: str = Field(default="", description="Unique execution ID")
|
|
43
|
+
skill_ids: list[str] = Field(description="Skill(s) used in this execution")
|
|
44
|
+
timestamp: str = Field(default="", description="ISO 8601 timestamp")
|
|
45
|
+
intent: str = Field(default="", description="Original agent intent")
|
|
46
|
+
scores: GainScores = Field(default_factory=GainScores)
|
|
47
|
+
duration_ms: int = Field(default=0, description="Execution duration in ms")
|
|
48
|
+
token_count: int = Field(default=0, description="Token consumption")
|
|
49
|
+
success: bool = Field(default=True, description="Whether execution succeeded")
|
|
50
|
+
source: str = Field(default="implicit", description="implicit or explicit scoring")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GainReport(BaseModel):
|
|
54
|
+
"""Aggregated gain report for a skill or combination."""
|
|
55
|
+
|
|
56
|
+
skill_id: str
|
|
57
|
+
execution_count: int = 0
|
|
58
|
+
avg_effectiveness: float = 0.0
|
|
59
|
+
avg_efficiency: float = 0.0
|
|
60
|
+
avg_quality: float = 0.0
|
|
61
|
+
avg_gain: float = 0.0
|
|
62
|
+
combined_score: float = 0.0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GainTracker:
|
|
66
|
+
"""Tracks and quantifies skill combination gains.
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
tracker = GainTracker(data_dir=Path("~/.skillpool/gain"))
|
|
70
|
+
tracker.record(SkillExecution(
|
|
71
|
+
skill_ids=["multi-dim-review", "karpathy-guidelines"],
|
|
72
|
+
intent="code review",
|
|
73
|
+
scores=GainScores(effectiveness=8.5, efficiency=7.0, quality=9.0, gain=1.5),
|
|
74
|
+
source="explicit",
|
|
75
|
+
))
|
|
76
|
+
report = tracker.report("multi-dim-review")
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, data_dir: Path | None = None):
|
|
80
|
+
self.data_dir = data_dir or get_data_dir() / "gain"
|
|
81
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
self._executions: list[SkillExecution] = []
|
|
83
|
+
self._loaded = False
|
|
84
|
+
|
|
85
|
+
def _ensure_loaded(self) -> None:
|
|
86
|
+
"""Lazy-load execution history from disk."""
|
|
87
|
+
if self._loaded:
|
|
88
|
+
return
|
|
89
|
+
log_file = self.data_dir / "executions.jsonl"
|
|
90
|
+
if log_file.exists():
|
|
91
|
+
for line in log_file.read_text().splitlines():
|
|
92
|
+
line = line.strip()
|
|
93
|
+
if not line:
|
|
94
|
+
continue
|
|
95
|
+
try:
|
|
96
|
+
self._executions.append(SkillExecution(**json.loads(line)))
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.warning("Failed to parse execution record: %s", e)
|
|
99
|
+
continue
|
|
100
|
+
self._loaded = True
|
|
101
|
+
|
|
102
|
+
def record(self, execution: SkillExecution) -> None:
|
|
103
|
+
"""Record a skill execution outcome.
|
|
104
|
+
|
|
105
|
+
Writes immediately to disk for persistence.
|
|
106
|
+
"""
|
|
107
|
+
self._ensure_loaded()
|
|
108
|
+
if not execution.timestamp:
|
|
109
|
+
execution.timestamp = utc_now().isoformat()
|
|
110
|
+
if not execution.execution_id:
|
|
111
|
+
execution.execution_id = f"exec-{len(self._executions) + 1:06d}"
|
|
112
|
+
|
|
113
|
+
self._executions.append(execution)
|
|
114
|
+
|
|
115
|
+
# Append to log file
|
|
116
|
+
log_file = self.data_dir / "executions.jsonl"
|
|
117
|
+
with open(log_file, "a") as f:
|
|
118
|
+
f.write(execution.model_dump_json() + "\n")
|
|
119
|
+
|
|
120
|
+
def record_implicit(
|
|
121
|
+
self,
|
|
122
|
+
skill_ids: list[str],
|
|
123
|
+
intent: str = "",
|
|
124
|
+
duration_ms: int = 0,
|
|
125
|
+
token_count: int = 0,
|
|
126
|
+
success: bool = True,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Record an implicit (auto-collected) execution.
|
|
129
|
+
|
|
130
|
+
Derives basic scores from available metrics:
|
|
131
|
+
- effectiveness: 7.0 if success, 3.0 if failure
|
|
132
|
+
- efficiency: based on duration (lower = better)
|
|
133
|
+
- quality: neutral 5.0 (no data to assess)
|
|
134
|
+
- gain: 0.0 (unknown for single execution)
|
|
135
|
+
"""
|
|
136
|
+
eff_score = 7.0 if success else 3.0
|
|
137
|
+
# Efficiency: 10 for <30s, 8 for <2min, 5 for <5min, 3 for longer
|
|
138
|
+
if duration_ms < 30_000:
|
|
139
|
+
eff_score_eff = 10.0
|
|
140
|
+
elif duration_ms < 120_000:
|
|
141
|
+
eff_score_eff = 8.0
|
|
142
|
+
elif duration_ms < 300_000:
|
|
143
|
+
eff_score_eff = 5.0
|
|
144
|
+
else:
|
|
145
|
+
eff_score_eff = 3.0
|
|
146
|
+
|
|
147
|
+
scores = GainScores(
|
|
148
|
+
effectiveness=eff_score,
|
|
149
|
+
efficiency=eff_score_eff,
|
|
150
|
+
quality=5.0, # No data to assess
|
|
151
|
+
gain=0.0, # Unknown for single execution
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
self.record(
|
|
155
|
+
SkillExecution(
|
|
156
|
+
skill_ids=skill_ids,
|
|
157
|
+
intent=intent,
|
|
158
|
+
scores=scores,
|
|
159
|
+
duration_ms=duration_ms,
|
|
160
|
+
token_count=token_count,
|
|
161
|
+
success=success,
|
|
162
|
+
source="implicit",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def report(self, skill_id: str, last_n: int = 50) -> GainReport:
|
|
167
|
+
"""Generate aggregated gain report for a skill.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
skill_id: Skill ID to report on
|
|
171
|
+
last_n: Consider only the last N executions
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
GainReport with aggregated scores
|
|
175
|
+
"""
|
|
176
|
+
self._ensure_loaded()
|
|
177
|
+
|
|
178
|
+
# Find executions involving this skill
|
|
179
|
+
relevant = [e for e in self._executions if skill_id in e.skill_ids]
|
|
180
|
+
relevant = relevant[-last_n:] # Last N only
|
|
181
|
+
|
|
182
|
+
if not relevant:
|
|
183
|
+
return GainReport(skill_id=skill_id)
|
|
184
|
+
|
|
185
|
+
n = len(relevant)
|
|
186
|
+
avg_eff = sum(e.scores.effectiveness for e in relevant) / n
|
|
187
|
+
avg_effi = sum(e.scores.efficiency for e in relevant) / n
|
|
188
|
+
avg_qual = sum(e.scores.quality for e in relevant) / n
|
|
189
|
+
avg_gain = sum(e.scores.gain for e in relevant) / n
|
|
190
|
+
|
|
191
|
+
# Combined score: weighted average
|
|
192
|
+
combined = (
|
|
193
|
+
avg_eff * 0.35 # Effectiveness 35%
|
|
194
|
+
+ avg_effi * 0.20 # Efficiency 20%
|
|
195
|
+
+ avg_qual * 0.25 # Quality 25%
|
|
196
|
+
+ max(0, avg_gain) * 0.20 # Gain 20% (only positive contributes)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return GainReport(
|
|
200
|
+
skill_id=skill_id,
|
|
201
|
+
execution_count=n,
|
|
202
|
+
avg_effectiveness=round(avg_eff, 2),
|
|
203
|
+
avg_efficiency=round(avg_effi, 2),
|
|
204
|
+
avg_quality=round(avg_qual, 2),
|
|
205
|
+
avg_gain=round(avg_gain, 2),
|
|
206
|
+
combined_score=round(combined, 2),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def combination_gain(self, skill_a: str, skill_b: str) -> float:
|
|
210
|
+
"""Calculate the marginal gain of using A+B vs A alone.
|
|
211
|
+
|
|
212
|
+
Compares executions where A+B were used together vs A alone.
|
|
213
|
+
Returns the effectiveness difference (positive = B helps A).
|
|
214
|
+
"""
|
|
215
|
+
self._ensure_loaded()
|
|
216
|
+
|
|
217
|
+
together = [e for e in self._executions if skill_a in e.skill_ids and skill_b in e.skill_ids]
|
|
218
|
+
alone = [e for e in self._executions if skill_a in e.skill_ids and skill_b not in e.skill_ids]
|
|
219
|
+
|
|
220
|
+
if not together or not alone:
|
|
221
|
+
return 0.0
|
|
222
|
+
|
|
223
|
+
avg_together = sum(e.scores.effectiveness for e in together) / len(together)
|
|
224
|
+
avg_alone = sum(e.scores.effectiveness for e in alone) / len(alone)
|
|
225
|
+
|
|
226
|
+
return round(avg_together - avg_alone, 2)
|
|
227
|
+
|
|
228
|
+
def get_top_combinations(self, intent: str = "", top_k: int = 5) -> list[dict]:
|
|
229
|
+
"""Get top-performing skill combinations based on historical gain data.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
intent: Optional intent text to filter by (keyword match on skill_ids)
|
|
233
|
+
top_k: Maximum results to return
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of dicts with skill_id, avg_gain, execution_count, last_execution
|
|
237
|
+
"""
|
|
238
|
+
self._ensure_loaded()
|
|
239
|
+
|
|
240
|
+
# Aggregate gain per skill_id
|
|
241
|
+
skill_stats: dict[str, dict] = {}
|
|
242
|
+
for ex in self._executions:
|
|
243
|
+
for sid in ex.skill_ids:
|
|
244
|
+
if sid not in skill_stats:
|
|
245
|
+
skill_stats[sid] = {"total_gain": 0.0, "count": 0, "last": ""}
|
|
246
|
+
skill_stats[sid]["total_gain"] += ex.scores.effectiveness
|
|
247
|
+
skill_stats[sid]["count"] += 1
|
|
248
|
+
if ex.timestamp > skill_stats[sid]["last"]:
|
|
249
|
+
skill_stats[sid]["last"] = ex.timestamp
|
|
250
|
+
|
|
251
|
+
# Filter by intent if provided
|
|
252
|
+
if intent:
|
|
253
|
+
intent_lower = intent.lower()
|
|
254
|
+
skill_stats = {
|
|
255
|
+
k: v
|
|
256
|
+
for k, v in skill_stats.items()
|
|
257
|
+
if k.lower() in intent_lower or any(w in k.lower() for w in intent_lower.split())
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# Sort by average gain
|
|
261
|
+
results = []
|
|
262
|
+
for sid, stats in skill_stats.items():
|
|
263
|
+
avg_gain = stats["total_gain"] / stats["count"] if stats["count"] > 0 else 0.0
|
|
264
|
+
results.append(
|
|
265
|
+
{
|
|
266
|
+
"skill_id": sid,
|
|
267
|
+
"avg_gain": round(avg_gain, 2),
|
|
268
|
+
"execution_count": stats["count"],
|
|
269
|
+
"last_execution": stats["last"],
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return sorted(results, key=lambda x: x["avg_gain"], reverse=True)[:top_k]
|
|
274
|
+
|
|
275
|
+
def get_gain_history(self, skill_id: str) -> list[dict]:
|
|
276
|
+
"""Get execution history for a specific skill.
|
|
277
|
+
|
|
278
|
+
Returns list of dicts with timestamp, gain, skill_ids.
|
|
279
|
+
"""
|
|
280
|
+
self._ensure_loaded()
|
|
281
|
+
return [
|
|
282
|
+
{"timestamp": ex.timestamp, "gain": ex.scores.effectiveness, "skill_ids": ex.skill_ids}
|
|
283
|
+
for ex in self._executions
|
|
284
|
+
if skill_id in ex.skill_ids
|
|
285
|
+
]
|
skillpool/gate.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GateManager — Skill 执行门禁 + ComplexityAssessor。
|
|
3
|
+
|
|
4
|
+
GateManager 根据 ComplexityAssessor 的评估结果决定是否放行 Skill 执行。
|
|
5
|
+
复杂度评估维度:context_size, dependency_depth, trust_requirement, veto_risk
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import StrEnum
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from skillpool.gate_policy.parser import GatePolicyError, load_gate_policy
|
|
16
|
+
from skillpool.gate_policy.state_machine import GateStateMachine
|
|
17
|
+
from skillpool.profile import AgentCapabilityProfile
|
|
18
|
+
from skillpool.telemetry import TelemetryBridge
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GateDecision(StrEnum):
|
|
22
|
+
ALLOW = "allow"
|
|
23
|
+
GUARD = "guard" # 允许但需要额外监督
|
|
24
|
+
DENY = "deny" # 拒绝执行
|
|
25
|
+
ESCALATE = "escalate" # 需要人工审批
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ComplexityScore:
|
|
30
|
+
"""复杂度评分结果。"""
|
|
31
|
+
|
|
32
|
+
total: float = 0.0
|
|
33
|
+
context_size: float = 0.0
|
|
34
|
+
dependency_depth: float = 0.0
|
|
35
|
+
trust_requirement: float = 0.0
|
|
36
|
+
veto_risk: float = 0.0
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def level(self) -> str:
|
|
40
|
+
"""复杂度等级: low / medium / high / critical"""
|
|
41
|
+
if self.total < 0.3:
|
|
42
|
+
return "low"
|
|
43
|
+
elif self.total < 0.6:
|
|
44
|
+
return "medium"
|
|
45
|
+
elif self.total < 0.8:
|
|
46
|
+
return "high"
|
|
47
|
+
return "critical"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ComplexityAssessor:
|
|
51
|
+
"""评估 Skill 执行复杂度。
|
|
52
|
+
|
|
53
|
+
4 个维度,每维度 0-1 分,加权汇总:
|
|
54
|
+
- context_size (w=0.25): CSDF checklist 条目数 / 20
|
|
55
|
+
- dependency_depth (w=0.25): skill_graph 依赖深度 / 5
|
|
56
|
+
- trust_requirement (w=0.25): min_trust_level / 3
|
|
57
|
+
- veto_risk (w=0.25): 有 veto_rule → 0.8, 否则 0.1
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
weights: Optional[dict[str, float]] = None,
|
|
63
|
+
):
|
|
64
|
+
self.weights = weights or {
|
|
65
|
+
"context_size": 0.25,
|
|
66
|
+
"dependency_depth": 0.25,
|
|
67
|
+
"trust_requirement": 0.25,
|
|
68
|
+
"veto_risk": 0.25,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def assess(self, csdf: dict) -> ComplexityScore:
|
|
72
|
+
"""评估 CSDF 的执行复杂度。"""
|
|
73
|
+
ctx = min(len(csdf.get("checklist", [])) / 20.0, 1.0)
|
|
74
|
+
deps = csdf.get("dependency_depth", 0)
|
|
75
|
+
dep_score = min(deps / 5.0, 1.0)
|
|
76
|
+
trust = min(csdf.get("min_trust_level", 0) / 3.0, 1.0)
|
|
77
|
+
veto = 0.8 if csdf.get("veto_rule") else 0.1
|
|
78
|
+
|
|
79
|
+
total = (
|
|
80
|
+
ctx * self.weights["context_size"]
|
|
81
|
+
+ dep_score * self.weights["dependency_depth"]
|
|
82
|
+
+ trust * self.weights["trust_requirement"]
|
|
83
|
+
+ veto * self.weights["veto_risk"]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return ComplexityScore(
|
|
87
|
+
total=total,
|
|
88
|
+
context_size=ctx,
|
|
89
|
+
dependency_depth=dep_score,
|
|
90
|
+
trust_requirement=trust,
|
|
91
|
+
veto_risk=veto,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class GateResult:
|
|
97
|
+
"""门禁决策结果。"""
|
|
98
|
+
|
|
99
|
+
decision: GateDecision
|
|
100
|
+
reason: str = ""
|
|
101
|
+
complexity: Optional[ComplexityScore] = None
|
|
102
|
+
conditions: list[str] = field(default_factory=list)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class GatePolicyResult(GateResult):
|
|
107
|
+
"""Gate result with policy-based phase information."""
|
|
108
|
+
|
|
109
|
+
policy_level: Optional[str] = None
|
|
110
|
+
skip_phases: list[str] = field(default_factory=list)
|
|
111
|
+
state: Optional[Any] = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class GateManager:
|
|
115
|
+
"""Skill 执行门禁管理器。
|
|
116
|
+
|
|
117
|
+
根据复杂度和能力匹配做执行决策:
|
|
118
|
+
- low complexity + capability match → ALLOW
|
|
119
|
+
- medium complexity → GUARD(允许但记录遥测)
|
|
120
|
+
- high complexity → ESCALATE
|
|
121
|
+
- critical / capability mismatch → DENY
|
|
122
|
+
- veto_risk > 0.5 → ESCALATE
|
|
123
|
+
|
|
124
|
+
Usage:
|
|
125
|
+
gm = GateManager(profile=CLAUDE_CODE_PROFILE, telemetry=bridge)
|
|
126
|
+
result = gm.check(csdf)
|
|
127
|
+
if result.decision == GateDecision.ALLOW:
|
|
128
|
+
# 执行 skill
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
profile: AgentCapabilityProfile,
|
|
134
|
+
telemetry: Optional[TelemetryBridge] = None,
|
|
135
|
+
assessor: Optional[ComplexityAssessor] = None,
|
|
136
|
+
):
|
|
137
|
+
self.profile = profile
|
|
138
|
+
self.telemetry = telemetry
|
|
139
|
+
self.assessor = assessor or ComplexityAssessor()
|
|
140
|
+
|
|
141
|
+
def check(self, csdf: dict) -> GateResult:
|
|
142
|
+
"""对 CSDF 做门禁检查,返回决策。"""
|
|
143
|
+
# 1. 能力匹配
|
|
144
|
+
can_exec, reason = self.profile.can_execute(
|
|
145
|
+
{
|
|
146
|
+
"required_capabilities": csdf.get("required_agent_capabilities", set()),
|
|
147
|
+
"min_trust_level": csdf.get("min_trust_level", 0),
|
|
148
|
+
"paradigm": csdf.get("paradigm"),
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if not can_exec:
|
|
153
|
+
result = GateResult(
|
|
154
|
+
decision=GateDecision.DENY,
|
|
155
|
+
reason=f"capability mismatch: {reason}",
|
|
156
|
+
)
|
|
157
|
+
self._emit_gate_event(csdf, result)
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
# 2. 复杂度评估
|
|
161
|
+
complexity = self.assessor.assess(csdf)
|
|
162
|
+
|
|
163
|
+
# 3. 决策逻辑
|
|
164
|
+
if complexity.veto_risk > 0.5 and complexity.total >= 0.6:
|
|
165
|
+
decision = GateDecision.ESCALATE
|
|
166
|
+
reason = f"veto risk + high complexity ({complexity.level})"
|
|
167
|
+
conditions = ["require_human_approval"]
|
|
168
|
+
elif complexity.level == "critical":
|
|
169
|
+
decision = GateDecision.DENY
|
|
170
|
+
reason = f"critical complexity ({complexity.total:.2f})"
|
|
171
|
+
conditions = []
|
|
172
|
+
elif complexity.level == "high":
|
|
173
|
+
decision = GateDecision.ESCALATE
|
|
174
|
+
reason = f"high complexity ({complexity.total:.2f})"
|
|
175
|
+
conditions = ["require_human_approval"]
|
|
176
|
+
elif complexity.level == "medium":
|
|
177
|
+
decision = GateDecision.GUARD
|
|
178
|
+
reason = f"medium complexity ({complexity.total:.2f})"
|
|
179
|
+
conditions = ["log_telemetry", "monitor_execution"]
|
|
180
|
+
else:
|
|
181
|
+
decision = GateDecision.ALLOW
|
|
182
|
+
reason = f"low complexity ({complexity.total:.2f})"
|
|
183
|
+
conditions = []
|
|
184
|
+
|
|
185
|
+
result = GateResult(
|
|
186
|
+
decision=decision,
|
|
187
|
+
reason=reason,
|
|
188
|
+
complexity=complexity,
|
|
189
|
+
conditions=conditions,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
self._emit_gate_event(csdf, result)
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
def check_with_policy(
|
|
196
|
+
self,
|
|
197
|
+
csdf: dict,
|
|
198
|
+
policy_path: Path | None = None,
|
|
199
|
+
changed_files: list[str] | None = None,
|
|
200
|
+
) -> GatePolicyResult:
|
|
201
|
+
"""Gate check with policy-based phase enforcement.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
csdf: CSDF skill definition dict.
|
|
205
|
+
policy_path: Path to gate.policy file.
|
|
206
|
+
changed_files: Optional list of changed files for incremental mode.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
GatePolicyResult with policy_level, skip_phases, and state.
|
|
210
|
+
"""
|
|
211
|
+
# Run standard gate check first
|
|
212
|
+
base = self.check(csdf)
|
|
213
|
+
|
|
214
|
+
policy_level: str | None = None
|
|
215
|
+
skip_phases: list[str] = []
|
|
216
|
+
state = None
|
|
217
|
+
|
|
218
|
+
if policy_path:
|
|
219
|
+
try:
|
|
220
|
+
policy = load_gate_policy(policy_path)
|
|
221
|
+
except GatePolicyError:
|
|
222
|
+
return GatePolicyResult(
|
|
223
|
+
decision=base.decision,
|
|
224
|
+
reason=base.reason,
|
|
225
|
+
complexity=base.complexity,
|
|
226
|
+
conditions=base.conditions,
|
|
227
|
+
policy_level=None,
|
|
228
|
+
skip_phases=[],
|
|
229
|
+
state=None,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Create state machine and assess
|
|
233
|
+
state_path = policy_path.parent / "gate.json"
|
|
234
|
+
sm = GateStateMachine(state_path)
|
|
235
|
+
sm.set_policy(policy)
|
|
236
|
+
|
|
237
|
+
task_desc = csdf.get("description", csdf.get("id", "unknown"))
|
|
238
|
+
if changed_files is None:
|
|
239
|
+
changed_files = []
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
assessed = sm.assess(task_desc, changed_files, policy)
|
|
243
|
+
except GatePolicyError:
|
|
244
|
+
assessed = None
|
|
245
|
+
|
|
246
|
+
if assessed:
|
|
247
|
+
policy_level = assessed
|
|
248
|
+
# Resolve skip_phases from policy for changed files
|
|
249
|
+
if changed_files:
|
|
250
|
+
all_skip: set[str] = set()
|
|
251
|
+
for f in changed_files:
|
|
252
|
+
from skillpool.gate_policy.parser import resolve_level_for_path
|
|
253
|
+
|
|
254
|
+
resolution = resolve_level_for_path(f, policy)
|
|
255
|
+
all_skip.update(resolution.skip_phases)
|
|
256
|
+
skip_phases = sorted(all_skip)
|
|
257
|
+
|
|
258
|
+
state = sm.state
|
|
259
|
+
|
|
260
|
+
return GatePolicyResult(
|
|
261
|
+
decision=base.decision,
|
|
262
|
+
reason=base.reason,
|
|
263
|
+
complexity=base.complexity,
|
|
264
|
+
conditions=base.conditions,
|
|
265
|
+
policy_level=policy_level,
|
|
266
|
+
skip_phases=skip_phases,
|
|
267
|
+
state=state,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def _emit_gate_event(self, csdf: dict, result: GateResult) -> None:
|
|
271
|
+
"""发射门禁遥测事件。"""
|
|
272
|
+
if self.telemetry:
|
|
273
|
+
self.telemetry.emit(
|
|
274
|
+
event_type="gate_check",
|
|
275
|
+
skill_id=csdf.get("id", "unknown"),
|
|
276
|
+
channel="hook",
|
|
277
|
+
payload={
|
|
278
|
+
"decision": str(result.decision),
|
|
279
|
+
"reason": result.reason,
|
|
280
|
+
"complexity": result.complexity.level if result.complexity else None,
|
|
281
|
+
},
|
|
282
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Gate Policy — Phase enforcement via gate.policy and gate.json."""
|
|
2
|
+
|
|
3
|
+
from skillpool.gate_policy.parser import (
|
|
4
|
+
GatePolicyConfig,
|
|
5
|
+
GatePolicyError,
|
|
6
|
+
LevelResolution,
|
|
7
|
+
load_gate_policy,
|
|
8
|
+
resolve_level_for_path,
|
|
9
|
+
)
|
|
10
|
+
from skillpool.gate_policy.state_machine import (
|
|
11
|
+
GateCheckResult,
|
|
12
|
+
GateStateFile,
|
|
13
|
+
GateStateMachine,
|
|
14
|
+
)
|
|
15
|
+
from skillpool.gate_policy.incremental import (
|
|
16
|
+
ComplexityAssessment,
|
|
17
|
+
IncrementalAssessor,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"GatePolicyConfig",
|
|
22
|
+
"GatePolicyError",
|
|
23
|
+
"LevelResolution",
|
|
24
|
+
"load_gate_policy",
|
|
25
|
+
"resolve_level_for_path",
|
|
26
|
+
"GateCheckResult",
|
|
27
|
+
"GateStateFile",
|
|
28
|
+
"GateStateMachine",
|
|
29
|
+
"ComplexityAssessment",
|
|
30
|
+
"IncrementalAssessor",
|
|
31
|
+
]
|