devsquad 3.6.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 (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. skills/test/handler.py +78 -0
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Scratchpad - 共享黑板实现
5
+
6
+ 设计决策(门禁1解决):
7
+ - 并发写入策略:时间戳排序 + 版本号 + 最后写入胜出(LWW)
8
+ - 对于发现类数据(FINDING/QUESTION),覆盖是可接受的
9
+ - 对于决策类数据(DECISION),需要 Consensus 机制保护
10
+ - 容量上限:默认 1000 条,LRU 淘汰最旧的非 RESOLVED 条目
11
+ - 存储选型(门禁3):内存主存储 + JSON 文件持久化备份
12
+ """
13
+
14
+ import os
15
+ import json
16
+ import logging
17
+ import threading
18
+ from datetime import datetime
19
+ from typing import Dict, List, Optional, Any
20
+ from collections import OrderedDict
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ from .models import (
25
+ ScratchpadEntry,
26
+ EntryType,
27
+ EntryStatus,
28
+ ReferenceType,
29
+ Reference,
30
+ )
31
+ from .usage_tracker import track_usage
32
+
33
+
34
+ MAX_ENTRIES_DEFAULT = 1000
35
+
36
+
37
+ class Scratchpad:
38
+ """
39
+ 共享黑板 - 多 Worker 协作的信息交换中心
40
+
41
+ 所有 Worker 通过 Scratchpad 共享发现、决策、问题和冲突。
42
+ 采用 LWW (Last-Writer-Wins) 并发策略 + 版本号机制处理并发写入。
43
+
44
+ 核心能力:
45
+ - write(): 写入条目(自动版本递增、容量管理、持久化)
46
+ - read(): 多维查询(关键词/类型/状态/Worker/标签/时间)
47
+ - resolve(): 标记冲突/问题为已解决
48
+ - get_summary(): 生成 Markdown 格式的全局摘要
49
+
50
+ 设计特点:
51
+ - 容量上限: 默认 1000 条,LRU 淘汰最旧的非 RESOLVED 条目
52
+ - 持久化: JSONL 追加写模式,支持断点恢复
53
+ - 线程安全: RLock 保护所有读写操作
54
+
55
+ 使用示例:
56
+ sp = Scratchpad(persist_dir="/tmp/sp_data")
57
+ entry = ScratchpadEntry(worker_id="w1", role_id="architect",
58
+ entry_type=EntryType.FINDING,
59
+ content="建议使用微服务架构")
60
+ sp.write(entry)
61
+ findings = sp.read(entry_type=EntryType.FINDING)
62
+ """
63
+
64
+ def __init__(self, scratchpad_id: Optional[str] = None, persist_dir: Optional[str] = None):
65
+ """
66
+ 初始化共享黑板
67
+
68
+ Args:
69
+ scratchpad_id: 黑板唯一标识(自动生成时间戳ID如未提供)
70
+ persist_dir: 持久化目录路径(为空则不持久化,纯内存模式)
71
+ """
72
+ self.scratchpad_id = scratchpad_id or f"scratchpad-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
73
+ if '..' in self.scratchpad_id or '/' in self.scratchpad_id or '\\' in self.scratchpad_id:
74
+ raise ValueError(f"Invalid scratchpad_id (path traversal detected): {self.scratchpad_id}")
75
+ self.persist_dir = persist_dir
76
+
77
+ self._entries: OrderedDict[str, ScratchpadEntry] = OrderedDict()
78
+ self._lock = threading.RLock()
79
+ self._max_entries: int = MAX_ENTRIES_DEFAULT
80
+ self._write_count: int = 0
81
+ self._read_count: int = 0
82
+
83
+ if self.persist_dir:
84
+ os.makedirs(self.persist_dir, exist_ok=True)
85
+ self._load_from_disk()
86
+
87
+ def write(self, entry: ScratchpadEntry) -> str:
88
+ """
89
+ [MCE 集成点 Phase C] Worker 写入共享黑板
90
+
91
+ 当前行为: 直接存储原始 ScratchpadEntry (type/content/confidence)
92
+ MCE 就绪后: 每个 entry 写入前经过 MCE 分类标注
93
+ → [decision] "我们决定用微服务架构"
94
+ → [correction] "数据库从Mongo改为PostgreSQL"
95
+ → [user_pref] "团队习惯TDD开发模式"
96
+ → [relationship] "Alice负责后端API"
97
+ 后续 Worker 读取时看到带类型的结构化发现,而非纯文本
98
+
99
+ 接口预留: mce_engine 参数, enable_mce_annotate: bool
100
+ """
101
+ with self._lock:
102
+ if len(self._entries) >= self._max_entries:
103
+ self._evict_oldest(count=len(self._entries) - self._max_entries + 1)
104
+
105
+ existing = self._entries.get(entry.entry_id)
106
+ if existing and existing.version >= entry.version:
107
+ entry.version = existing.version + 1
108
+
109
+ self._entries[entry.entry_id] = entry
110
+ self._write_count += 1
111
+ self._persist_entry(entry)
112
+ track_usage("scratchpad.write", success=True, metadata={
113
+ "entry_type": entry.entry_type.value
114
+ })
115
+ return entry.entry_id
116
+
117
+ def read(self, query: str = "", since: Optional[datetime] = None,
118
+ entry_type: Optional[EntryType] = None,
119
+ status: Optional[EntryStatus] = None,
120
+ worker_id: Optional[str] = None,
121
+ tags: Optional[List[str]] = None,
122
+ limit: int = 50) -> List[ScratchpadEntry]:
123
+ """
124
+ 多维查询黑板条目
125
+
126
+ 支持按关键词、时间、类型、状态、Worker、标签等多维度组合过滤。
127
+ 返回结果按时间倒序(最新的在前)。
128
+
129
+ Args:
130
+ query: 关键词模糊匹配(在 content 和 tags 中搜索)
131
+ since: 起始时间(只返回此之后的条目)
132
+ entry_type: 按条目类型过滤 (FINDING/QUESTION/DECISION/CONFLICT)
133
+ status: 按状态过滤 (ACTIVE/RESOLVED)
134
+ worker_id: 按 Worker ID 过滤
135
+ tags: 标签列表(任一匹配即返回)
136
+ limit: 最大返回条数
137
+
138
+ Returns:
139
+ List[ScratchpadEntry]: 匹配的条目列表(时间倒序)
140
+ """
141
+ with self._lock:
142
+ results = []
143
+ for entry in reversed(self._entries.values()):
144
+ if since and entry.timestamp < since:
145
+ continue
146
+ if entry_type and entry.entry_type != entry_type:
147
+ continue
148
+ if status and entry.status != status:
149
+ continue
150
+ if worker_id and entry.worker_id != worker_id:
151
+ continue
152
+ if tags and not any(t in entry.tags for t in tags):
153
+ continue
154
+ if query:
155
+ q_lower = query.lower()
156
+ if (q_lower not in entry.content.lower() and
157
+ not any(q_lower in t.lower() for t in entry.tags)):
158
+ continue
159
+ results.append(entry)
160
+ if len(results) >= limit:
161
+ break
162
+ self._read_count += 1
163
+ track_usage("scratchpad.read", success=True, metadata={
164
+ "results_count": len(results),
165
+ "has_query": bool(query)
166
+ })
167
+ return results
168
+
169
+ def resolve(self, entry_id: str, resolution: str = ""):
170
+ """
171
+ 将条目标记为已解决
172
+
173
+ 修改条目状态为 RESOLVED,并可选地附加解决方案说明。
174
+ 解决方案会追加到原内容末尾。
175
+
176
+ Args:
177
+ entry_id: 要解决的条目 ID
178
+ resolution: 解决方案描述(追加到原内容后)
179
+ """
180
+ with self._lock:
181
+ entry = self._entries.get(entry_id)
182
+ if entry:
183
+ entry.status = EntryStatus.RESOLVED
184
+ if resolution:
185
+ entry.content = f"{entry.content}\n\n[RESOLVED] {resolution}"
186
+ entry.version += 1
187
+ self._persist_entry(entry)
188
+
189
+ def get_conflicts(self) -> List[ScratchpadEntry]:
190
+ """
191
+ 获取所有活跃冲突
192
+
193
+ 快捷方法,等价于 read(entry_type=CONFLICT, status=ACTIVE)
194
+
195
+ Returns:
196
+ List[ScratchpadEntry]: 当前未解决的冲突列表
197
+ """
198
+ return self.read(
199
+ entry_type=EntryType.CONFLICT,
200
+ status=EntryStatus.ACTIVE,
201
+ )
202
+
203
+ def get_summary(self, for_role: Optional[str] = None,
204
+ max_entries: int = 20) -> str:
205
+ """
206
+ 生成黑板全局摘要(Markdown格式)
207
+
208
+ 包含: 总览统计、活跃冲突列表、最近决策、关键发现。
209
+
210
+ Args:
211
+ for_role: 为指定角色定制摘要(预留参数)
212
+ max_entries: 各类别的最大展示条数
213
+
214
+ Returns:
215
+ str: Markdown 格式的摘要文本
216
+ """
217
+ active_findings = self.read(
218
+ entry_type=EntryType.FINDING,
219
+ status=EntryStatus.ACTIVE,
220
+ limit=max_entries,
221
+ )
222
+ decisions = self.read(
223
+ entry_type=EntryType.DECISION,
224
+ status=EntryStatus.ACTIVE,
225
+ limit=max_entries // 2,
226
+ )
227
+ conflicts = self.get_conflicts()
228
+
229
+ lines = [f"# Scratchpad Summary ({self.scratchpad_id})"]
230
+ lines.append(f"**Total entries**: {len(self._entries)} | **Active findings**: {len(active_findings)} | **Conflicts**: {len(conflicts)}")
231
+ lines.append("")
232
+
233
+ if conflicts:
234
+ lines.append(f"## ⚠️ Active Conflicts ({len(conflicts)})")
235
+ for c in conflicts[:5]:
236
+ role_tag = f"[{c.role_id}]"
237
+ conf_str = f"- {role_tag} {c.content[:100]}"
238
+ lines.append(conf_str)
239
+ lines.append("")
240
+
241
+ if decisions:
242
+ lines.append(f"## ✅ Recent Decisions ({len(decisions)})")
243
+ for d in decisions[:10]:
244
+ role_tag = f"[{d.role_id}]"
245
+ dec_str = f"- {role_tag} {d.content[:120]} (confidence: {d.confidence:.0%})"
246
+ lines.append(dec_str)
247
+ lines.append("")
248
+
249
+ if active_findings:
250
+ lines.append(f"## 🔍 Key Findings ({len(active_findings)})")
251
+ for f in active_findings[:15]:
252
+ role_tag = f"[{f.worker_id}/{f.role_id}]"
253
+ find_str = f"- {role_tag} {f.content[:120]} (confidence: {f.confidence:.0%})"
254
+ lines.append(find_str)
255
+
256
+ return "\n".join(lines)
257
+
258
+ def get_stats(self) -> Dict[str, Any]:
259
+ """
260
+ 获取黑板详细统计信息
261
+
262
+ Returns:
263
+ Dict[str, Any]: 统计字典,包含:
264
+ - scratchpad_id: 黑板标识
265
+ - total_entries: 总条目数
266
+ - by_type: 按类型分布
267
+ - by_status: 按状态分布
268
+ - by_worker: 按 Worker 分布
269
+ - write_count/read_count: 读写计数
270
+ - max_entries: 容量上限
271
+ """
272
+ with self._lock:
273
+ by_type = {}
274
+ by_status = {}
275
+ by_worker = {}
276
+ for e in self._entries.values():
277
+ by_type[e.entry_type.value] = by_type.get(e.entry_type.value, 0) + 1
278
+ by_status[e.status.value] = by_status.get(e.status.value, 0) + 1
279
+ by_worker[e.worker_id] = by_worker.get(e.worker_id, 0) + 1
280
+ return {
281
+ "scratchpad_id": self.scratchpad_id,
282
+ "total_entries": len(self._entries),
283
+ "by_type": by_type,
284
+ "by_status": by_status,
285
+ "by_worker": by_worker,
286
+ "write_count": self._write_count,
287
+ "read_count": self._read_count,
288
+ "max_entries": self._max_entries,
289
+ }
290
+
291
+ def _evict_oldest(self, count: int = 1):
292
+ to_evict = []
293
+ for eid, entry in self._entries.items():
294
+ if entry.status == EntryStatus.RESOLVED:
295
+ to_evict.append((eid, entry.timestamp))
296
+ if len(to_evict) >= count:
297
+ break
298
+ if not to_evict:
299
+ to_evict = [(eid, e.timestamp) for eid, e in list(self._entries.items())[:count]]
300
+ to_evict.sort(key=lambda x: x[1])
301
+ for eid, _ in to_evict:
302
+ del self._entries[eid]
303
+
304
+ def _persist_entry(self, entry: ScratchpadEntry):
305
+ if not self.persist_dir:
306
+ return
307
+ filepath = os.path.join(self.persist_dir, f"{self.scratchpad_id}.jsonl")
308
+ try:
309
+ with open(filepath, "a", encoding="utf-8") as f:
310
+ f.write(json.dumps(entry.to_dict(), ensure_ascii=False) + "\n")
311
+ except Exception as e:
312
+ logger.warning("Failed to persist scratchpad entry %s: %s", entry.entry_id, e)
313
+
314
+ def _load_from_disk(self):
315
+ if not self.persist_dir:
316
+ return
317
+ filepath = os.path.join(self.persist_dir, f"{self.scratchpad_id}.jsonl")
318
+ if not os.path.exists(filepath):
319
+ return
320
+ try:
321
+ with open(filepath, "r", encoding="utf-8") as f:
322
+ for line in f:
323
+ line = line.strip()
324
+ if not line:
325
+ continue
326
+ data = json.loads(line)
327
+ entry = ScratchpadEntry.from_dict(data)
328
+ self._entries[entry.entry_id] = entry
329
+ except Exception as e:
330
+ logger.warning("Failed to load scratchpad from %s: %s", filepath, e)
331
+
332
+ def clear(self):
333
+ """清空所有黑板条目(仅内存,不影响已持久化的文件)"""
334
+ with self._lock:
335
+ self._entries.clear()
336
+
337
+ def export_json(self) -> str:
338
+ """
339
+ 导出所有条目为 JSON 字符串
340
+
341
+ Returns:
342
+ str: JSON 格式的完整数据快照
343
+ """
344
+ with self._lock:
345
+ entries = [e.to_dict() for e in self._entries.values()]
346
+ return json.dumps(entries, ensure_ascii=False, indent=2)
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import logging
4
+ import hashlib
5
+ from pathlib import Path
6
+ from typing import Dict, List, Any, Optional, Callable
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @dataclass
14
+ class SkillEntry:
15
+ skill_id: str = field(default_factory=lambda: f"skill-{hashlib.md5(str(datetime.now().isoformat()).encode()).hexdigest()[:8]}")
16
+ name: str = ""
17
+ description: str = ""
18
+ category: str = "general"
19
+ version: str = "1.0.0"
20
+ handler: Optional[str] = None
21
+ tags: List[str] = field(default_factory=list)
22
+ confidence: float = 0.0
23
+ usage_count: int = 0
24
+ last_used: Optional[str] = None
25
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
26
+ metadata: Dict[str, Any] = field(default_factory=dict)
27
+
28
+ def to_dict(self) -> Dict[str, Any]:
29
+ return {
30
+ 'skill_id': self.skill_id, 'name': self.name, 'description': self.description,
31
+ 'category': self.category, 'version': self.version, 'handler': self.handler,
32
+ 'tags': self.tags, 'confidence': self.confidence, 'usage_count': self.usage_count,
33
+ 'last_used': self.last_used, 'created_at': self.created_at, 'metadata': self.metadata,
34
+ }
35
+
36
+ @classmethod
37
+ def from_dict(cls, data: Dict[str, Any]) -> 'SkillEntry':
38
+ return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
39
+
40
+
41
+ class SkillRegistry:
42
+ """
43
+ Skill registry for DevSquad.
44
+
45
+ Manages reusable skill definitions that can be:
46
+ - Auto-discovered from dispatch results
47
+ - Manually registered by users
48
+ - Matched to new tasks by category/tags
49
+ - Persisted to disk for cross-session reuse
50
+ """
51
+
52
+ def __init__(self, storage_path: str = "./skills"):
53
+ self.storage_path = Path(storage_path)
54
+ self.storage_path.mkdir(parents=True, exist_ok=True)
55
+ self.skills: Dict[str, SkillEntry] = {}
56
+ self.handlers: Dict[str, Callable] = {}
57
+ self._load()
58
+
59
+ def register(self, skill: SkillEntry, handler: Optional[Callable] = None) -> str:
60
+ if '..' in skill.skill_id or '/' in skill.skill_id or '\\' in skill.skill_id:
61
+ raise ValueError(f"Invalid skill_id: {skill.skill_id}")
62
+ self.skills[skill.skill_id] = skill
63
+ if handler:
64
+ self.handlers[skill.skill_id] = handler
65
+ self._save()
66
+ logger.info("Skill registered: %s (%s)", skill.name, skill.skill_id)
67
+ return skill.skill_id
68
+
69
+ def unregister(self, skill_id: str) -> bool:
70
+ if skill_id in self.skills:
71
+ del self.skills[skill_id]
72
+ self.handlers.pop(skill_id, None)
73
+ self._save()
74
+ return True
75
+ return False
76
+
77
+ def get(self, skill_id: str) -> Optional[SkillEntry]:
78
+ return self.skills.get(skill_id)
79
+
80
+ def execute(self, skill_id: str, **kwargs) -> Any:
81
+ skill = self.skills.get(skill_id)
82
+ if not skill:
83
+ raise ValueError(f"Skill not found: {skill_id}")
84
+
85
+ handler = self.handlers.get(skill_id)
86
+ if not handler:
87
+ raise ValueError(f"No handler for skill: {skill_id}")
88
+
89
+ skill.usage_count += 1
90
+ skill.last_used = datetime.now().isoformat()
91
+ self._save()
92
+
93
+ return handler(**kwargs)
94
+
95
+ def search(self, query: str = "", category: str = "", tags: List[str] = None) -> List[SkillEntry]:
96
+ results = list(self.skills.values())
97
+ if category:
98
+ results = [s for s in results if s.category == category]
99
+ if tags:
100
+ results = [s for s in results if any(t in s.tags for t in tags)]
101
+ if query:
102
+ q = query.lower()
103
+ results = [s for s in results if q in s.name.lower() or q in s.description.lower()]
104
+ results.sort(key=lambda s: s.confidence, reverse=True)
105
+ return results
106
+
107
+ def propose_from_result(self, name: str, description: str, category: str = "",
108
+ confidence: float = 0.0, tags: List[str] = None) -> SkillEntry:
109
+ skill = SkillEntry(
110
+ name=name, description=description, category=category,
111
+ confidence=confidence, tags=tags or [],
112
+ )
113
+ self.register(skill)
114
+ return skill
115
+
116
+ def list_skills(self, category: str = "") -> List[Dict[str, Any]]:
117
+ skills = list(self.skills.values())
118
+ if category:
119
+ skills = [s for s in skills if s.category == category]
120
+ return [s.to_dict() for s in skills]
121
+
122
+ def get_stats(self) -> Dict[str, Any]:
123
+ categories = {}
124
+ for s in self.skills.values():
125
+ categories[s.category] = categories.get(s.category, 0) + 1
126
+ return {
127
+ "total_skills": len(self.skills),
128
+ "categories": categories,
129
+ "with_handlers": len(self.handlers),
130
+ }
131
+
132
+ def _load(self):
133
+ registry_file = self.storage_path / "registry.json"
134
+ if registry_file.exists():
135
+ try:
136
+ with open(registry_file, 'r', encoding='utf-8') as f:
137
+ data = json.load(f)
138
+ for skill_data in data.get('skills', []):
139
+ skill = SkillEntry.from_dict(skill_data)
140
+ self.skills[skill.skill_id] = skill
141
+ except Exception as e:
142
+ logger.warning("Failed to load skill registry: %s", e)
143
+
144
+ def _save(self):
145
+ registry_file = self.storage_path / "registry.json"
146
+ try:
147
+ data = {'skills': [s.to_dict() for s in self.skills.values()]}
148
+ with open(registry_file, 'w', encoding='utf-8') as f:
149
+ json.dump(data, f, indent=2, ensure_ascii=False)
150
+ except Exception as e:
151
+ logger.warning("Failed to save skill registry: %s", e)