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,264 @@
1
+ """TokenGovernor — per-agent daily token budget enforcement."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Callable
9
+
10
+ from skillpool.cost.models import AgentConfig, CostEstimate, ThrottleAction
11
+ from skillpool.utils.time_utils import utc_now
12
+
13
+
14
+ @dataclass
15
+ class _Usage:
16
+ """Internal daily usage tracker for a single agent."""
17
+
18
+ tokens: int = 0
19
+
20
+
21
+ # Preset agent configurations from CLAUDE.md Section 9.2
22
+ PRESET_AGENT_CONFIGS: list[AgentConfig] = [
23
+ AgentConfig(agent_id="root_planner", agent_type="planner", daily_limit_tokens=0, is_critical=True),
24
+ AgentConfig(agent_id="sub_planner", agent_type="planner", daily_limit_tokens=0, is_critical=True),
25
+ AgentConfig(agent_id="level1_worker", agent_type="worker", daily_limit_tokens=0, is_critical=True),
26
+ AgentConfig(agent_id="level2_worker", agent_type="worker", daily_limit_tokens=0, is_critical=True),
27
+ AgentConfig(
28
+ agent_id="evolver_v4",
29
+ agent_type="evolver",
30
+ daily_limit_tokens=100_000,
31
+ throttle_at_pct=0.8,
32
+ throttle_to_pct=0.5,
33
+ ),
34
+ AgentConfig(
35
+ agent_id="knowledge_refiner",
36
+ agent_type="refiner",
37
+ daily_limit_tokens=500_000,
38
+ throttle_at_pct=0.9,
39
+ throttle_to_pct=0.25,
40
+ ),
41
+ AgentConfig(
42
+ agent_id="sandbox_validator",
43
+ agent_type="validator",
44
+ daily_limit_tokens=50_000,
45
+ throttle_at_pct=0.9,
46
+ throttle_to_pct=0.25,
47
+ ),
48
+ AgentConfig(
49
+ agent_id="hermes_refiner",
50
+ agent_type="refiner",
51
+ daily_limit_tokens=300_000,
52
+ throttle_at_pct=0.8,
53
+ throttle_to_pct=0.5,
54
+ ),
55
+ ]
56
+
57
+
58
+ class TokenGovernor:
59
+ """Enforce per-agent daily token budgets with throttle/reject logic.
60
+
61
+ Agents with daily_limit_tokens=0 are unlimited.
62
+ When usage crosses throttle_at_pct of the limit, requests are throttled
63
+ (allowed but reduced). When usage reaches 100%, requests are rejected.
64
+ """
65
+
66
+ def __init__(self, configs: list[AgentConfig] | None = None) -> None:
67
+ self._configs: dict[str, AgentConfig] = {}
68
+ self._usage: dict[str, _Usage] = {}
69
+
70
+ for cfg in configs or PRESET_AGENT_CONFIGS:
71
+ self._configs[cfg.agent_id] = cfg
72
+ self._usage[cfg.agent_id] = _Usage()
73
+
74
+ def get_config(self, agent_id: str) -> AgentConfig | None:
75
+ """Return the config for an agent, or None if unknown."""
76
+ return self._configs.get(agent_id)
77
+
78
+ def check(self, agent_id: str, requested_tokens: int) -> tuple[ThrottleAction, int]:
79
+ """Check whether a token request is allowed.
80
+
81
+ Returns (action, allowed_tokens). For unlimited agents, always allows
82
+ the full request. For budgeted agents:
83
+ - below throttle threshold: ALLOW with full tokens
84
+ - between throttle and 100%: THROTTLE with reduced tokens
85
+ - at or above 100%: REJECT with 0 tokens
86
+ """
87
+ cfg = self._configs.get(agent_id)
88
+ if cfg is None:
89
+ # Unknown agent: allow by default
90
+ return ThrottleAction.ALLOW, requested_tokens
91
+
92
+ if cfg.daily_limit_tokens == 0:
93
+ return ThrottleAction.ALLOW, requested_tokens
94
+
95
+ usage = self._usage.get(agent_id, _Usage())
96
+ pct_consumed = usage.tokens / cfg.daily_limit_tokens
97
+
98
+ if pct_consumed >= 1.0:
99
+ return ThrottleAction.REJECT, 0
100
+
101
+ if pct_consumed >= cfg.throttle_at_pct:
102
+ # Throttle: reduce to throttle_to_pct of the remaining budget
103
+ remaining = cfg.daily_limit_tokens - usage.tokens
104
+ allowed = int(remaining * cfg.throttle_to_pct)
105
+ allowed = max(0, min(allowed, requested_tokens))
106
+ return ThrottleAction.THROTTLE, allowed
107
+
108
+ return ThrottleAction.ALLOW, requested_tokens
109
+
110
+ def record_usage(self, agent_id: str, tokens_used: int) -> None:
111
+ """Record actual token usage for an agent."""
112
+ if agent_id not in self._usage:
113
+ self._usage[agent_id] = _Usage()
114
+ self._usage[agent_id].tokens += tokens_used
115
+
116
+ def get_daily_usage(self, agent_id: str) -> int:
117
+ """Return tokens consumed today by an agent."""
118
+ usage = self._usage.get(agent_id)
119
+ return usage.tokens if usage else 0
120
+
121
+ def is_throttled(self, agent_id: str) -> bool:
122
+ """Check if an agent is currently in throttled state."""
123
+ cfg = self._configs.get(agent_id)
124
+ if cfg is None or cfg.daily_limit_tokens == 0:
125
+ return False
126
+ usage = self._usage.get(agent_id, _Usage())
127
+ return usage.tokens >= cfg.daily_limit_tokens * cfg.throttle_at_pct
128
+
129
+ def reset_daily(self) -> None:
130
+ """Reset all daily usage counters (for testing or day rollover)."""
131
+ for usage in self._usage.values():
132
+ usage.tokens = 0
133
+
134
+ # -------------------------------------------------------------------
135
+ # Session Cost Estimation (V4.2)
136
+ # -------------------------------------------------------------------
137
+
138
+ # P50 conservative pricing: $0.003/1K tokens (Claude Sonnet)
139
+ _P50_PRICE_PER_1K: float = 0.003
140
+ # Approximate chars per token for English/code content
141
+ _CHARS_PER_TOKEN: int = 4
142
+ # Review overhead: estimated token counts for review checkpoints
143
+ _L2_REVIEW_TOKENS: int = 8_000
144
+ _L3_REVIEW_TOKENS: int = 15_000
145
+ _REVIEW_CHECKPOINT_TOKENS: int = 3_000
146
+
147
+ def estimate_session_cost(
148
+ self,
149
+ skill_id: str,
150
+ skill_length: int = 0,
151
+ review_level: str = "L1",
152
+ include_review_checkpoint: bool = False,
153
+ skill_get_fn: Callable[[str], Any] | None = None,
154
+ gate_check_result: Any | None = None,
155
+ enforcement_mode: str = "strict",
156
+ emergency_bypass_path: str | None = None,
157
+ changed_files: list[str] | None = None,
158
+ ) -> CostEstimate:
159
+ """Estimate session cost for a skill execution.
160
+
161
+ Uses P50 conservative pricing ($0.003/1K tokens).
162
+ Combines: skill execution cost + L2/L3 review overhead + checkpoint overhead.
163
+
164
+ Args:
165
+ skill_id: Skill identifier (e.g. "dev-4d-sdd").
166
+ skill_length: Character count of skill definition (fallback if skill_get unavailable).
167
+ review_level: Complexity level (L0/L1/L2/L3+L2+).
168
+ include_review_checkpoint: Whether to include review checkpoint overhead.
169
+ skill_get_fn: Optional callable to fetch skill definition via MCP.
170
+ gate_check_result: Optional GateCheckResult from gate validation.
171
+ enforcement_mode: Gate enforcement mode (strict/permissive/disabled).
172
+ emergency_bypass_path: Path to emergency_overrides.json file.
173
+ changed_files: List of changed file paths for gate pattern matching.
174
+
175
+ Returns:
176
+ CostEstimate with full cost breakdown.
177
+ """
178
+ # Step 1: Resolve skill_length from skill_get or fallback
179
+ resolved_length = skill_length
180
+ if skill_get_fn is not None:
181
+ try:
182
+ skill_data = skill_get_fn(skill_id)
183
+ if skill_data is not None and hasattr(skill_data, "content"):
184
+ resolved_length = len(skill_data.content)
185
+ elif isinstance(skill_data, dict):
186
+ content = skill_data.get("content", "")
187
+ resolved_length = len(content) if content else skill_length
188
+ except Exception:
189
+ pass # Fallback to provided skill_length
190
+
191
+ # Step 2: Estimate token count
192
+ token_count = max(1, resolved_length // self._CHARS_PER_TOKEN)
193
+
194
+ # Step 3: Calculate base cost (P50 pricing)
195
+ base_cost_usd = token_count * self._P50_PRICE_PER_1K / 1000
196
+
197
+ # Step 4: Calculate review overhead based on level
198
+ l2_overhead = 0.0
199
+ l3_overhead = 0.0
200
+ if review_level in ("L2", "L3+L2+"):
201
+ l2_overhead = self._L2_REVIEW_TOKENS * self._P50_PRICE_PER_1K / 1000
202
+ if review_level == "L3+L2+":
203
+ l3_overhead = self._L3_REVIEW_TOKENS * self._P50_PRICE_PER_1K / 1000
204
+
205
+ # Step 5: Calculate review checkpoint overhead
206
+ checkpoint_overhead = 0.0
207
+ if include_review_checkpoint:
208
+ checkpoint_overhead = self._REVIEW_CHECKPOINT_TOKENS * self._P50_PRICE_PER_1K / 1000
209
+
210
+ # Step 6: Gate validation
211
+ gate_passed = True
212
+ gate_block_reason = None
213
+ if gate_check_result is not None:
214
+ if enforcement_mode == "disabled":
215
+ gate_passed = True
216
+ elif enforcement_mode == "permissive":
217
+ gate_passed = gate_check_result.passed
218
+ else: # strict
219
+ gate_passed = gate_check_result.passed
220
+ if not gate_passed:
221
+ gate_block_reason = gate_check_result.validation_message
222
+
223
+ # Step 7: Emergency bypass check
224
+ emergency_bypass_active = False
225
+ if emergency_bypass_path is not None:
226
+ bypass_path = Path(emergency_bypass_path)
227
+ if bypass_path.exists():
228
+ try:
229
+ data = json.loads(bypass_path.read_text())
230
+ expires_at = data.get("expires_at")
231
+ if expires_at:
232
+ from datetime import datetime
233
+
234
+ expiry = datetime.fromisoformat(expires_at)
235
+ if utc_now() < expiry:
236
+ emergency_bypass_active = True
237
+ else:
238
+ # No expiry → bypass active indefinitely
239
+ emergency_bypass_active = True
240
+ except Exception:
241
+ pass # Corrupt file → bypass not active
242
+
243
+ # If bypass active, override gate result
244
+ if emergency_bypass_active:
245
+ gate_passed = True
246
+ gate_block_reason = None
247
+
248
+ # Step 8: Total cost
249
+ total_cost = base_cost_usd + l2_overhead + l3_overhead + checkpoint_overhead
250
+
251
+ return CostEstimate(
252
+ skill_id=skill_id,
253
+ skill_length=resolved_length,
254
+ token_count=token_count,
255
+ base_cost_usd=round(base_cost_usd, 6),
256
+ l2_review_overhead_usd=round(l2_overhead, 6),
257
+ l3_review_overhead_usd=round(l3_overhead, 6),
258
+ review_checkpoint_overhead_usd=round(checkpoint_overhead, 6),
259
+ total_cost_usd=round(total_cost, 6),
260
+ price_per_1k_tokens=self._P50_PRICE_PER_1K,
261
+ gate_passed=gate_passed,
262
+ gate_block_reason=gate_block_reason,
263
+ emergency_bypass_active=emergency_bypass_active,
264
+ )
@@ -0,0 +1,38 @@
1
+ """TraceCeiling — per-trace cost ceiling with circuit breaker."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class TraceCeiling:
7
+ """Enforce a cost ceiling per trace_id.
8
+
9
+ When a trace's cumulative cost exceeds the ceiling, all further
10
+ operations under that trace are blocked (circuit broken).
11
+ """
12
+
13
+ def __init__(self, ceiling_usd: float = 5.0) -> None:
14
+ self.ceiling_usd = ceiling_usd
15
+ self._trace_costs: dict[str, float] = {}
16
+
17
+ def record_trace_cost(self, trace_id: str, cost_usd: float) -> None:
18
+ """Record a cost against a trace."""
19
+ if trace_id not in self._trace_costs:
20
+ self._trace_costs[trace_id] = 0.0
21
+ self._trace_costs[trace_id] += cost_usd
22
+
23
+ def check(self, trace_id: str, additional_cost: float) -> tuple[bool, str]:
24
+ """Check if an additional cost is allowed for a trace.
25
+
26
+ Returns (allowed, reason).
27
+ """
28
+ current = self._trace_costs.get(trace_id, 0.0)
29
+ if current + additional_cost > self.ceiling_usd:
30
+ return (
31
+ False,
32
+ f"trace {trace_id} ceiling exceeded: ${current:.4f} + ${additional_cost:.4f} > ${self.ceiling_usd:.2f}",
33
+ )
34
+ return True, "ok"
35
+
36
+ def is_circuit_broken(self, trace_id: str) -> bool:
37
+ """Check if a trace has hit its cost ceiling."""
38
+ return self._trace_costs.get(trace_id, 0.0) >= self.ceiling_usd
skillpool/csdf.py ADDED
@@ -0,0 +1,126 @@
1
+ """SkillPool CSDF Parser — Parse SKILL.md frontmatter into structured data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import yaml
11
+ from pydantic import BaseModel, Field, field_validator
12
+
13
+
14
+ class CSDFDocument(BaseModel):
15
+ """Parsed CSDF (Codex Skill Definition Format) document."""
16
+
17
+ name: str
18
+ version: str = "0.1.0"
19
+ description: str = ""
20
+ triggers: list[str] = Field(default_factory=list)
21
+ references: list[str] = Field(default_factory=list)
22
+ dependencies: list[str] = Field(default_factory=list)
23
+ quality: dict[str, Any] = Field(default_factory=dict)
24
+ dimensions: dict[str, float] = Field(default_factory=dict)
25
+ body: str = ""
26
+ content_hash: str = ""
27
+ source_path: str = ""
28
+
29
+ @field_validator("version")
30
+ @classmethod
31
+ def version_format(cls, v: str) -> str:
32
+ """Accept semver (1.2.3) or short (1.0) version strings."""
33
+ if not re.match(r"^\d+(\.\d+)*$", v):
34
+ raise ValueError(f"Invalid version format: {v!r}")
35
+ return v
36
+
37
+
38
+ class CSDFParser:
39
+ """Stateless parser for CSDF frontmatter documents."""
40
+
41
+ def __init__(self) -> None:
42
+ self._frontmatter_re = re.compile(r"^---\s*\n(.*?)\n---\s*\n?(.*)", re.DOTALL)
43
+
44
+ def parse(self, content: str, source_path: str = "") -> CSDFDocument:
45
+ """Parse a SKILL.md string into a CSDFDocument.
46
+
47
+ Raises ValueError if no valid YAML frontmatter is found.
48
+ """
49
+ if isinstance(content, Path):
50
+ content = content.read_text(encoding="utf-8")
51
+
52
+ content = str(content)
53
+ match = self._frontmatter_re.match(content)
54
+ if not match:
55
+ raise ValueError("No valid YAML frontmatter found")
56
+
57
+ fm_text, body = match.group(1), match.group(2)
58
+ try:
59
+ fm: dict[str, Any] = yaml.safe_load(fm_text) or {}
60
+ except yaml.YAMLError as exc:
61
+ raise ValueError(f"Invalid YAML in frontmatter: {exc}") from exc
62
+
63
+ quality = fm.pop("quality", {}) or {}
64
+ dimensions = fm.pop("dimensions", {}) or {}
65
+
66
+ doc = CSDFDocument(
67
+ name=fm.get("name", ""),
68
+ version=fm.get("version", "0.1.0"),
69
+ description=fm.get("description", ""),
70
+ triggers=fm.get("triggers", []),
71
+ references=fm.get("references", []),
72
+ dependencies=fm.get("dependencies", []),
73
+ quality=quality,
74
+ dimensions=dimensions,
75
+ body=body.strip(),
76
+ content_hash=self._hash(content),
77
+ source_path=source_path,
78
+ )
79
+ return doc
80
+
81
+ @staticmethod
82
+ def _hash(content: str) -> str:
83
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()[:16]
84
+
85
+
86
+ def parse_csdf(source: str | Path) -> CSDFDocument:
87
+ """Parse a CSDF document from a file path or string content.
88
+
89
+ Unlike CSDFParser.parse(), this function gracefully handles content
90
+ without frontmatter by returning a default CSDFDocument.
91
+ """
92
+ parser = CSDFParser()
93
+ if isinstance(source, Path):
94
+ content = source.read_text(encoding="utf-8")
95
+ try:
96
+ return parser.parse(content, source_path=str(source))
97
+ except ValueError:
98
+ return CSDFDocument(
99
+ name="",
100
+ body=content.strip(),
101
+ content_hash=parser._hash(content),
102
+ source_path=str(source),
103
+ )
104
+ content = str(source)
105
+ try:
106
+ return parser.parse(content)
107
+ except ValueError:
108
+ return CSDFDocument(
109
+ name="",
110
+ body=content.strip(),
111
+ content_hash=parser._hash(content),
112
+ )
113
+
114
+
115
+ def validate_csdf(doc: CSDFDocument) -> list[str]:
116
+ """Validate a CSDFDocument and return a list of issues."""
117
+ issues: list[str] = []
118
+ if not doc.name:
119
+ issues.append("Missing name")
120
+ if not doc.description:
121
+ issues.append("Missing description")
122
+ if not doc.triggers:
123
+ issues.append("Missing triggers")
124
+ if not doc.body:
125
+ issues.append("Missing body content after frontmatter")
126
+ return issues