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
skillpool/mcp_server.py
ADDED
|
@@ -0,0 +1,2005 @@
|
|
|
1
|
+
"""MCP Server — expose skillpool via FastMCP with Resources/Tools/Prompts separation.
|
|
2
|
+
|
|
3
|
+
Architecture (V4.3 dual-channel):
|
|
4
|
+
- CLI (Start Hook): Materialization channel — one-time file writes at session start
|
|
5
|
+
- MCP Resources: Read-only context delivery — skill definitions, audit records
|
|
6
|
+
- MCP Tools: Governance actions — register, transition, gate_check, review, telemetry
|
|
7
|
+
- MCP Prompts: User-controlled templates — skill invocation, review trigger
|
|
8
|
+
|
|
9
|
+
Why Resources vs Tools (per MCP 2025-03-26 spec):
|
|
10
|
+
- Resources = application-controlled, read-only contextual data
|
|
11
|
+
- Tools = model-controlled, executable functions that change state
|
|
12
|
+
- Skill content delivery is read-only context → Resources
|
|
13
|
+
- Governance mutations are state-changing actions → Tools
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
import yaml
|
|
24
|
+
from fastmcp import FastMCP
|
|
25
|
+
from fastmcp.server.middleware import Middleware
|
|
26
|
+
|
|
27
|
+
from skillpool.materializer.lazy_loader import LazySkillLoader # Part of SkillPool — independent infrastructure
|
|
28
|
+
from skillpool.materializer.csdf_loader import load_csdf as _load_csdf_shared # Part of SkillPool
|
|
29
|
+
from skillpool.hooks.security_scanner import SecurityScanner # Part of SkillPool — independent infrastructure
|
|
30
|
+
from skillpool.telemetry import TelemetryBridge
|
|
31
|
+
from skillpool.gate import GateManager
|
|
32
|
+
from skillpool.profile import (
|
|
33
|
+
CLAUDE_CODE_PROFILE,
|
|
34
|
+
CODEX_PROFILE,
|
|
35
|
+
HERMES_PROFILE,
|
|
36
|
+
OPENCLAW_PROFILE,
|
|
37
|
+
AgentCapabilityProfile,
|
|
38
|
+
)
|
|
39
|
+
from skillpool.audit import AuditLayer
|
|
40
|
+
from skillpool.evolver import EvolverLayer, DefectSeverity
|
|
41
|
+
from skillpool.registry import Registry
|
|
42
|
+
from skillpool.monitor import MonitorLayer, MetricType
|
|
43
|
+
from skillpool.monitor.bug_collector import BugCollector # Part of SkillPool — independent infrastructure
|
|
44
|
+
from skillpool.monitor.self_healing import SelfHealingLoop # Part of SkillPool — independent infrastructure
|
|
45
|
+
from skillpool.health import HealthManager
|
|
46
|
+
from skillpool.config import get_data_dir
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger("skillpool.mcp")
|
|
49
|
+
|
|
50
|
+
mcp = FastMCP("skillpool", version="4.3.0")
|
|
51
|
+
|
|
52
|
+
# Search-first enforcement: tracks which callers have performed a search.
|
|
53
|
+
# Keyed by agent_type (all agents sharing the same MCP process).
|
|
54
|
+
# This is Agent-neutral — no dependency on Claude Code session IDs.
|
|
55
|
+
_search_done_callers: set[str] = set()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
59
|
+
# MIDDLEWARE — Auth + Logging + Timing
|
|
60
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AuthMiddleware(Middleware):
|
|
64
|
+
"""API key authentication for MCP endpoints.
|
|
65
|
+
|
|
66
|
+
Enabled by setting SKILLPOOL_API_KEY environment variable.
|
|
67
|
+
If not set, authentication is disabled (backward compatible).
|
|
68
|
+
Clients must send the key via Authorization: Bearer <key> header
|
|
69
|
+
or as api_key query parameter in HTTP transport.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self) -> None:
|
|
73
|
+
self._required_key: str | None = os.environ.get("SKILLPOOL_API_KEY")
|
|
74
|
+
if self._required_key:
|
|
75
|
+
logger.info("mcp_auth_enabled")
|
|
76
|
+
else:
|
|
77
|
+
logger.info("mcp_auth_disabled_no_key_set")
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def auth_enabled(self) -> bool:
|
|
81
|
+
return self._required_key is not None
|
|
82
|
+
|
|
83
|
+
def _validate_key(self, context) -> bool:
|
|
84
|
+
"""Extract and validate API key from request context."""
|
|
85
|
+
if not self._required_key:
|
|
86
|
+
return True
|
|
87
|
+
# Check Authorization: Bearer <key>
|
|
88
|
+
headers = getattr(context, "headers", None) or {}
|
|
89
|
+
auth_header = headers.get("authorization", "") or headers.get("Authorization", "")
|
|
90
|
+
if auth_header.startswith("Bearer "):
|
|
91
|
+
token = auth_header[7:].strip()
|
|
92
|
+
if token == self._required_key:
|
|
93
|
+
return True
|
|
94
|
+
# Check api_key in message arguments (for tool calls)
|
|
95
|
+
args = getattr(context.message, "arguments", None) or {}
|
|
96
|
+
if isinstance(args, dict) and args.get("api_key") == self._required_key:
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
async def on_call_tool(self, context, call_next):
|
|
101
|
+
if not self._validate_key(context):
|
|
102
|
+
logger.warning("mcp_auth_rejected", extra={"tool": context.message.name})
|
|
103
|
+
from fastmcp.utilities.types import TextContent
|
|
104
|
+
from fastmcp.server.types import ToolResult
|
|
105
|
+
|
|
106
|
+
return ToolResult(
|
|
107
|
+
content=[
|
|
108
|
+
TextContent(type="text", text='{"error": "unauthorized", "detail": "Invalid or missing API key"}')
|
|
109
|
+
]
|
|
110
|
+
)
|
|
111
|
+
return await call_next(context)
|
|
112
|
+
|
|
113
|
+
async def on_read_resource(self, context, call_next):
|
|
114
|
+
if not self._validate_key(context):
|
|
115
|
+
logger.warning("mcp_auth_rejected_resource", extra={"uri": str(getattr(context.message, "uri", "unknown"))})
|
|
116
|
+
from fastmcp.utilities.types import TextContent
|
|
117
|
+
|
|
118
|
+
return [TextContent(type="text", text='{"error": "unauthorized", "detail": "Invalid or missing API key"}')]
|
|
119
|
+
return await call_next(context)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SkillPoolLoggingMiddleware(Middleware):
|
|
123
|
+
"""Log every MCP tool call and resource read."""
|
|
124
|
+
|
|
125
|
+
async def on_call_tool(self, context, call_next):
|
|
126
|
+
tool_name = context.message.name
|
|
127
|
+
args = context.message.arguments or {}
|
|
128
|
+
args_summary = {k: type(v).__name__ for k, v in args.items()} if args else {}
|
|
129
|
+
logger.info("mcp_tool_call_start", extra={"tool": tool_name, "args_summary": args_summary})
|
|
130
|
+
result = await call_next(context)
|
|
131
|
+
status = self._detect_error_status(result)
|
|
132
|
+
logger.info("mcp_tool_call_end", extra={"tool": tool_name, "status": status})
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
async def on_read_resource(self, context, call_next):
|
|
136
|
+
uri = str(context.message.uri) if hasattr(context.message, "uri") else "unknown"
|
|
137
|
+
logger.info("mcp_resource_read_start", extra={"uri": uri})
|
|
138
|
+
|
|
139
|
+
# Search-first enforcement: soft guidance for Resource reads
|
|
140
|
+
# Hard enforcement is in skill_get Tool (for model-controlled access)
|
|
141
|
+
# Resource reads can't be blocked (application-controlled), so we only log
|
|
142
|
+
uri_str = uri.lower()
|
|
143
|
+
if uri_str.startswith("skill://") and uri_str != "skill://list" and uri_str != "skill://graph":
|
|
144
|
+
if not _search_done_callers:
|
|
145
|
+
logger.info("search_enforcement_soft", extra={"uri": uri, "note": "skill_get Tool enforces hard block"})
|
|
146
|
+
|
|
147
|
+
result = await call_next(context)
|
|
148
|
+
logger.info("mcp_resource_read_end", extra={"uri": uri, "status": "success"})
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _detect_error_status(result) -> str:
|
|
153
|
+
"""Detect error from ToolResult — no is_error field on FastMCP ToolResult."""
|
|
154
|
+
# Check structured_content for error key
|
|
155
|
+
sc = getattr(result, "structured_content", None)
|
|
156
|
+
if isinstance(sc, dict) and "error" in sc:
|
|
157
|
+
return "error"
|
|
158
|
+
# Check content text for error indicators
|
|
159
|
+
content = getattr(result, "content", [])
|
|
160
|
+
for block in content:
|
|
161
|
+
text = getattr(block, "text", "")
|
|
162
|
+
if isinstance(text, str) and '"error"' in text:
|
|
163
|
+
return "error"
|
|
164
|
+
return "success"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TimingMiddleware(Middleware):
|
|
168
|
+
"""Track execution time for tool calls and resource reads; store in MonitorLayer."""
|
|
169
|
+
|
|
170
|
+
def __init__(self, monitor: MonitorLayer) -> None:
|
|
171
|
+
self._monitor = monitor
|
|
172
|
+
|
|
173
|
+
async def on_call_tool(self, context, call_next):
|
|
174
|
+
tool_name = context.message.name
|
|
175
|
+
start = time.monotonic()
|
|
176
|
+
result = await call_next(context)
|
|
177
|
+
elapsed_ms = (time.monotonic() - start) * 1000
|
|
178
|
+
success = SkillPoolLoggingMiddleware._detect_error_status(result) == "success"
|
|
179
|
+
self._monitor.record_latency(
|
|
180
|
+
skill_id=f"mcp_tool:{tool_name}",
|
|
181
|
+
latency_ms=elapsed_ms,
|
|
182
|
+
success=success,
|
|
183
|
+
)
|
|
184
|
+
logger.debug("mcp_tool_timing", extra={"tool": tool_name, "elapsed_ms": round(elapsed_ms, 2)})
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
async def on_read_resource(self, context, call_next):
|
|
188
|
+
uri = str(context.message.uri) if hasattr(context.message, "uri") else "unknown"
|
|
189
|
+
start = time.monotonic()
|
|
190
|
+
result = await call_next(context)
|
|
191
|
+
elapsed_ms = (time.monotonic() - start) * 1000
|
|
192
|
+
self._monitor.record_metric(
|
|
193
|
+
name="mcp_resource_read_latency_ms",
|
|
194
|
+
value=elapsed_ms,
|
|
195
|
+
metric_type=MetricType.HISTOGRAM,
|
|
196
|
+
labels={"uri": uri},
|
|
197
|
+
)
|
|
198
|
+
logger.debug("mcp_resource_timing", extra={"uri": uri, "elapsed_ms": round(elapsed_ms, 2)})
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
_PROFILES: dict[str, AgentCapabilityProfile] = {
|
|
203
|
+
"claude-code": CLAUDE_CODE_PROFILE,
|
|
204
|
+
"codex": CODEX_PROFILE,
|
|
205
|
+
"hermes": HERMES_PROFILE,
|
|
206
|
+
"openclaw": OPENCLAW_PROFILE,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_SKILLPOOL_DIR = get_data_dir()
|
|
210
|
+
_SKILLS_DIR = _SKILLPOOL_DIR / "skills"
|
|
211
|
+
|
|
212
|
+
# Resource cache — TTL-based LRU for skill definitions/rules/manifests
|
|
213
|
+
_RESOURCE_CACHE: dict[str, tuple[float, object]] = {} # uri → (timestamp, result)
|
|
214
|
+
_RESOURCE_CACHE_TTL = 60.0 # seconds — stale entries expire after 1 minute
|
|
215
|
+
_RESOURCE_CACHE_MAX = 50 # max cached entries
|
|
216
|
+
|
|
217
|
+
# Shared instances (lazy-initialized)
|
|
218
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
219
|
+
_audit = AuditLayer()
|
|
220
|
+
_evolver = EvolverLayer(audit_layer=_audit)
|
|
221
|
+
_registry = Registry(audit_layer=_audit)
|
|
222
|
+
_monitor = MonitorLayer(audit_layer=_audit)
|
|
223
|
+
_health = HealthManager(monitor=_monitor)
|
|
224
|
+
_lazy_loader = LazySkillLoader()
|
|
225
|
+
_bug_collector = BugCollector() # Part of SkillPool — independent infrastructure
|
|
226
|
+
_self_healing = SelfHealingLoop(bug_collector=_bug_collector, evolver=_evolver) # Part of SkillPool
|
|
227
|
+
_security_scanner = SecurityScanner() # Part of SkillPool
|
|
228
|
+
|
|
229
|
+
# Register middleware on the MCP instance
|
|
230
|
+
mcp.add_middleware(AuthMiddleware())
|
|
231
|
+
mcp.add_middleware(SkillPoolLoggingMiddleware())
|
|
232
|
+
mcp.add_middleware(TimingMiddleware(monitor=_monitor))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _get_profile(name: str) -> AgentCapabilityProfile:
|
|
236
|
+
"""Resolve profile by name. Raises ValueError for unknown agent types."""
|
|
237
|
+
if name not in _PROFILES:
|
|
238
|
+
raise ValueError(f"Unknown agent_type '{name}'. Must be one of: {', '.join(_PROFILES.keys())}")
|
|
239
|
+
return _PROFILES[name]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _cached_resource(uri: str, compute_fn):
|
|
243
|
+
"""TTL-based cache for MCP Resources. Returns cached value if fresh."""
|
|
244
|
+
now = time.monotonic()
|
|
245
|
+
if uri in _RESOURCE_CACHE:
|
|
246
|
+
ts, val = _RESOURCE_CACHE[uri]
|
|
247
|
+
if now - ts < _RESOURCE_CACHE_TTL:
|
|
248
|
+
return val
|
|
249
|
+
# Evict oldest if at capacity
|
|
250
|
+
if len(_RESOURCE_CACHE) >= _RESOURCE_CACHE_MAX:
|
|
251
|
+
oldest = min(_RESOURCE_CACHE, key=lambda k: _RESOURCE_CACHE[k][0])
|
|
252
|
+
del _RESOURCE_CACHE[oldest]
|
|
253
|
+
val = compute_fn()
|
|
254
|
+
_RESOURCE_CACHE[uri] = (now, val)
|
|
255
|
+
return val
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _load_csdf(skill_id: str) -> dict | None:
|
|
259
|
+
"""Load CSDF YAML for a skill_id from ~/.skillpool/skills/.
|
|
260
|
+
|
|
261
|
+
Delegates to the shared csdf_loader module.
|
|
262
|
+
"""
|
|
263
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
264
|
+
return _load_csdf_shared(skill_id, _SKILLS_DIR)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
268
|
+
# RESOURCES — Application-controlled, read-only context delivery
|
|
269
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@mcp.resource("skill://list")
|
|
273
|
+
def skill_list() -> list[dict]:
|
|
274
|
+
"""Return lightweight metadata for all available skills (cached 60s).
|
|
275
|
+
|
|
276
|
+
Returns only id, name, version, dimension, tags — no full content.
|
|
277
|
+
Uses LazySkillLoader L0 tier for token-efficient delivery (~50 tokens/skill).
|
|
278
|
+
|
|
279
|
+
Use skill://{skill_id}/summary for medium detail (~200 tokens).
|
|
280
|
+
Use skill://{skill_id}/definition for full content.
|
|
281
|
+
"""
|
|
282
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
283
|
+
return _cached_resource("skill://list", lambda: _compute_skill_list())
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _compute_skill_list() -> list[dict]:
|
|
287
|
+
"""Compute skill list from filesystem (uncached)."""
|
|
288
|
+
skills = []
|
|
289
|
+
if not _SKILLS_DIR.exists():
|
|
290
|
+
return skills
|
|
291
|
+
|
|
292
|
+
# Collect all skill IDs from filesystem
|
|
293
|
+
skill_ids = []
|
|
294
|
+
# 1. CSDF YAML skills
|
|
295
|
+
for yaml_file in sorted(_SKILLS_DIR.glob("*.yaml")):
|
|
296
|
+
if yaml_file.name == "skill_graph.yaml":
|
|
297
|
+
continue
|
|
298
|
+
# Extract skill_id from filename: "S09-resilience-degradation" → "S09"
|
|
299
|
+
stem = yaml_file.stem
|
|
300
|
+
skill_id = stem.split("-")[0] if stem[0:2].isupper() else stem
|
|
301
|
+
skill_ids.append(skill_id)
|
|
302
|
+
# 2. Directory-based skills
|
|
303
|
+
for skill_dir in sorted(_SKILLS_DIR.iterdir()):
|
|
304
|
+
if skill_dir.is_dir() and (skill_dir / "SKILL.md").exists():
|
|
305
|
+
skill_ids.append(skill_dir.name)
|
|
306
|
+
|
|
307
|
+
# Use LazySkillLoader for L0 metadata
|
|
308
|
+
loaded = _lazy_loader.preload(skill_ids, tier="L0")
|
|
309
|
+
for skill_id, data in sorted(loaded.items()):
|
|
310
|
+
skills.append(data)
|
|
311
|
+
|
|
312
|
+
return skills
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@mcp.resource("skill://{skill_id}/definition")
|
|
316
|
+
def skill_definition(skill_id: str) -> str:
|
|
317
|
+
"""Return the full SKILL.md content for a skill.
|
|
318
|
+
|
|
319
|
+
Uses LazySkillLoader L2 tier (full materialization) with in-memory cache.
|
|
320
|
+
First call materializes, subsequent calls return cached result.
|
|
321
|
+
"""
|
|
322
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
323
|
+
try:
|
|
324
|
+
data = _lazy_loader.load(skill_id, tier="L2")
|
|
325
|
+
# For directory-based skills, prefer the raw SKILL.md body over
|
|
326
|
+
# the Materializer-generated summary (which only uses frontmatter fields)
|
|
327
|
+
if "_markdown_body" in data and data["_markdown_body"]:
|
|
328
|
+
return data["_markdown_body"]
|
|
329
|
+
if "markdown" in data and data["markdown"]:
|
|
330
|
+
return data["markdown"]
|
|
331
|
+
# Materialization failed — return partial info
|
|
332
|
+
name = data.get("name", skill_id)
|
|
333
|
+
return f"# {name}\n\nContent unavailable (materialization errors: {data.get('_materialization_errors', [])})"
|
|
334
|
+
except ValueError:
|
|
335
|
+
return f"Skill not found: {skill_id}"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@mcp.resource("skill://{skill_id}/summary")
|
|
339
|
+
def skill_summary(skill_id: str) -> dict:
|
|
340
|
+
"""Return medium-detail metadata for a skill (~200 tokens).
|
|
341
|
+
|
|
342
|
+
Uses LazySkillLoader L1 tier: includes description, triggers,
|
|
343
|
+
veto rules, and dependencies — enough to decide whether to load
|
|
344
|
+
the full definition.
|
|
345
|
+
"""
|
|
346
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
347
|
+
try:
|
|
348
|
+
return _lazy_loader.load(skill_id, tier="L1")
|
|
349
|
+
except ValueError:
|
|
350
|
+
return {"error": f"Skill not found: {skill_id}"}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@mcp.resource("skill://{skill_id}/manifest.yaml")
|
|
354
|
+
def skill_manifest(skill_id: str) -> dict:
|
|
355
|
+
"""Return the dependency manifest for a skill.
|
|
356
|
+
|
|
357
|
+
Includes: dependencies, conflicts, requires, veto_rule.
|
|
358
|
+
"""
|
|
359
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
360
|
+
csdf = _load_csdf(skill_id)
|
|
361
|
+
if csdf is None:
|
|
362
|
+
return {"error": f"Skill not found: {skill_id}"}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
"id": csdf.get("id", skill_id),
|
|
366
|
+
"name": csdf.get("name", ""),
|
|
367
|
+
"version": csdf.get("version", ""),
|
|
368
|
+
"description": csdf.get("description", ""),
|
|
369
|
+
"dimension": csdf.get("dimension", ""),
|
|
370
|
+
"dependencies": csdf.get("dependencies", []),
|
|
371
|
+
"conflicts": csdf.get("conflicts", []),
|
|
372
|
+
"requires": csdf.get("requires", []),
|
|
373
|
+
"synergies": csdf.get("synergies", []),
|
|
374
|
+
"values": csdf.get("values", []),
|
|
375
|
+
"veto_rule": csdf.get("veto_rule", ""),
|
|
376
|
+
"weight": csdf.get("weight", 0),
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@mcp.resource("skill://{skill_id}/x-execution")
|
|
381
|
+
def skill_execution(skill_id: str) -> dict:
|
|
382
|
+
"""Return the execution method for a skill.
|
|
383
|
+
|
|
384
|
+
Describes how the skill is invoked: prompt, script, or mcp_tool.
|
|
385
|
+
"""
|
|
386
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
387
|
+
csdf = _load_csdf(skill_id)
|
|
388
|
+
if csdf is None:
|
|
389
|
+
return {"error": f"Skill not found: {skill_id}"}
|
|
390
|
+
|
|
391
|
+
input_schema = csdf.get("input_schema", {})
|
|
392
|
+
output_schema = csdf.get("output_schema", {})
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
"id": csdf.get("id", skill_id),
|
|
396
|
+
"execution_type": "prompt", # All current skills are prompt-based
|
|
397
|
+
"input_schema": input_schema,
|
|
398
|
+
"output_schema": output_schema,
|
|
399
|
+
"checklist": csdf.get("checklist", []),
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@mcp.resource("skill://{skill_id}/rules")
|
|
404
|
+
def skill_rules(skill_id: str) -> str:
|
|
405
|
+
"""Return the full RULES.md content for a directory-based skill.
|
|
406
|
+
|
|
407
|
+
For skills like multi-dim-review that have a separate RULES.md with
|
|
408
|
+
detailed scoring rules, veto conditions, and blind spot management.
|
|
409
|
+
Returns empty string if no RULES.md exists.
|
|
410
|
+
"""
|
|
411
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
412
|
+
skill_dir = _SKILLS_DIR / skill_id
|
|
413
|
+
rules_path = skill_dir / "RULES.md"
|
|
414
|
+
if rules_path.exists():
|
|
415
|
+
return rules_path.read_text(encoding="utf-8")
|
|
416
|
+
# Not a directory-based skill or no RULES.md
|
|
417
|
+
return ""
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@mcp.resource("skill://graph")
|
|
421
|
+
def skill_graph() -> dict:
|
|
422
|
+
"""Return the skill dependency graph (DAG structure, cached 60s)."""
|
|
423
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
424
|
+
return _cached_resource("skill://graph", lambda: _compute_skill_graph())
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _compute_skill_graph() -> dict:
|
|
428
|
+
"""Compute skill graph from filesystem (uncached)."""
|
|
429
|
+
graph_path = _SKILLS_DIR / "skill_graph.yaml"
|
|
430
|
+
if not graph_path.exists():
|
|
431
|
+
return {"error": "skill_graph.yaml not found"}
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
return yaml.safe_load(graph_path.read_text()) or {}
|
|
435
|
+
except Exception as e:
|
|
436
|
+
return {"error": f"Failed to load graph: {e}"}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@mcp.resource("audit://records/{cursor}")
|
|
440
|
+
def audit_records(cursor: int = 0, limit: int = 100) -> dict:
|
|
441
|
+
"""Return audit records with pagination (read-only, immutable).
|
|
442
|
+
|
|
443
|
+
Audit records cannot be modified via MCP — only read for traceability.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
cursor: Offset index to start from (0-based).
|
|
447
|
+
limit: Maximum number of records to return (1-500, default 100).
|
|
448
|
+
"""
|
|
449
|
+
limit = max(1, min(limit, 500))
|
|
450
|
+
records = _audit.get_records()
|
|
451
|
+
total = len(records)
|
|
452
|
+
page = records[cursor : cursor + limit]
|
|
453
|
+
return {
|
|
454
|
+
"total": total,
|
|
455
|
+
"cursor": cursor,
|
|
456
|
+
"limit": limit,
|
|
457
|
+
"next_cursor": cursor + limit if cursor + limit < total else None,
|
|
458
|
+
"records": [
|
|
459
|
+
{
|
|
460
|
+
"audit_id": r.audit_id,
|
|
461
|
+
"action": r.action,
|
|
462
|
+
"actor": r.actor,
|
|
463
|
+
"resource_id": r.resource_id,
|
|
464
|
+
"result": r.result,
|
|
465
|
+
"severity": r.severity,
|
|
466
|
+
"timestamp": r.created_at,
|
|
467
|
+
"chain_index": r.chain_index,
|
|
468
|
+
}
|
|
469
|
+
for r in page
|
|
470
|
+
],
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@mcp.resource("bug://list/{cursor}")
|
|
475
|
+
def bug_list(cursor: int = 0, limit: int = 100) -> dict:
|
|
476
|
+
"""Return collected bug records with pagination from BugCollector.
|
|
477
|
+
|
|
478
|
+
Query bug records captured by the 4-stage pipeline
|
|
479
|
+
(Capture→Enrich→Filter→Persist). Read-only via MCP.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
cursor: Offset index to start from (0-based).
|
|
483
|
+
limit: Maximum number of records to return (1-500, default 100).
|
|
484
|
+
"""
|
|
485
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
486
|
+
limit = max(1, min(limit, 500))
|
|
487
|
+
bugs = _bug_collector.get_bugs()
|
|
488
|
+
total = len(bugs)
|
|
489
|
+
page = bugs[cursor : cursor + limit]
|
|
490
|
+
return {
|
|
491
|
+
"total": total,
|
|
492
|
+
"cursor": cursor,
|
|
493
|
+
"limit": limit,
|
|
494
|
+
"next_cursor": cursor + limit if cursor + limit < total else None,
|
|
495
|
+
"bugs": [b.to_dict() for b in page],
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
500
|
+
# TOOLS — Model-controlled, state-changing governance actions
|
|
501
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
@mcp.tool()
|
|
505
|
+
def gate_check(csdf: dict, profile_name: str) -> dict:
|
|
506
|
+
"""Check gate decision for a CSDF skill.
|
|
507
|
+
|
|
508
|
+
Returns ALLOW/GUARD/ESCALATE/DENY based on complexity and profile.
|
|
509
|
+
On timeout or error, returns DENY (safe-deny default).
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
csdf: CSDF skill definition dict
|
|
513
|
+
profile_name: Agent profile name — MUST be one of: claude-code, codex, hermes, openclaw
|
|
514
|
+
"""
|
|
515
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
516
|
+
try:
|
|
517
|
+
profile = _get_profile(profile_name)
|
|
518
|
+
gate = GateManager(profile=profile)
|
|
519
|
+
result = gate.check(csdf)
|
|
520
|
+
return {
|
|
521
|
+
"decision": str(result.decision),
|
|
522
|
+
"reason": result.reason,
|
|
523
|
+
"complexity_level": result.complexity.level if result.complexity else None,
|
|
524
|
+
"complexity_total": result.complexity.total if result.complexity else None,
|
|
525
|
+
"conditions": result.conditions,
|
|
526
|
+
}
|
|
527
|
+
except Exception as e:
|
|
528
|
+
# Safe-deny: gate_check failure defaults to DENY
|
|
529
|
+
return {
|
|
530
|
+
"decision": "DENY",
|
|
531
|
+
"reason": f"gate_check error (safe-deny): {type(e).__name__}: {e}",
|
|
532
|
+
"complexity_level": None,
|
|
533
|
+
"complexity_total": None,
|
|
534
|
+
"conditions": [],
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@mcp.tool()
|
|
539
|
+
def gate_check_with_policy(
|
|
540
|
+
csdf: dict,
|
|
541
|
+
profile_name: str,
|
|
542
|
+
policy_path: str = "",
|
|
543
|
+
changed_files: Optional[list[str]] = None,
|
|
544
|
+
) -> dict:
|
|
545
|
+
"""Gate check with policy-based 4D phase enforcement.
|
|
546
|
+
|
|
547
|
+
Extends gate_check with gate.policy integration for:
|
|
548
|
+
- Path-based complexity level resolution
|
|
549
|
+
- Incremental mode (git diff changed files)
|
|
550
|
+
- Phase gate artifact validation
|
|
551
|
+
- Emergency bypass awareness
|
|
552
|
+
|
|
553
|
+
On timeout or error, returns DENY (safe-deny default).
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
csdf: CSDF skill definition dict
|
|
557
|
+
profile_name: Agent profile name — MUST be one of: claude-code, codex, hermes, openclaw
|
|
558
|
+
policy_path: Path to gate.policy YAML file
|
|
559
|
+
changed_files: Optional list of changed files for incremental mode
|
|
560
|
+
"""
|
|
561
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
562
|
+
try:
|
|
563
|
+
profile = _get_profile(profile_name)
|
|
564
|
+
gate = GateManager(profile=profile)
|
|
565
|
+
|
|
566
|
+
from pathlib import Path as _Path
|
|
567
|
+
|
|
568
|
+
pp = _Path(policy_path) if policy_path else None
|
|
569
|
+
|
|
570
|
+
result = gate.check_with_policy(
|
|
571
|
+
csdf,
|
|
572
|
+
policy_path=pp,
|
|
573
|
+
changed_files=changed_files,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
state_dict = None
|
|
577
|
+
if result.state:
|
|
578
|
+
state_dict = result.state.model_dump(mode="json")
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
"decision": str(result.decision),
|
|
582
|
+
"reason": result.reason,
|
|
583
|
+
"complexity_level": result.complexity.level if result.complexity else None,
|
|
584
|
+
"complexity_total": result.complexity.total if result.complexity else None,
|
|
585
|
+
"conditions": result.conditions,
|
|
586
|
+
"policy_level": result.policy_level,
|
|
587
|
+
"skip_phases": result.skip_phases,
|
|
588
|
+
"state": state_dict,
|
|
589
|
+
}
|
|
590
|
+
except Exception as e:
|
|
591
|
+
return {
|
|
592
|
+
"decision": "DENY",
|
|
593
|
+
"reason": f"gate_check_with_policy error (safe-deny): {type(e).__name__}: {e}",
|
|
594
|
+
"complexity_level": None,
|
|
595
|
+
"complexity_total": None,
|
|
596
|
+
"conditions": [],
|
|
597
|
+
"policy_level": None,
|
|
598
|
+
"skip_phases": [],
|
|
599
|
+
"state": None,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@mcp.tool()
|
|
604
|
+
def telemetry_report(
|
|
605
|
+
event_type: str,
|
|
606
|
+
skill_id: str,
|
|
607
|
+
channel: str = "hook",
|
|
608
|
+
payload: Optional[dict] = None,
|
|
609
|
+
trace_id: str = "",
|
|
610
|
+
) -> dict:
|
|
611
|
+
"""Report a telemetry event.
|
|
612
|
+
|
|
613
|
+
On failure, silently drops the event and returns error info
|
|
614
|
+
(does not block the caller).
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
event_type: Event type string (e.g., skill_used, skill_error)
|
|
618
|
+
skill_id: Skill ID this event relates to
|
|
619
|
+
channel: Telemetry channel (hook, mcp, log_file)
|
|
620
|
+
payload: Optional event payload dict
|
|
621
|
+
trace_id: Optional W3C trace ID
|
|
622
|
+
"""
|
|
623
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
624
|
+
try:
|
|
625
|
+
bridge = TelemetryBridge()
|
|
626
|
+
event = bridge.emit(
|
|
627
|
+
event_type=event_type,
|
|
628
|
+
skill_id=skill_id,
|
|
629
|
+
channel=channel,
|
|
630
|
+
payload=payload or {},
|
|
631
|
+
trace_id=trace_id,
|
|
632
|
+
)
|
|
633
|
+
return {
|
|
634
|
+
"event_type": event.event_type,
|
|
635
|
+
"skill_id": event.skill_id,
|
|
636
|
+
"channel": str(event.channel),
|
|
637
|
+
"timestamp": event.timestamp,
|
|
638
|
+
}
|
|
639
|
+
except Exception as e:
|
|
640
|
+
# Silent failure — telemetry should not block operations
|
|
641
|
+
return {
|
|
642
|
+
"event_type": event_type,
|
|
643
|
+
"skill_id": skill_id,
|
|
644
|
+
"error": f"telemetry dropped: {type(e).__name__}: {e}",
|
|
645
|
+
"fallback": "event logged locally for retry",
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@mcp.tool()
|
|
650
|
+
def audit_verify() -> dict:
|
|
651
|
+
"""Verify the integrity of the audit hash chain.
|
|
652
|
+
|
|
653
|
+
Returns True if all records form a valid chain, False if tampered.
|
|
654
|
+
On error, returns integrity=False with error details.
|
|
655
|
+
"""
|
|
656
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
657
|
+
try:
|
|
658
|
+
return {
|
|
659
|
+
"integrity": _audit.verify_integrity(),
|
|
660
|
+
"record_count": _audit.get_record_count(),
|
|
661
|
+
}
|
|
662
|
+
except Exception as e:
|
|
663
|
+
return {
|
|
664
|
+
"integrity": False,
|
|
665
|
+
"record_count": 0,
|
|
666
|
+
"error": f"audit_verify error: {type(e).__name__}",
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
# ── Registry tools ──
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
@mcp.tool()
|
|
674
|
+
def skill_register(
|
|
675
|
+
skill_id: str,
|
|
676
|
+
name: str,
|
|
677
|
+
version: str,
|
|
678
|
+
sbom_ref: str = "",
|
|
679
|
+
provenance_ref: str = "",
|
|
680
|
+
source_pin: str = "",
|
|
681
|
+
signature_ref: str = "",
|
|
682
|
+
trace_id: str = "",
|
|
683
|
+
) -> dict:
|
|
684
|
+
"""Register a skill candidate into the Registry.
|
|
685
|
+
|
|
686
|
+
Requires SPDX SBOM, SLSA provenance, source pin, and signature evidence.
|
|
687
|
+
Skill enters 'testing' state (not production-routable).
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
skill_id: Unique skill identifier
|
|
691
|
+
name: Human-readable skill name
|
|
692
|
+
version: Semantic version string
|
|
693
|
+
sbom_ref: SPDX SBOM reference
|
|
694
|
+
provenance_ref: SLSA provenance reference
|
|
695
|
+
source_pin: Source pin (sha256 or version)
|
|
696
|
+
signature_ref: Signature reference
|
|
697
|
+
"""
|
|
698
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
699
|
+
from skillpool.registry.models import RegisterSkillRequest, SkillMetadata
|
|
700
|
+
|
|
701
|
+
meta = SkillMetadata(
|
|
702
|
+
skill_id=skill_id,
|
|
703
|
+
name=name,
|
|
704
|
+
version=version,
|
|
705
|
+
security={
|
|
706
|
+
"sbom_ref": sbom_ref,
|
|
707
|
+
"provenance_ref": provenance_ref,
|
|
708
|
+
"source_pin": source_pin,
|
|
709
|
+
"signature_ref": signature_ref,
|
|
710
|
+
},
|
|
711
|
+
)
|
|
712
|
+
req = RegisterSkillRequest(skill_metadata=meta)
|
|
713
|
+
try:
|
|
714
|
+
resp = _registry.register_candidate(req)
|
|
715
|
+
return {"skill_id": resp.skill_id, "status": resp.status, "audit_ref": resp.audit_ref}
|
|
716
|
+
except Exception as e:
|
|
717
|
+
return {"error": type(e).__name__, "detail": str(e)}
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@mcp.tool()
|
|
721
|
+
def skill_transition(
|
|
722
|
+
skill_id: str,
|
|
723
|
+
from_status: str,
|
|
724
|
+
to_status: str,
|
|
725
|
+
sandbox_result: str = "",
|
|
726
|
+
policy_approval: bool = False,
|
|
727
|
+
trace_id: str = "",
|
|
728
|
+
) -> dict:
|
|
729
|
+
"""Transition a skill's lifecycle state in the Registry.
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
skill_id: Skill to transition
|
|
733
|
+
from_status: Current state (draft/imported/testing/enabled/disabled/deprecated)
|
|
734
|
+
to_status: Target state
|
|
735
|
+
sandbox_result: "pass" required for enabled state
|
|
736
|
+
policy_approval: True required for enabled state
|
|
737
|
+
trace_id: Optional W3C TraceContext trace_id for cross-Agent correlation
|
|
738
|
+
"""
|
|
739
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
740
|
+
from skillpool.registry.models import StateTransitionRequest, SkillStatus
|
|
741
|
+
|
|
742
|
+
try:
|
|
743
|
+
req = StateTransitionRequest(
|
|
744
|
+
from_status=SkillStatus(from_status),
|
|
745
|
+
to_status=SkillStatus(to_status),
|
|
746
|
+
)
|
|
747
|
+
resp = _registry.transition_state(
|
|
748
|
+
skill_id,
|
|
749
|
+
req,
|
|
750
|
+
sandbox_result=sandbox_result or None,
|
|
751
|
+
policy_approval=policy_approval,
|
|
752
|
+
)
|
|
753
|
+
if trace_id and _audit:
|
|
754
|
+
_audit.append(action="skill_transition", object_id=skill_id, result=to_status, trace_id=trace_id)
|
|
755
|
+
return {
|
|
756
|
+
"skill_id": resp.skill_id,
|
|
757
|
+
"from_status": resp.from_status,
|
|
758
|
+
"to_status": resp.to_status,
|
|
759
|
+
"audit_ref": resp.audit_ref,
|
|
760
|
+
"trace_id": trace_id,
|
|
761
|
+
}
|
|
762
|
+
except Exception as e:
|
|
763
|
+
return {"error": type(e).__name__, "detail": str(e)}
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
@mcp.tool()
|
|
767
|
+
def skill_status(skill_id: str) -> dict:
|
|
768
|
+
"""Query a skill's current lifecycle state and metadata.
|
|
769
|
+
|
|
770
|
+
All Agents share the same Registry via SkillPool MCP — any state
|
|
771
|
+
change made by one Agent (via skill_transition) is immediately
|
|
772
|
+
visible to other Agents via this tool.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
skill_id: Skill ID to query.
|
|
776
|
+
"""
|
|
777
|
+
try:
|
|
778
|
+
record = _registry.get_skill(skill_id)
|
|
779
|
+
if record is None:
|
|
780
|
+
return {"skill_id": skill_id, "status": "not_found"}
|
|
781
|
+
return {
|
|
782
|
+
"skill_id": record.metadata.skill_id,
|
|
783
|
+
"name": record.metadata.name,
|
|
784
|
+
"version": record.metadata.version,
|
|
785
|
+
"status": str(record.metadata.status),
|
|
786
|
+
"enabled": _registry.is_enabled(skill_id),
|
|
787
|
+
}
|
|
788
|
+
except Exception as e:
|
|
789
|
+
return {"skill_id": skill_id, "error": type(e).__name__, "detail": str(e)}
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
# ── Evolver tools ──
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
@mcp.tool()
|
|
796
|
+
def evolution_trigger(
|
|
797
|
+
skill_id: str,
|
|
798
|
+
version: str,
|
|
799
|
+
severity: str,
|
|
800
|
+
description: str,
|
|
801
|
+
trace_id: str = "",
|
|
802
|
+
) -> dict:
|
|
803
|
+
"""Record a defect that may trigger skill evolution.
|
|
804
|
+
|
|
805
|
+
Args:
|
|
806
|
+
skill_id: Skill with the defect
|
|
807
|
+
version: Version string
|
|
808
|
+
severity: "critical", "major", or "minor"
|
|
809
|
+
description: Defect description
|
|
810
|
+
"""
|
|
811
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
812
|
+
try:
|
|
813
|
+
defect = _evolver.record_defect(
|
|
814
|
+
skill_id=skill_id,
|
|
815
|
+
version=version,
|
|
816
|
+
severity=DefectSeverity(severity),
|
|
817
|
+
description=description,
|
|
818
|
+
)
|
|
819
|
+
pending = _evolver.get_pending_evolutions()
|
|
820
|
+
return {
|
|
821
|
+
"defect_id": defect.defect_id,
|
|
822
|
+
"severity": defect.severity.value,
|
|
823
|
+
"evolution_queued": len(pending) > 0,
|
|
824
|
+
"pending_evolutions": len(pending),
|
|
825
|
+
}
|
|
826
|
+
except (ValueError, KeyError) as e:
|
|
827
|
+
return {"error": type(e).__name__, "detail": str(e)}
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
@mcp.tool()
|
|
831
|
+
def evolution_proposal(
|
|
832
|
+
reason: str,
|
|
833
|
+
risk: str = "medium",
|
|
834
|
+
trace_id: str = "",
|
|
835
|
+
) -> dict:
|
|
836
|
+
"""Create a recommendation-only evolution proposal.
|
|
837
|
+
|
|
838
|
+
IMPORTANT: This does NOT mutate any Registry state.
|
|
839
|
+
It only creates a proposal for human review.
|
|
840
|
+
|
|
841
|
+
Args:
|
|
842
|
+
reason: Context/reason for the evolution
|
|
843
|
+
risk: Risk level ("low", "medium", "high")
|
|
844
|
+
"""
|
|
845
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
846
|
+
proposal = _evolver.create_proposal(
|
|
847
|
+
context={"reason": reason},
|
|
848
|
+
risk=risk,
|
|
849
|
+
)
|
|
850
|
+
return {
|
|
851
|
+
"proposal_id": proposal.proposal_id,
|
|
852
|
+
"recommendation_only": proposal.recommendation_only,
|
|
853
|
+
"risk": proposal.risk,
|
|
854
|
+
"audit_ref": proposal.audit_ref,
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
# ── Monitor tools ──
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
@mcp.tool()
|
|
862
|
+
def monitor_evaluate(
|
|
863
|
+
skill_id: str,
|
|
864
|
+
error_rate: float = 0.0,
|
|
865
|
+
security_issues: int = 0,
|
|
866
|
+
coverage: float = 0.5,
|
|
867
|
+
doc_completeness: float = 0.5,
|
|
868
|
+
p99_latency_ms: float = 1000.0,
|
|
869
|
+
update_frequency_days: float = 30.0,
|
|
870
|
+
resource_efficiency: float = 0.5,
|
|
871
|
+
) -> dict:
|
|
872
|
+
"""Perform five-dimension evaluation on a skill.
|
|
873
|
+
|
|
874
|
+
Args:
|
|
875
|
+
skill_id: Skill to evaluate
|
|
876
|
+
error_rate: Error rate (0.0-1.0)
|
|
877
|
+
security_issues: Number of security issues
|
|
878
|
+
coverage: Test coverage (0.0-1.0)
|
|
879
|
+
doc_completeness: Documentation completeness (0.0-1.0)
|
|
880
|
+
p99_latency_ms: P99 latency in milliseconds
|
|
881
|
+
update_frequency_days: Days since last update
|
|
882
|
+
resource_efficiency: Resource efficiency (0.0-1.0)
|
|
883
|
+
"""
|
|
884
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
885
|
+
try:
|
|
886
|
+
eval_ = _monitor.evaluate_skill(
|
|
887
|
+
skill_id,
|
|
888
|
+
{
|
|
889
|
+
"error_rate": error_rate,
|
|
890
|
+
"security_issues": security_issues,
|
|
891
|
+
"coverage": coverage,
|
|
892
|
+
"doc_completeness": doc_completeness,
|
|
893
|
+
"p99_latency_ms": p99_latency_ms,
|
|
894
|
+
"update_frequency_days": update_frequency_days,
|
|
895
|
+
"resource_efficiency": resource_efficiency,
|
|
896
|
+
},
|
|
897
|
+
)
|
|
898
|
+
return {
|
|
899
|
+
"skill_id": eval_.skill_id,
|
|
900
|
+
"overall_score": round(eval_.overall_score, 4),
|
|
901
|
+
"safety": {"score": eval_.safety_score, "level": eval_.safety.value},
|
|
902
|
+
"completeness": {"score": eval_.completeness_score, "level": eval_.completeness.value},
|
|
903
|
+
"executability": {"score": eval_.executability_score, "level": eval_.executability.value},
|
|
904
|
+
"maintainability": {"score": eval_.maintainability_score, "level": eval_.maintainability.value},
|
|
905
|
+
"cost_awareness": {"score": eval_.cost_awareness_score, "level": eval_.cost_awareness.value},
|
|
906
|
+
}
|
|
907
|
+
except Exception as e:
|
|
908
|
+
return {
|
|
909
|
+
"skill_id": skill_id,
|
|
910
|
+
"overall_score": 0.0,
|
|
911
|
+
"error": f"monitor_evaluate error: {type(e).__name__}",
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
# ── Health tools ──
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
@mcp.tool()
|
|
919
|
+
def health_check(include_gateway: bool = False) -> dict:
|
|
920
|
+
"""Run health checks on all registered components and return status.
|
|
921
|
+
|
|
922
|
+
On error, returns DEGRADED status.
|
|
923
|
+
|
|
924
|
+
Args:
|
|
925
|
+
include_gateway: If True, also check vMCP Gateway /health endpoint.
|
|
926
|
+
"""
|
|
927
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
928
|
+
try:
|
|
929
|
+
resp = _health.check_health()
|
|
930
|
+
result = {
|
|
931
|
+
"status": str(resp.status),
|
|
932
|
+
"components": [
|
|
933
|
+
{
|
|
934
|
+
"component": c.component,
|
|
935
|
+
"status": str(c.status),
|
|
936
|
+
"critical": c.critical,
|
|
937
|
+
"message": c.message,
|
|
938
|
+
}
|
|
939
|
+
for c in resp.components
|
|
940
|
+
],
|
|
941
|
+
"degradation_level": str(_health.get_degradation_level()),
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if include_gateway:
|
|
945
|
+
try:
|
|
946
|
+
import httpx
|
|
947
|
+
|
|
948
|
+
with httpx.Client(timeout=3.0) as client:
|
|
949
|
+
gw_resp = client.get("http://127.0.0.1:9000/health")
|
|
950
|
+
result["gateway"] = (
|
|
951
|
+
gw_resp.json()
|
|
952
|
+
if gw_resp.status_code == 200
|
|
953
|
+
else {
|
|
954
|
+
"status": "unreachable",
|
|
955
|
+
"http_status": gw_resp.status_code,
|
|
956
|
+
}
|
|
957
|
+
)
|
|
958
|
+
except Exception as e:
|
|
959
|
+
result["gateway"] = {"status": "unreachable", "error": str(e)}
|
|
960
|
+
|
|
961
|
+
return result
|
|
962
|
+
except Exception as e:
|
|
963
|
+
return {
|
|
964
|
+
"status": "DEGRADED",
|
|
965
|
+
"error": f"health_check error: {type(e).__name__}: {e}",
|
|
966
|
+
"components": [],
|
|
967
|
+
"degradation_level": "unknown",
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
# ── Review tool ──
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
@mcp.tool()
|
|
975
|
+
def review_trigger(
|
|
976
|
+
checkpoint: str = "L2",
|
|
977
|
+
skill_ids: Optional[list[str]] = None,
|
|
978
|
+
trace_id: str = "",
|
|
979
|
+
) -> dict:
|
|
980
|
+
"""Trigger a review checkpoint (L1-L4).
|
|
981
|
+
|
|
982
|
+
L1: DocsDD — 7-dim shadow review (non-blocking)
|
|
983
|
+
L2: SDD — 12-dim full review + VETO V1-V6
|
|
984
|
+
L3: BDD — baseline 5-dim + all VETO
|
|
985
|
+
L4: TDD — baseline regression, new blind spots only
|
|
986
|
+
|
|
987
|
+
Args:
|
|
988
|
+
checkpoint: Review level (L1, L2, L3, L4)
|
|
989
|
+
skill_ids: Optional list of skill IDs to review (empty = all)
|
|
990
|
+
"""
|
|
991
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
992
|
+
from skillpool.review import ReviewManager
|
|
993
|
+
from skillpool.review.models import CheckpointLevel, ReviewTrigger, ReviewTriggerRequest
|
|
994
|
+
|
|
995
|
+
try:
|
|
996
|
+
rm = ReviewManager()
|
|
997
|
+
request = ReviewTriggerRequest(
|
|
998
|
+
trigger=ReviewTrigger.MANUAL,
|
|
999
|
+
checkpoint=CheckpointLevel(checkpoint),
|
|
1000
|
+
affected_skills=skill_ids or ["all"],
|
|
1001
|
+
)
|
|
1002
|
+
result = rm.trigger(request)
|
|
1003
|
+
return {
|
|
1004
|
+
"status": result.status.value,
|
|
1005
|
+
"review_id": result.review_id,
|
|
1006
|
+
"checkpoint": checkpoint,
|
|
1007
|
+
"scores": result.scores,
|
|
1008
|
+
"veto_triggered": result.veto_triggered,
|
|
1009
|
+
"veto_details": [
|
|
1010
|
+
{
|
|
1011
|
+
"rule": v.rule.value,
|
|
1012
|
+
"dimension": v.dimension,
|
|
1013
|
+
"score": v.score,
|
|
1014
|
+
"threshold": v.threshold,
|
|
1015
|
+
"blocks": v.blocks,
|
|
1016
|
+
"decision": "block" if v.blocks else "risk_notice",
|
|
1017
|
+
"reason": v.recommendation,
|
|
1018
|
+
}
|
|
1019
|
+
for v in result.veto_details
|
|
1020
|
+
],
|
|
1021
|
+
"suspect_skills": [
|
|
1022
|
+
{"skill_id": s.skill_id, "reason": s.reason, "dimension": s.suspected_dimension}
|
|
1023
|
+
for s in result.suspect_skills
|
|
1024
|
+
],
|
|
1025
|
+
"recommendation": result.recommendation.value,
|
|
1026
|
+
"duration_ms": result.duration_ms,
|
|
1027
|
+
}
|
|
1028
|
+
except Exception as e:
|
|
1029
|
+
return {"error": type(e).__name__, "detail": str(e)}
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
@mcp.tool()
|
|
1033
|
+
def security_scan(content: str, skill_id: str = "") -> dict:
|
|
1034
|
+
"""Scan skill content for security threats before materialization.
|
|
1035
|
+
|
|
1036
|
+
Runs YAML safety checks, dangerous pattern scanning, and signature
|
|
1037
|
+
verification (placeholder). Returns threat level and details.
|
|
1038
|
+
|
|
1039
|
+
Args:
|
|
1040
|
+
content: The skill content (YAML or SKILL.md) to scan.
|
|
1041
|
+
skill_id: Optional skill ID for context (used in warnings).
|
|
1042
|
+
"""
|
|
1043
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1044
|
+
try:
|
|
1045
|
+
result = _security_scanner.full_check(content)
|
|
1046
|
+
return {
|
|
1047
|
+
"skill_id": skill_id,
|
|
1048
|
+
"threat_level": result.threat_level.value,
|
|
1049
|
+
"is_safe": result.is_safe,
|
|
1050
|
+
"checks_passed": result.checks_passed,
|
|
1051
|
+
"warnings": result.warnings,
|
|
1052
|
+
"blockers": result.blockers,
|
|
1053
|
+
}
|
|
1054
|
+
except Exception as e:
|
|
1055
|
+
return {
|
|
1056
|
+
"skill_id": skill_id,
|
|
1057
|
+
"threat_level": "critical",
|
|
1058
|
+
"is_safe": False,
|
|
1059
|
+
"error": f"security_scan error: {type(e).__name__}",
|
|
1060
|
+
"checks_passed": [],
|
|
1061
|
+
"warnings": [],
|
|
1062
|
+
"blockers": ["Scan failed: internal error"],
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
@mcp.tool()
|
|
1067
|
+
def healing_scan() -> dict:
|
|
1068
|
+
"""Scan BugCollector for recurring defects and propose self-healing evolutions.
|
|
1069
|
+
|
|
1070
|
+
Groups bugs by (skill_id, defect_type), applies trigger thresholds,
|
|
1071
|
+
and returns proposed healing actions. Does NOT execute any changes.
|
|
1072
|
+
|
|
1073
|
+
Trigger thresholds:
|
|
1074
|
+
- >=3 P2 bugs -> PATCH (auto)
|
|
1075
|
+
- >=1 P1 or >=5 P2 -> MINOR (auto + notify)
|
|
1076
|
+
- >=1 P0 -> MAJOR (needs_human, must NOT auto-execute)
|
|
1077
|
+
"""
|
|
1078
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1079
|
+
try:
|
|
1080
|
+
proposals = _self_healing.scan_and_propose()
|
|
1081
|
+
return {
|
|
1082
|
+
"proposals": proposals,
|
|
1083
|
+
"total_proposals": len(proposals),
|
|
1084
|
+
"status": "scanned",
|
|
1085
|
+
}
|
|
1086
|
+
except Exception as e:
|
|
1087
|
+
return {
|
|
1088
|
+
"proposals": [],
|
|
1089
|
+
"total_proposals": 0,
|
|
1090
|
+
"status": "error",
|
|
1091
|
+
"error": f"healing_scan error: {type(e).__name__}: {e}",
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
@mcp.tool()
|
|
1096
|
+
def healing_execute(proposal_id: str) -> dict:
|
|
1097
|
+
"""Execute a proposed self-healing evolution with BDD verification.
|
|
1098
|
+
|
|
1099
|
+
Steps:
|
|
1100
|
+
1. Validate proposal exists and is in PROPOSED state
|
|
1101
|
+
2. MAJOR proposals require human approval (return needs_human)
|
|
1102
|
+
3. Run BDD verification (check bug count decreased)
|
|
1103
|
+
4. If verification fails -> auto-rollback
|
|
1104
|
+
|
|
1105
|
+
Args:
|
|
1106
|
+
proposal_id: The healing proposal ID to execute (from healing_scan).
|
|
1107
|
+
"""
|
|
1108
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1109
|
+
try:
|
|
1110
|
+
result = _self_healing.execute_healing(proposal_id)
|
|
1111
|
+
return result
|
|
1112
|
+
except Exception as e:
|
|
1113
|
+
return {
|
|
1114
|
+
"proposal_id": proposal_id,
|
|
1115
|
+
"status": "error",
|
|
1116
|
+
"error": f"healing_execute error: {type(e).__name__}: {e}",
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
1121
|
+
# PROMPTS — User-controlled, templated skill invocation
|
|
1122
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
@mcp.prompt()
|
|
1126
|
+
def skill_context(skill_id: str) -> str:
|
|
1127
|
+
"""Inject skill context into the conversation.
|
|
1128
|
+
|
|
1129
|
+
Use this to load a specific skill's definition and checklist
|
|
1130
|
+
as context for the current task.
|
|
1131
|
+
|
|
1132
|
+
Args:
|
|
1133
|
+
skill_id: Skill ID to load (e.g., S09, S13a)
|
|
1134
|
+
"""
|
|
1135
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1136
|
+
definition = skill_definition(skill_id)
|
|
1137
|
+
manifest = skill_manifest(skill_id)
|
|
1138
|
+
|
|
1139
|
+
deps = manifest.get("dependencies", [])
|
|
1140
|
+
deps_str = ", ".join(deps) if deps else "none"
|
|
1141
|
+
|
|
1142
|
+
return f"""# Skill: {skill_id}
|
|
1143
|
+
|
|
1144
|
+
## Dependencies
|
|
1145
|
+
{deps_str}
|
|
1146
|
+
|
|
1147
|
+
## Definition
|
|
1148
|
+
{definition}
|
|
1149
|
+
"""
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
@mcp.prompt()
|
|
1153
|
+
def trigger_review() -> str:
|
|
1154
|
+
"""Trigger a multi-dimension review of the current work.
|
|
1155
|
+
|
|
1156
|
+
Evaluates across 12 dimensions (D1-D12) with VETO rules V1-V6.
|
|
1157
|
+
Uses SkillPool MCP Resources for dynamic content — no hardcoded paths.
|
|
1158
|
+
"""
|
|
1159
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1160
|
+
return """Execute a Multi-Dimension Review V9.0 on the current work.
|
|
1161
|
+
|
|
1162
|
+
Steps:
|
|
1163
|
+
1. Read skill://multi-dim-review/definition for full rules and rubric
|
|
1164
|
+
2. Read skill://multi-dim-review/manifest.yaml for dependencies and veto rules
|
|
1165
|
+
3. Score all 12 dimensions per V9.0 rubric
|
|
1166
|
+
4. Check VETO rules V1-V6
|
|
1167
|
+
5. Collect blind spots with severity (P0/P1/P2)
|
|
1168
|
+
6. Determine if any skill upgrades are triggered
|
|
1169
|
+
7. Write blind spots to blindspots/ directory and ClawMem
|
|
1170
|
+
|
|
1171
|
+
Fallback: If MCP Resources are unavailable, read local files at
|
|
1172
|
+
~/.skillpool/skills/multi-dim-review/RULES.md and state.yaml
|
|
1173
|
+
"""
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
@mcp.prompt()
|
|
1177
|
+
def gate_status(skill_id: str, agent_type: str = "claude-code") -> str:
|
|
1178
|
+
"""Check gate status for a skill.
|
|
1179
|
+
|
|
1180
|
+
Returns whether the skill is ALLOWED, GUARDED, ESCALATED, or DENIED.
|
|
1181
|
+
|
|
1182
|
+
Args:
|
|
1183
|
+
skill_id: Skill ID to check
|
|
1184
|
+
"""
|
|
1185
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1186
|
+
csdf = _load_csdf(skill_id)
|
|
1187
|
+
if csdf is None:
|
|
1188
|
+
return f"Skill not found: {skill_id}"
|
|
1189
|
+
|
|
1190
|
+
profile = _get_profile(agent_type)
|
|
1191
|
+
gate = GateManager(profile=profile)
|
|
1192
|
+
result = gate.check(csdf)
|
|
1193
|
+
|
|
1194
|
+
return f"""Gate Check Result for {skill_id}:
|
|
1195
|
+
- Decision: {result.decision}
|
|
1196
|
+
- Reason: {result.reason}
|
|
1197
|
+
- Complexity Level: {result.complexity.level if result.complexity else "N/A"}
|
|
1198
|
+
- Complexity Total: {result.complexity.total if result.complexity else "N/A"}
|
|
1199
|
+
"""
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
1203
|
+
# MCP Tools — V4.1 补齐(match / report_usage / assess_paradigm / get_emergency_overrides)
|
|
1204
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
@mcp.tool()
|
|
1208
|
+
def skill_search(
|
|
1209
|
+
intent: str,
|
|
1210
|
+
agent_type: str,
|
|
1211
|
+
top_k: int = 5,
|
|
1212
|
+
include_lifecycle: bool = True,
|
|
1213
|
+
) -> dict:
|
|
1214
|
+
"""Search for optimal skill combinations for a given intent.
|
|
1215
|
+
|
|
1216
|
+
This is the MANDATORY first step before accessing any skill definition.
|
|
1217
|
+
Uses four-layer routing (semantic + logical + causal + predictive) to
|
|
1218
|
+
find the best skill combination for the described intent.
|
|
1219
|
+
|
|
1220
|
+
Layer 1 (Semantic): BGE-M3 embedding similarity via Ollama
|
|
1221
|
+
Layer 2 (Logical): DAG dependencies + synergy edges from CSDF
|
|
1222
|
+
Layer 3 (Causal): Historical combination gain data (Thompson Sampling)
|
|
1223
|
+
Layer 4 (Predictive): Collaborative filtering + gain decay
|
|
1224
|
+
|
|
1225
|
+
Returns ranked skill candidates with combination recommendations
|
|
1226
|
+
and lifecycle state information.
|
|
1227
|
+
|
|
1228
|
+
Args:
|
|
1229
|
+
intent: Natural language description of what you want to do.
|
|
1230
|
+
agent_type: Agent profile name for capability filtering.
|
|
1231
|
+
top_k: Maximum number of candidates to return (1-10).
|
|
1232
|
+
include_lifecycle: If True, include combination lifecycle state.
|
|
1233
|
+
"""
|
|
1234
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1235
|
+
from skillpool.router import IntentRouter
|
|
1236
|
+
from skillpool.synergy import SynergyDetector
|
|
1237
|
+
|
|
1238
|
+
top_k = max(1, min(top_k, 10))
|
|
1239
|
+
|
|
1240
|
+
# L1+L2: Intent routing (semantic + logical)
|
|
1241
|
+
intent_router = IntentRouter(skills_dir=_SKILLS_DIR)
|
|
1242
|
+
routing_result = intent_router.route(intent, top_k=top_k)
|
|
1243
|
+
|
|
1244
|
+
primary = routing_result.primary
|
|
1245
|
+
result = {
|
|
1246
|
+
"intent": intent,
|
|
1247
|
+
"agent": agent_type,
|
|
1248
|
+
"layers_used": routing_result.layers_used,
|
|
1249
|
+
"primary_skill": {
|
|
1250
|
+
"id": primary.skill_id if primary else None,
|
|
1251
|
+
"score": primary.score if primary else 0,
|
|
1252
|
+
"layer": primary.layer if primary else "none",
|
|
1253
|
+
"reason": primary.reason if primary else "",
|
|
1254
|
+
}
|
|
1255
|
+
if primary
|
|
1256
|
+
else None,
|
|
1257
|
+
"candidates": [
|
|
1258
|
+
{"id": c.skill_id, "score": c.score, "layer": c.layer, "reason": c.reason[:100]}
|
|
1259
|
+
for c in routing_result.candidates[:top_k]
|
|
1260
|
+
],
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
# Include combination recommendations
|
|
1264
|
+
if routing_result.enhancers:
|
|
1265
|
+
result["recommended_combinations"] = [
|
|
1266
|
+
{
|
|
1267
|
+
"primary": primary.skill_id if primary else "",
|
|
1268
|
+
"enhancer": e.skill_id,
|
|
1269
|
+
"gain": e.gain,
|
|
1270
|
+
"reason": e.reason[:100],
|
|
1271
|
+
"score": e.score,
|
|
1272
|
+
}
|
|
1273
|
+
for e in routing_result.enhancers[:5]
|
|
1274
|
+
]
|
|
1275
|
+
|
|
1276
|
+
# Add synergy data from CSDF
|
|
1277
|
+
if primary:
|
|
1278
|
+
detector = SynergyDetector(skills_dir=_SKILLS_DIR)
|
|
1279
|
+
detector.load_expert_synergies()
|
|
1280
|
+
synergies = detector.get_synergies_for(primary.skill_id)
|
|
1281
|
+
if synergies:
|
|
1282
|
+
result["expert_synergies"] = [
|
|
1283
|
+
{"skill_id": s.target, "gain": s.gain, "reason": s.reason, "weight": s.weight} for s in synergies
|
|
1284
|
+
]
|
|
1285
|
+
|
|
1286
|
+
# Include combination lifecycle data
|
|
1287
|
+
if include_lifecycle:
|
|
1288
|
+
from skillpool.combiner import CombinationLifecycleManager
|
|
1289
|
+
|
|
1290
|
+
lifecycle_mgr = CombinationLifecycleManager()
|
|
1291
|
+
|
|
1292
|
+
# Get promoted combinations for the primary skill
|
|
1293
|
+
if primary:
|
|
1294
|
+
promoted = lifecycle_mgr.get_promoted_combinations(primary.skill_id)
|
|
1295
|
+
if promoted:
|
|
1296
|
+
result["active_combinations"] = [
|
|
1297
|
+
{
|
|
1298
|
+
"combination_id": c.combination_id,
|
|
1299
|
+
"enhancers": c.enhancers,
|
|
1300
|
+
"state": c.state.name,
|
|
1301
|
+
"gain_avg": round(c.gain_avg, 2),
|
|
1302
|
+
"weight": round(c.current_weight(), 3),
|
|
1303
|
+
"source": c.source,
|
|
1304
|
+
}
|
|
1305
|
+
for c in promoted
|
|
1306
|
+
]
|
|
1307
|
+
|
|
1308
|
+
# Get validating combinations (candidates being evaluated)
|
|
1309
|
+
validating = lifecycle_mgr.get_validating_combinations()
|
|
1310
|
+
if validating:
|
|
1311
|
+
result["validating_combinations"] = [
|
|
1312
|
+
{
|
|
1313
|
+
"combination_id": c.combination_id,
|
|
1314
|
+
"primary": c.primary,
|
|
1315
|
+
"enhancers": c.enhancers,
|
|
1316
|
+
"execution_count": c.execution_count,
|
|
1317
|
+
"gain_avg": round(c.gain_avg, 2),
|
|
1318
|
+
"confidence": round(c.gain_confidence, 2),
|
|
1319
|
+
}
|
|
1320
|
+
for c in validating[:5]
|
|
1321
|
+
]
|
|
1322
|
+
|
|
1323
|
+
# Mark search as done for this caller (enables direct skill access)
|
|
1324
|
+
# Agent-neutral: uses agent_type as caller identifier, not Claude Code session ID
|
|
1325
|
+
_search_done_callers.add(agent_type)
|
|
1326
|
+
|
|
1327
|
+
return result
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
@mcp.tool()
|
|
1331
|
+
def skill_get(
|
|
1332
|
+
skill_id: str,
|
|
1333
|
+
agent_type: str,
|
|
1334
|
+
detail: str = "definition",
|
|
1335
|
+
) -> dict:
|
|
1336
|
+
"""Get skill content with search-first enforcement.
|
|
1337
|
+
|
|
1338
|
+
This is a model-controlled Tool wrapper around skill Resources.
|
|
1339
|
+
It enforces that skill_search must be called before accessing
|
|
1340
|
+
skill content — applies to ALL Agents equally via MCP server logic.
|
|
1341
|
+
|
|
1342
|
+
If skill_search has not been called for this agent_type yet,
|
|
1343
|
+
returns a guidance message instead of skill content.
|
|
1344
|
+
|
|
1345
|
+
Args:
|
|
1346
|
+
skill_id: Skill ID to retrieve.
|
|
1347
|
+
agent_type: Agent identifier for search-first tracking.
|
|
1348
|
+
detail: Level of detail: "summary" (~200 tokens), "definition" (full), "manifest" (deps only).
|
|
1349
|
+
"""
|
|
1350
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1351
|
+
|
|
1352
|
+
# Search-first enforcement: Agent-neutral, no Claude Code dependencies
|
|
1353
|
+
if agent_type not in _search_done_callers:
|
|
1354
|
+
return {
|
|
1355
|
+
"error": "search_required",
|
|
1356
|
+
"message": "Please call skill_search(intent=...) first to find the optimal skill combination. "
|
|
1357
|
+
"Direct skill access is blocked until you search for the best match.",
|
|
1358
|
+
"skill_id": skill_id,
|
|
1359
|
+
"agent_type": agent_type,
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
# Delegate to the appropriate Resource function
|
|
1363
|
+
if detail == "summary":
|
|
1364
|
+
data = _lazy_loader.load(skill_id, tier="L1")
|
|
1365
|
+
return {"skill_id": skill_id, "detail": "summary", "data": data}
|
|
1366
|
+
elif detail == "manifest":
|
|
1367
|
+
csdf = _load_csdf(skill_id)
|
|
1368
|
+
if csdf is None:
|
|
1369
|
+
return {"error": f"Skill not found: {skill_id}"}
|
|
1370
|
+
return {
|
|
1371
|
+
"skill_id": skill_id,
|
|
1372
|
+
"detail": "manifest",
|
|
1373
|
+
"data": {
|
|
1374
|
+
"id": csdf.get("id", skill_id),
|
|
1375
|
+
"version": csdf.get("version", ""),
|
|
1376
|
+
"dependencies": csdf.get("dependencies", []),
|
|
1377
|
+
"synergies": csdf.get("synergies", []),
|
|
1378
|
+
"values": csdf.get("values", {}),
|
|
1379
|
+
},
|
|
1380
|
+
}
|
|
1381
|
+
else: # definition
|
|
1382
|
+
try:
|
|
1383
|
+
data = _lazy_loader.load(skill_id, tier="L2")
|
|
1384
|
+
if "markdown" in data and data["markdown"]:
|
|
1385
|
+
content = data["markdown"]
|
|
1386
|
+
elif "_markdown_body" in data and data["_markdown_body"]:
|
|
1387
|
+
content = data["_markdown_body"]
|
|
1388
|
+
else:
|
|
1389
|
+
content = f"Content unavailable (errors: {data.get('_materialization_errors', [])})"
|
|
1390
|
+
return {"skill_id": skill_id, "detail": "definition", "content": content}
|
|
1391
|
+
except ValueError:
|
|
1392
|
+
return {"error": f"Skill not found: {skill_id}"}
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
@mcp.tool()
|
|
1396
|
+
def skill_match(task_description: str, agent_type: str, include_combinations: bool = True) -> dict:
|
|
1397
|
+
"""Match skills to a task description using IntentRouter + Resolver DAG traversal.
|
|
1398
|
+
|
|
1399
|
+
V4.3 Phase 9 upgrade: Uses IntentRouter (L1 semantic + L2 logical routing)
|
|
1400
|
+
to find optimal skill combinations, including enhancers from synergy data.
|
|
1401
|
+
|
|
1402
|
+
Args:
|
|
1403
|
+
task_description: Description of the task to match skills for.
|
|
1404
|
+
agent_type: Agent profile name for capability filtering.
|
|
1405
|
+
include_combinations: If True, also recommend skill combinations with gain data.
|
|
1406
|
+
"""
|
|
1407
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1408
|
+
from skillpool.resolver import SkillResolver, SkillResolveRequest
|
|
1409
|
+
from skillpool.router import IntentRouter
|
|
1410
|
+
|
|
1411
|
+
_profile = _get_profile(agent_type)
|
|
1412
|
+
|
|
1413
|
+
# L1+L2: Intent routing (semantic + logical)
|
|
1414
|
+
intent_router = IntentRouter(skills_dir=_SKILLS_DIR)
|
|
1415
|
+
routing_result = intent_router.route(task_description, top_k=5)
|
|
1416
|
+
|
|
1417
|
+
# Resolver DAG traversal for dependency resolution
|
|
1418
|
+
resolver = SkillResolver()
|
|
1419
|
+
skill_ids = []
|
|
1420
|
+
if _SKILLS_DIR.exists():
|
|
1421
|
+
for yaml_file in sorted(_SKILLS_DIR.glob("*.yaml")):
|
|
1422
|
+
if yaml_file.name == "skill_graph.yaml":
|
|
1423
|
+
continue
|
|
1424
|
+
skill_id = yaml_file.stem.split("-")[0]
|
|
1425
|
+
csdf = _load_csdf(skill_id)
|
|
1426
|
+
if csdf:
|
|
1427
|
+
skill_ids.append(skill_id)
|
|
1428
|
+
|
|
1429
|
+
request = SkillResolveRequest(
|
|
1430
|
+
skill_ids=skill_ids or ["S00"],
|
|
1431
|
+
task_description=task_description,
|
|
1432
|
+
)
|
|
1433
|
+
response = resolver.resolve(request)
|
|
1434
|
+
matches = response.resolved if hasattr(response, "resolved") else []
|
|
1435
|
+
|
|
1436
|
+
# Combine results
|
|
1437
|
+
primary = routing_result.primary
|
|
1438
|
+
result = {
|
|
1439
|
+
"task": task_description,
|
|
1440
|
+
"agent": agent_type,
|
|
1441
|
+
"layers_used": routing_result.layers_used,
|
|
1442
|
+
"primary_skill": {
|
|
1443
|
+
"id": primary.skill_id if primary else None,
|
|
1444
|
+
"score": primary.score if primary else 0,
|
|
1445
|
+
"layer": primary.layer if primary else "none",
|
|
1446
|
+
"reason": primary.reason if primary else "",
|
|
1447
|
+
}
|
|
1448
|
+
if primary
|
|
1449
|
+
else None,
|
|
1450
|
+
"skill_candidates": [
|
|
1451
|
+
{"id": c.skill_id, "score": c.score, "layer": c.layer, "reason": c.reason[:100]}
|
|
1452
|
+
for c in routing_result.candidates[:10]
|
|
1453
|
+
],
|
|
1454
|
+
"dag_matches": [{"id": m.skill_id, "name": m.name, "score": m.score} for m in matches[:10]],
|
|
1455
|
+
"total_candidates": len(skill_ids),
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
# Include combination recommendations if requested
|
|
1459
|
+
if include_combinations and routing_result.enhancers:
|
|
1460
|
+
# Load lifecycle data for combination state info
|
|
1461
|
+
combo_lifecycle_states: dict[str, str] = {}
|
|
1462
|
+
try:
|
|
1463
|
+
from skillpool.combiner import CombinationLifecycleManager
|
|
1464
|
+
from skillpool.combiner.models import CombinationLifecycleState
|
|
1465
|
+
|
|
1466
|
+
lifecycle_mgr = CombinationLifecycleManager()
|
|
1467
|
+
for e in routing_result.enhancers[:5]:
|
|
1468
|
+
combos = lifecycle_mgr.get_combinations_for_skill(e.skill_id)
|
|
1469
|
+
if combos:
|
|
1470
|
+
best = max(combos, key=lambda c: c.current_weight())
|
|
1471
|
+
combo_lifecycle_states[e.skill_id] = CombinationLifecycleState(best.state).name
|
|
1472
|
+
except Exception as e:
|
|
1473
|
+
logger.warning("Failed to load combination lifecycle states: %s", e)
|
|
1474
|
+
|
|
1475
|
+
result["combinations"] = [
|
|
1476
|
+
{
|
|
1477
|
+
"primary": primary.skill_id if primary else "",
|
|
1478
|
+
"enhancer": e.skill_id,
|
|
1479
|
+
"gain": e.gain,
|
|
1480
|
+
"reason": e.reason[:100],
|
|
1481
|
+
"score": e.score,
|
|
1482
|
+
"lifecycle_state": combo_lifecycle_states.get(e.skill_id, ""),
|
|
1483
|
+
}
|
|
1484
|
+
for e in routing_result.enhancers[:5]
|
|
1485
|
+
]
|
|
1486
|
+
|
|
1487
|
+
# Add synergy data from CSDF
|
|
1488
|
+
if primary and include_combinations:
|
|
1489
|
+
from skillpool.synergy import SynergyDetector
|
|
1490
|
+
|
|
1491
|
+
detector = SynergyDetector(skills_dir=_SKILLS_DIR)
|
|
1492
|
+
detector.load_expert_synergies()
|
|
1493
|
+
synergies = detector.get_synergies_for(primary.skill_id)
|
|
1494
|
+
if synergies:
|
|
1495
|
+
result["expert_synergies"] = [
|
|
1496
|
+
{"skill_id": s.target, "gain": s.gain, "reason": s.reason, "weight": s.weight} for s in synergies
|
|
1497
|
+
]
|
|
1498
|
+
|
|
1499
|
+
return result
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
@mcp.tool()
|
|
1503
|
+
def report_usage(
|
|
1504
|
+
skill_name: str,
|
|
1505
|
+
session_id: str,
|
|
1506
|
+
agent_type: str,
|
|
1507
|
+
duration_ms: int = 0,
|
|
1508
|
+
result: str = "success",
|
|
1509
|
+
combination_skills: Optional[list[str]] = None,
|
|
1510
|
+
effectiveness: float = 0.0,
|
|
1511
|
+
efficiency: float = 0.0,
|
|
1512
|
+
quality: float = 0.0,
|
|
1513
|
+
gain: float = 0.0,
|
|
1514
|
+
intent: str = "",
|
|
1515
|
+
) -> dict:
|
|
1516
|
+
"""Report skill usage with optional combination data and four-dimension scores.
|
|
1517
|
+
|
|
1518
|
+
V4.3 Phase 9 upgrade: Supports reporting skill combinations and gain data.
|
|
1519
|
+
Implicit tracking is zero-burden — only skill_name + session_id required.
|
|
1520
|
+
Explicit scoring is optional — provide four-dimension scores for richer data.
|
|
1521
|
+
|
|
1522
|
+
Args:
|
|
1523
|
+
skill_name: Name of the primary skill that was used.
|
|
1524
|
+
session_id: Session identifier for correlation.
|
|
1525
|
+
duration_ms: Duration of skill execution in milliseconds.
|
|
1526
|
+
result: Execution result status (success/partial/failed).
|
|
1527
|
+
agent_type: Agent profile name.
|
|
1528
|
+
combination_skills: Other skills used alongside the primary skill.
|
|
1529
|
+
effectiveness: Task goal achievement (0-10, optional).
|
|
1530
|
+
efficiency: Resource consumption reasonableness (0-10, optional).
|
|
1531
|
+
quality: Output sustainability (0-10, optional).
|
|
1532
|
+
gain: Combination marginal contribution (-10 to +10, optional).
|
|
1533
|
+
intent: Original agent intent description (optional).
|
|
1534
|
+
"""
|
|
1535
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1536
|
+
|
|
1537
|
+
# 1. Record telemetry event
|
|
1538
|
+
payload = {
|
|
1539
|
+
"session_id": session_id,
|
|
1540
|
+
"duration_ms": duration_ms,
|
|
1541
|
+
"result": result,
|
|
1542
|
+
"agent_type": agent_type,
|
|
1543
|
+
}
|
|
1544
|
+
if combination_skills:
|
|
1545
|
+
payload["combination_skills"] = combination_skills
|
|
1546
|
+
payload["combination"] = f"{skill_name}+{','.join(combination_skills)}"
|
|
1547
|
+
if intent:
|
|
1548
|
+
payload["intent"] = intent
|
|
1549
|
+
|
|
1550
|
+
tel_result = telemetry_report(
|
|
1551
|
+
event_type="usage",
|
|
1552
|
+
skill_id=skill_name,
|
|
1553
|
+
channel="mcp",
|
|
1554
|
+
payload=payload,
|
|
1555
|
+
)
|
|
1556
|
+
|
|
1557
|
+
# 2. Record gain data if scores provided
|
|
1558
|
+
gain_recorded = False
|
|
1559
|
+
if effectiveness > 0 or efficiency > 0 or quality > 0 or gain != 0:
|
|
1560
|
+
from skillpool.gain import GainTracker, SkillExecution, GainScores
|
|
1561
|
+
|
|
1562
|
+
tracker = GainTracker()
|
|
1563
|
+
|
|
1564
|
+
skill_ids = [skill_name]
|
|
1565
|
+
if combination_skills:
|
|
1566
|
+
skill_ids.extend(combination_skills)
|
|
1567
|
+
|
|
1568
|
+
scores = GainScores(
|
|
1569
|
+
effectiveness=effectiveness,
|
|
1570
|
+
efficiency=efficiency,
|
|
1571
|
+
quality=quality,
|
|
1572
|
+
gain=gain,
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
source = "explicit" if effectiveness > 0 else "implicit"
|
|
1576
|
+
tracker.record(
|
|
1577
|
+
SkillExecution(
|
|
1578
|
+
skill_ids=skill_ids,
|
|
1579
|
+
intent=intent,
|
|
1580
|
+
scores=scores,
|
|
1581
|
+
duration_ms=duration_ms,
|
|
1582
|
+
source=source,
|
|
1583
|
+
)
|
|
1584
|
+
)
|
|
1585
|
+
gain_recorded = True
|
|
1586
|
+
|
|
1587
|
+
# 3. Update combination lifecycle if combination_skills provided
|
|
1588
|
+
lifecycle_updated = False
|
|
1589
|
+
_lifecycle_state = ""
|
|
1590
|
+
if combination_skills:
|
|
1591
|
+
from skillpool.combiner import CombinationLifecycleManager
|
|
1592
|
+
from skillpool.combiner.models import CombinationLifecycleState
|
|
1593
|
+
|
|
1594
|
+
lifecycle_mgr = CombinationLifecycleManager()
|
|
1595
|
+
|
|
1596
|
+
# Record execution for the combination
|
|
1597
|
+
combo_id = f"{skill_name}+{'+'.join(sorted(combination_skills))}"
|
|
1598
|
+
combo = lifecycle_mgr.record_execution(
|
|
1599
|
+
combo_id,
|
|
1600
|
+
gain=gain,
|
|
1601
|
+
success=(result == "success"),
|
|
1602
|
+
)
|
|
1603
|
+
|
|
1604
|
+
if combo:
|
|
1605
|
+
_lifecycle_state = CombinationLifecycleState(combo.state).name
|
|
1606
|
+
|
|
1607
|
+
# DISCOVERED → VALIDATING: first execution triggers validation
|
|
1608
|
+
if combo.state == CombinationLifecycleState.DISCOVERED:
|
|
1609
|
+
transition_result = lifecycle_mgr.transition(
|
|
1610
|
+
combo_id,
|
|
1611
|
+
CombinationLifecycleState.VALIDATING,
|
|
1612
|
+
)
|
|
1613
|
+
if transition_result.success:
|
|
1614
|
+
lifecycle_updated = True
|
|
1615
|
+
_lifecycle_state = "VALIDATING"
|
|
1616
|
+
|
|
1617
|
+
# VALIDATING → PROMOTED: try promotion after enough executions
|
|
1618
|
+
if combo.state == CombinationLifecycleState.VALIDATING:
|
|
1619
|
+
promote_result = lifecycle_mgr.try_promote(combo_id)
|
|
1620
|
+
if promote_result.success:
|
|
1621
|
+
lifecycle_updated = True
|
|
1622
|
+
_lifecycle_state = "PROMOTED"
|
|
1623
|
+
|
|
1624
|
+
# If combination doesn't exist yet, create it
|
|
1625
|
+
if combo is None:
|
|
1626
|
+
lifecycle_mgr.create_combination(
|
|
1627
|
+
primary=skill_name,
|
|
1628
|
+
enhancers=combination_skills,
|
|
1629
|
+
source="auto_discovered",
|
|
1630
|
+
)
|
|
1631
|
+
_lifecycle_state = "DISCOVERED"
|
|
1632
|
+
|
|
1633
|
+
return {
|
|
1634
|
+
"skill_name": skill_name,
|
|
1635
|
+
"session_id": session_id,
|
|
1636
|
+
"telemetry": tel_result,
|
|
1637
|
+
"gain_recorded": gain_recorded,
|
|
1638
|
+
"combination_count": len(combination_skills) if combination_skills else 0,
|
|
1639
|
+
"lifecycle_updated": lifecycle_updated,
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
|
|
1643
|
+
@mcp.tool()
|
|
1644
|
+
def assess_paradigm(paradigm: str, agent_type: str, skill_id: str = "") -> dict:
|
|
1645
|
+
"""Assess whether an agent can execute a given paradigm.
|
|
1646
|
+
|
|
1647
|
+
Checks agent profile capabilities and trust level against paradigm requirements.
|
|
1648
|
+
|
|
1649
|
+
Args:
|
|
1650
|
+
paradigm: Paradigm to assess (docsdd/sdd/bdd/tdd or review/code/test/planning).
|
|
1651
|
+
skill_id: Optional skill ID for context-specific assessment.
|
|
1652
|
+
agent_type: Agent profile name.
|
|
1653
|
+
"""
|
|
1654
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1655
|
+
from skillpool.paradigm import ParadigmRegistry
|
|
1656
|
+
|
|
1657
|
+
profile = _get_profile(agent_type)
|
|
1658
|
+
registry = ParadigmRegistry()
|
|
1659
|
+
|
|
1660
|
+
# Check if paradigm exists
|
|
1661
|
+
paradigms = registry.list_paradigms() if hasattr(registry, "list_paradigms") else []
|
|
1662
|
+
paradigm_exists = paradigm in {p.get("paradigm", "") for p in paradigms}
|
|
1663
|
+
|
|
1664
|
+
# Check agent compatibility
|
|
1665
|
+
can_execute, reason = profile.can_execute({"paradigm": paradigm})
|
|
1666
|
+
|
|
1667
|
+
return {
|
|
1668
|
+
"paradigm": paradigm,
|
|
1669
|
+
"agent_type": agent_type,
|
|
1670
|
+
"paradigm_registered": paradigm_exists,
|
|
1671
|
+
"can_execute": can_execute,
|
|
1672
|
+
"reason": reason,
|
|
1673
|
+
"trust_level": profile.trust_level,
|
|
1674
|
+
"capabilities": sorted(profile.required_capabilities),
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
@mcp.tool()
|
|
1679
|
+
def combination_create(
|
|
1680
|
+
primary: str,
|
|
1681
|
+
enhancers: list[str],
|
|
1682
|
+
agent_type: str,
|
|
1683
|
+
source: str = "human_specified",
|
|
1684
|
+
) -> dict:
|
|
1685
|
+
"""Create a new skill combination via MCP.
|
|
1686
|
+
|
|
1687
|
+
Human-specified combinations skip DISCOVERED, enter VALIDATING directly.
|
|
1688
|
+
Auto-discovered combinations start at DISCOVERED.
|
|
1689
|
+
ALL Agents can create combinations — this is MCP-level, agent-neutral.
|
|
1690
|
+
|
|
1691
|
+
Args:
|
|
1692
|
+
primary: Primary skill ID.
|
|
1693
|
+
enhancers: List of enhancing skill IDs.
|
|
1694
|
+
agent_type: Agent creating this combination (for audit trail).
|
|
1695
|
+
source: Discovery source: human_specified or auto_discovered.
|
|
1696
|
+
"""
|
|
1697
|
+
# Part of SkillPool — independent infrastructure
|
|
1698
|
+
from skillpool.combiner import CombinationLifecycleManager
|
|
1699
|
+
|
|
1700
|
+
mgr = CombinationLifecycleManager()
|
|
1701
|
+
combo = mgr.create_combination(
|
|
1702
|
+
primary=primary,
|
|
1703
|
+
enhancers=enhancers,
|
|
1704
|
+
source=source,
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1707
|
+
return {
|
|
1708
|
+
"combination_id": combo.combination_id,
|
|
1709
|
+
"primary": combo.primary,
|
|
1710
|
+
"enhancers": combo.enhancers,
|
|
1711
|
+
"state": combo.state.name,
|
|
1712
|
+
"source": combo.source,
|
|
1713
|
+
"message": f"Combination created in {combo.state.name} state",
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
|
|
1717
|
+
@mcp.tool()
|
|
1718
|
+
def combination_get(combination_id: str) -> dict:
|
|
1719
|
+
"""Get details of a specific skill combination.
|
|
1720
|
+
|
|
1721
|
+
Returns combination state, gain data, and lifecycle information.
|
|
1722
|
+
|
|
1723
|
+
Args:
|
|
1724
|
+
combination_id: The combination ID (e.g. "review+karnpathy-guidelines").
|
|
1725
|
+
"""
|
|
1726
|
+
from skillpool.combiner import CombinationLifecycleManager
|
|
1727
|
+
|
|
1728
|
+
mgr = CombinationLifecycleManager()
|
|
1729
|
+
combo = mgr.get_combination(combination_id)
|
|
1730
|
+
|
|
1731
|
+
if combo is None:
|
|
1732
|
+
return {"error": "not_found", "message": f"Combination '{combination_id}' not found"}
|
|
1733
|
+
|
|
1734
|
+
return {
|
|
1735
|
+
"combination_id": combo.combination_id,
|
|
1736
|
+
"primary": combo.primary,
|
|
1737
|
+
"enhancers": combo.enhancers,
|
|
1738
|
+
"state": combo.state.name,
|
|
1739
|
+
"source": combo.source,
|
|
1740
|
+
"gain_avg": combo.gain_avg,
|
|
1741
|
+
"gain_confidence": combo.gain_confidence,
|
|
1742
|
+
"all_time_gain_avg": combo.all_time_gain_avg,
|
|
1743
|
+
"recent_gain_avg": combo.recent_gain_avg,
|
|
1744
|
+
"execution_count": combo.execution_count,
|
|
1745
|
+
"current_weight": combo.current_weight(),
|
|
1746
|
+
"last_execution": combo.last_execution,
|
|
1747
|
+
"promoted_at": combo.promoted_at,
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
@mcp.tool()
|
|
1752
|
+
def combination_list(
|
|
1753
|
+
state: str = "",
|
|
1754
|
+
primary: str = "",
|
|
1755
|
+
) -> dict:
|
|
1756
|
+
"""List skill combinations, optionally filtered by state or primary skill.
|
|
1757
|
+
|
|
1758
|
+
Args:
|
|
1759
|
+
state: Filter by lifecycle state (DISCOVERED/VALIDATING/PROMOTED/
|
|
1760
|
+
REJECTED/DEPRECATED/RETIRED). Empty = all states.
|
|
1761
|
+
primary: Filter by primary skill ID. Empty = all skills.
|
|
1762
|
+
"""
|
|
1763
|
+
from skillpool.combiner import CombinationLifecycleManager, CombinationLifecycleState
|
|
1764
|
+
|
|
1765
|
+
mgr = CombinationLifecycleManager()
|
|
1766
|
+
mgr._ensure_loaded()
|
|
1767
|
+
|
|
1768
|
+
combos = list(mgr._combinations.values())
|
|
1769
|
+
|
|
1770
|
+
if state:
|
|
1771
|
+
try:
|
|
1772
|
+
state_enum = CombinationLifecycleState[state]
|
|
1773
|
+
except KeyError:
|
|
1774
|
+
return {"error": "invalid_state", "message": f"Unknown state '{state}'"}
|
|
1775
|
+
combos = [c for c in combos if c.state == state_enum]
|
|
1776
|
+
|
|
1777
|
+
if primary:
|
|
1778
|
+
combos = [c for c in combos if c.primary == primary]
|
|
1779
|
+
|
|
1780
|
+
return {
|
|
1781
|
+
"count": len(combos),
|
|
1782
|
+
"combinations": [
|
|
1783
|
+
{
|
|
1784
|
+
"combination_id": c.combination_id,
|
|
1785
|
+
"primary": c.primary,
|
|
1786
|
+
"enhancers": c.enhancers,
|
|
1787
|
+
"state": c.state.name,
|
|
1788
|
+
"gain_avg": c.gain_avg,
|
|
1789
|
+
"execution_count": c.execution_count,
|
|
1790
|
+
"current_weight": c.current_weight(),
|
|
1791
|
+
}
|
|
1792
|
+
for c in combos
|
|
1793
|
+
],
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
|
|
1797
|
+
@mcp.tool()
|
|
1798
|
+
def combination_transition(
|
|
1799
|
+
combination_id: str,
|
|
1800
|
+
to_state: str,
|
|
1801
|
+
agent_type: str,
|
|
1802
|
+
reason: str = "",
|
|
1803
|
+
) -> dict:
|
|
1804
|
+
"""Manually transition a combination's lifecycle state.
|
|
1805
|
+
|
|
1806
|
+
Use with caution — automated transitions happen via report_usage
|
|
1807
|
+
and skill_lifecycle_check. This tool exists for administrative overrides.
|
|
1808
|
+
|
|
1809
|
+
Args:
|
|
1810
|
+
combination_id: The combination ID to transition.
|
|
1811
|
+
to_state: Target state name (DISCOVERED/VALIDATING/PROMOTED/
|
|
1812
|
+
REJECTED/DEPRECATED/RETIRED).
|
|
1813
|
+
agent_type: Agent requesting the transition (for audit trail).
|
|
1814
|
+
reason: Reason for the manual transition.
|
|
1815
|
+
"""
|
|
1816
|
+
from skillpool.combiner import CombinationLifecycleManager, CombinationLifecycleState
|
|
1817
|
+
|
|
1818
|
+
try:
|
|
1819
|
+
target = CombinationLifecycleState[to_state]
|
|
1820
|
+
except KeyError:
|
|
1821
|
+
return {"error": "invalid_state", "message": f"Unknown state '{to_state}'"}
|
|
1822
|
+
|
|
1823
|
+
mgr = CombinationLifecycleManager()
|
|
1824
|
+
result = mgr.transition(combination_id, target, reason=reason)
|
|
1825
|
+
|
|
1826
|
+
return {
|
|
1827
|
+
"combination_id": result.combination_id,
|
|
1828
|
+
"from_state": result.from_state.name,
|
|
1829
|
+
"to_state": result.to_state.name,
|
|
1830
|
+
"success": result.success,
|
|
1831
|
+
"reason": result.reason,
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
|
|
1835
|
+
@mcp.tool()
|
|
1836
|
+
def skill_lifecycle_check(
|
|
1837
|
+
skill_id: str = "",
|
|
1838
|
+
check_deprecation: bool = True,
|
|
1839
|
+
check_combinations: bool = True,
|
|
1840
|
+
) -> dict:
|
|
1841
|
+
"""Check skill lifecycle state and trigger auto-deprecation if needed.
|
|
1842
|
+
|
|
1843
|
+
Available to ALL Agents — this is the MCP-level entry point for
|
|
1844
|
+
skill auto-deprecation, not an internal-only function.
|
|
1845
|
+
|
|
1846
|
+
Args:
|
|
1847
|
+
skill_id: Specific skill to check. Empty = check all ACTIVE skills.
|
|
1848
|
+
check_deprecation: If True, trigger auto-deprecation checks.
|
|
1849
|
+
check_combinations: If True, also check combination lifecycle.
|
|
1850
|
+
"""
|
|
1851
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1852
|
+
from skillpool.lifecycle import check_auto_deprecation
|
|
1853
|
+
from skillpool.combiner import CombinationLifecycleManager
|
|
1854
|
+
|
|
1855
|
+
results = {
|
|
1856
|
+
"deprecation_checks": [],
|
|
1857
|
+
"combination_checks": [],
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
if check_deprecation:
|
|
1861
|
+
if skill_id:
|
|
1862
|
+
deprecated = check_auto_deprecation(skill_id)
|
|
1863
|
+
results["deprecation_checks"].append(
|
|
1864
|
+
{
|
|
1865
|
+
"skill_id": skill_id,
|
|
1866
|
+
"deprecated": deprecated,
|
|
1867
|
+
}
|
|
1868
|
+
)
|
|
1869
|
+
|
|
1870
|
+
if check_combinations:
|
|
1871
|
+
mgr = CombinationLifecycleManager()
|
|
1872
|
+
|
|
1873
|
+
if skill_id:
|
|
1874
|
+
combos = mgr.get_combinations_for_skill(skill_id)
|
|
1875
|
+
else:
|
|
1876
|
+
mgr._ensure_loaded()
|
|
1877
|
+
combos = list(mgr._combinations.values())
|
|
1878
|
+
|
|
1879
|
+
for combo in combos:
|
|
1880
|
+
if combo.state.value == 2: # PROMOTED
|
|
1881
|
+
result = mgr.check_deprecation(combo.combination_id)
|
|
1882
|
+
if result:
|
|
1883
|
+
results["combination_checks"].append(
|
|
1884
|
+
{
|
|
1885
|
+
"combination_id": combo.combination_id,
|
|
1886
|
+
"action": result.to_state.name,
|
|
1887
|
+
"reason": result.reason,
|
|
1888
|
+
}
|
|
1889
|
+
)
|
|
1890
|
+
|
|
1891
|
+
elif combo.state.value == 4: # DEPRECATED
|
|
1892
|
+
result = mgr.check_retirement(combo.combination_id)
|
|
1893
|
+
if result:
|
|
1894
|
+
results["combination_checks"].append(
|
|
1895
|
+
{
|
|
1896
|
+
"combination_id": combo.combination_id,
|
|
1897
|
+
"action": result.to_state.name,
|
|
1898
|
+
"reason": result.reason,
|
|
1899
|
+
}
|
|
1900
|
+
)
|
|
1901
|
+
|
|
1902
|
+
return results
|
|
1903
|
+
|
|
1904
|
+
|
|
1905
|
+
@mcp.tool()
|
|
1906
|
+
def get_emergency_overrides(skill_id: str = "") -> dict:
|
|
1907
|
+
"""Query current emergency overrides for skills.
|
|
1908
|
+
|
|
1909
|
+
Returns active WARN/DEGRADE/QUARANTINE/KILL overrides.
|
|
1910
|
+
If skill_id is provided, returns overrides for that skill only.
|
|
1911
|
+
If omitted, returns all active overrides.
|
|
1912
|
+
|
|
1913
|
+
Args:
|
|
1914
|
+
skill_id: Optional skill ID to filter overrides.
|
|
1915
|
+
"""
|
|
1916
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1917
|
+
import json
|
|
1918
|
+
|
|
1919
|
+
overrides_path = _SKILLPOOL_DIR / "emergency_overrides.json"
|
|
1920
|
+
if overrides_path.exists():
|
|
1921
|
+
overrides = json.loads(overrides_path.read_text())
|
|
1922
|
+
if skill_id:
|
|
1923
|
+
overrides = {k: v for k, v in overrides.items() if k == skill_id}
|
|
1924
|
+
return {"overrides": overrides, "count": len(overrides)}
|
|
1925
|
+
return {"overrides": {}, "count": 0}
|
|
1926
|
+
|
|
1927
|
+
|
|
1928
|
+
@mcp.tool()
|
|
1929
|
+
def cost_estimate(
|
|
1930
|
+
skill_id: str,
|
|
1931
|
+
skill_length: int = 0,
|
|
1932
|
+
review_level: str = "L1",
|
|
1933
|
+
include_review_checkpoint: bool = False,
|
|
1934
|
+
emergency_bypass_path: str = "",
|
|
1935
|
+
) -> dict:
|
|
1936
|
+
"""Estimate session cost for a skill execution using P50 conservative pricing.
|
|
1937
|
+
|
|
1938
|
+
Uses $0.003/1K tokens pricing model. Combines skill execution cost
|
|
1939
|
+
+ L2/L3 review overhead + review checkpoint overhead.
|
|
1940
|
+
|
|
1941
|
+
Args:
|
|
1942
|
+
skill_id: Skill identifier (e.g. "dev-4d-sdd").
|
|
1943
|
+
skill_length: Character count of skill definition (fallback if skill_get unavailable).
|
|
1944
|
+
review_level: Complexity level (L0/L1/L2/L3+L2+).
|
|
1945
|
+
include_review_checkpoint: Whether to include review checkpoint overhead.
|
|
1946
|
+
emergency_bypass_path: Path to emergency_overrides.json file.
|
|
1947
|
+
"""
|
|
1948
|
+
# Part of SkillPool — independent infrastructure, shared by all agents
|
|
1949
|
+
try:
|
|
1950
|
+
from skillpool.cost.token_governor import TokenGovernor, PRESET_AGENT_CONFIGS
|
|
1951
|
+
|
|
1952
|
+
governor = TokenGovernor(PRESET_AGENT_CONFIGS)
|
|
1953
|
+
result = governor.estimate_session_cost(
|
|
1954
|
+
skill_id=skill_id,
|
|
1955
|
+
skill_length=skill_length,
|
|
1956
|
+
review_level=review_level,
|
|
1957
|
+
include_review_checkpoint=include_review_checkpoint,
|
|
1958
|
+
emergency_bypass_path=emergency_bypass_path or None,
|
|
1959
|
+
)
|
|
1960
|
+
return result.model_dump()
|
|
1961
|
+
except Exception as e:
|
|
1962
|
+
return {
|
|
1963
|
+
"error": f"cost_estimate error: {type(e).__name__}: {e}",
|
|
1964
|
+
"skill_id": skill_id,
|
|
1965
|
+
"total_cost_usd": 0.0,
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
|
|
1969
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
1970
|
+
# Entry point
|
|
1971
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
def main():
|
|
1975
|
+
"""Entry point for skillpool-mcp CLI command."""
|
|
1976
|
+
import argparse
|
|
1977
|
+
|
|
1978
|
+
parser = argparse.ArgumentParser(description="SkillPool MCP Server")
|
|
1979
|
+
parser.add_argument("--agent-type", help="Agent type for logging/metadata (tools receive agent_type per-call)")
|
|
1980
|
+
parser.add_argument(
|
|
1981
|
+
"--transport",
|
|
1982
|
+
choices=["stdio", "sse", "streamable-http"],
|
|
1983
|
+
default="stdio",
|
|
1984
|
+
help="MCP transport protocol (default: stdio)",
|
|
1985
|
+
)
|
|
1986
|
+
parser.add_argument(
|
|
1987
|
+
"--port", type=int, default=8101, help="HTTP port for streamable-http transport (default: 8101)"
|
|
1988
|
+
)
|
|
1989
|
+
parser.add_argument(
|
|
1990
|
+
"--host", default="127.0.0.1", help="HTTP host for streamable-http transport (default: 127.0.0.1)"
|
|
1991
|
+
)
|
|
1992
|
+
args = parser.parse_args()
|
|
1993
|
+
|
|
1994
|
+
if args.transport in ("streamable-http", "sse"):
|
|
1995
|
+
import uvicorn
|
|
1996
|
+
|
|
1997
|
+
logger.info("Starting SkillPool MCP on %s:%d (%s)", args.host, args.port, args.transport)
|
|
1998
|
+
app = mcp.http_app()
|
|
1999
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
|
2000
|
+
else:
|
|
2001
|
+
mcp.run(transport=args.transport)
|
|
2002
|
+
|
|
2003
|
+
|
|
2004
|
+
if __name__ == "__main__":
|
|
2005
|
+
main()
|