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.
Files changed (90) hide show
  1. skillpool/__init__.py +74 -0
  2. skillpool/__main__.py +6 -0
  3. skillpool/adapters/__init__.py +8 -0
  4. skillpool/adapters/base.py +41 -0
  5. skillpool/adapters/claude_adapter.py +36 -0
  6. skillpool/adapters/codex_adapter.py +92 -0
  7. skillpool/adapters/hermes_adapter.py +38 -0
  8. skillpool/audit/__init__.py +651 -0
  9. skillpool/bridge/__init__.py +16 -0
  10. skillpool/bridge/freeze_detector.py +134 -0
  11. skillpool/bridge/maintenance.py +119 -0
  12. skillpool/bridge/wal_manager.py +136 -0
  13. skillpool/clawmem_client.py +176 -0
  14. skillpool/cli.py +700 -0
  15. skillpool/combiner/__init__.py +31 -0
  16. skillpool/combiner/lifecycle.py +453 -0
  17. skillpool/combiner/models.py +99 -0
  18. skillpool/config.py +34 -0
  19. skillpool/cost/__init__.py +111 -0
  20. skillpool/cost/audit_hash.py +51 -0
  21. skillpool/cost/budget_tracker.py +66 -0
  22. skillpool/cost/dashboard.py +189 -0
  23. skillpool/cost/models.py +129 -0
  24. skillpool/cost/token_governor.py +264 -0
  25. skillpool/cost/trace_ceiling.py +38 -0
  26. skillpool/csdf.py +126 -0
  27. skillpool/evolver/__init__.py +978 -0
  28. skillpool/gain/__init__.py +285 -0
  29. skillpool/gate.py +282 -0
  30. skillpool/gate_policy/__init__.py +31 -0
  31. skillpool/gate_policy/incremental.py +157 -0
  32. skillpool/gate_policy/parser.py +258 -0
  33. skillpool/gate_policy/state_machine.py +432 -0
  34. skillpool/graph/__init__.py +14 -0
  35. skillpool/graph/ppr.py +279 -0
  36. skillpool/health/__init__.py +73 -0
  37. skillpool/health/check.py +85 -0
  38. skillpool/health/degradation.py +90 -0
  39. skillpool/health/models.py +43 -0
  40. skillpool/hooks/__init__.py +4 -0
  41. skillpool/hooks/security_scanner.py +288 -0
  42. skillpool/lifecycle.py +150 -0
  43. skillpool/materializer/__init__.py +124 -0
  44. skillpool/materializer/budget_cropper.py +178 -0
  45. skillpool/materializer/csdf_loader.py +114 -0
  46. skillpool/materializer/lazy_loader.py +265 -0
  47. skillpool/materializer/lifecycle_filter.py +93 -0
  48. skillpool/materializer/mapper.py +178 -0
  49. skillpool/materializer/models.py +66 -0
  50. skillpool/mcp_server.py +2005 -0
  51. skillpool/monitor/__init__.py +576 -0
  52. skillpool/monitor/bug_collector.py +392 -0
  53. skillpool/monitor/defect_classifier.py +218 -0
  54. skillpool/monitor/self_healing.py +530 -0
  55. skillpool/monitor/telemetry_bridge.py +197 -0
  56. skillpool/paradigm/__init__.py +312 -0
  57. skillpool/paradigm/override.py +285 -0
  58. skillpool/profile.py +94 -0
  59. skillpool/quality.py +254 -0
  60. skillpool/registry/__init__.py +509 -0
  61. skillpool/registry/models.py +98 -0
  62. skillpool/resolver/__init__.py +320 -0
  63. skillpool/resolver/cache.py +103 -0
  64. skillpool/resolver/circuit_breaker.py +103 -0
  65. skillpool/resolver/conflict_detector.py +111 -0
  66. skillpool/resolver/health_filter.py +38 -0
  67. skillpool/resolver/models.py +154 -0
  68. skillpool/resolver/rate_limiter.py +48 -0
  69. skillpool/resolver/skill_graph.py +183 -0
  70. skillpool/review/__init__.py +242 -0
  71. skillpool/review/async_queue.py +96 -0
  72. skillpool/review/checkpoint_runner.py +345 -0
  73. skillpool/review/models.py +164 -0
  74. skillpool/review/suspect_marker.py +39 -0
  75. skillpool/review/veto_evaluator.py +94 -0
  76. skillpool/router/__init__.py +481 -0
  77. skillpool/schemas.py +119 -0
  78. skillpool/synergy/__init__.py +240 -0
  79. skillpool/synergy/detector.py +5 -0
  80. skillpool/telemetry.py +126 -0
  81. skillpool/utils/__init__.py +21 -0
  82. skillpool/utils/changelog.py +218 -0
  83. skillpool/utils/logger.py +273 -0
  84. skillpool/utils/runtime_audit.py +163 -0
  85. skillpool/utils/time_utils.py +13 -0
  86. skillpool-4.3.0.dist-info/METADATA +21 -0
  87. skillpool-4.3.0.dist-info/RECORD +90 -0
  88. skillpool-4.3.0.dist-info/WHEEL +5 -0
  89. skillpool-4.3.0.dist-info/entry_points.txt +3 -0
  90. 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
+ ]