@johpaz/hive-sdk 0.0.12 → 0.0.15

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 (199) hide show
  1. package/.github/CODEOWNERS +9 -0
  2. package/.github/workflows/publish.yml +89 -0
  3. package/.github/workflows/version-bump.yml +102 -0
  4. package/CHANGELOG.md +38 -0
  5. package/README.md +158 -0
  6. package/bun.lock +543 -0
  7. package/bunfig.toml +7 -0
  8. package/docs/API-AGENTS.md +316 -0
  9. package/docs/API-CONTEXT-COMPILER.md +252 -0
  10. package/docs/API-DAG-SCHEDULER.md +273 -0
  11. package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
  12. package/docs/API-WORKERS-EVENTS.md +152 -0
  13. package/docs/INDEX.md +141 -0
  14. package/docs/README.md +68 -0
  15. package/package.json +54 -105
  16. package/packages/cli/package.json +17 -0
  17. package/packages/cli/src/commands/init.ts +56 -0
  18. package/packages/cli/src/commands/run.ts +45 -0
  19. package/packages/cli/src/commands/test.ts +42 -0
  20. package/packages/cli/src/commands/trace.ts +55 -0
  21. package/packages/cli/src/index.ts +43 -0
  22. package/packages/core/package.json +58 -0
  23. package/packages/core/src/ace/Curator.ts +158 -0
  24. package/packages/core/src/ace/Reflector.ts +200 -0
  25. package/packages/core/src/ace/Tracer.ts +100 -0
  26. package/packages/core/src/ace/index.ts +4 -0
  27. package/packages/core/src/agent/AgentRunner.ts +699 -0
  28. package/packages/core/src/agent/Compaction.ts +221 -0
  29. package/packages/core/src/agent/ContextCompiler.ts +567 -0
  30. package/packages/core/src/agent/ContextGuard.ts +91 -0
  31. package/packages/core/src/agent/ConversationStore.ts +244 -0
  32. package/packages/core/src/agent/Hooks.ts +166 -0
  33. package/packages/core/src/agent/NativeTools.ts +31 -0
  34. package/packages/core/src/agent/PromptBuilder.ts +169 -0
  35. package/packages/core/src/agent/Service.ts +267 -0
  36. package/packages/core/src/agent/StuckLoop.ts +133 -0
  37. package/packages/core/src/agent/index.ts +12 -0
  38. package/packages/core/src/agent/providers/LLMClient.ts +149 -0
  39. package/packages/core/src/agent/providers/anthropic.ts +212 -0
  40. package/packages/core/src/agent/providers/gemini.ts +215 -0
  41. package/packages/core/src/agent/providers/index.ts +199 -0
  42. package/packages/core/src/agent/providers/interface.ts +195 -0
  43. package/packages/core/src/agent/providers/ollama.ts +175 -0
  44. package/packages/core/src/agent/providers/openai-compat.ts +231 -0
  45. package/packages/core/src/agent/providers.ts +1 -0
  46. package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
  47. package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
  48. package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
  49. package/packages/core/src/agent/selectors/index.ts +6 -0
  50. package/packages/core/src/api/createAgent.test.ts +48 -0
  51. package/packages/core/src/api/createAgent.ts +122 -0
  52. package/packages/core/src/api/index.ts +2 -0
  53. package/packages/core/src/canvas/CanvasManager.ts +390 -0
  54. package/packages/core/src/canvas/a2ui-tools.ts +255 -0
  55. package/packages/core/src/canvas/canvas-tools.ts +448 -0
  56. package/packages/core/src/canvas/emitter.ts +149 -0
  57. package/packages/core/src/canvas/index.ts +6 -0
  58. package/packages/core/src/config/index.ts +2 -0
  59. package/packages/core/src/config/loader.ts +554 -0
  60. package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
  61. package/packages/core/src/ethics/EthicsGuard.ts +66 -0
  62. package/packages/core/src/ethics/index.ts +2 -0
  63. package/packages/core/src/gateway/channel-notify.test.ts +14 -0
  64. package/packages/core/src/gateway/channel-notify.ts +12 -0
  65. package/packages/core/src/gateway/index.ts +1 -0
  66. package/packages/core/src/index.ts +37 -0
  67. package/packages/core/src/mcp/MCPClient.ts +439 -0
  68. package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
  69. package/packages/core/src/mcp/config.ts +13 -0
  70. package/packages/core/src/mcp/hot-reload.ts +147 -0
  71. package/packages/core/src/mcp/index.ts +11 -0
  72. package/packages/core/src/mcp/logger.ts +42 -0
  73. package/packages/core/src/mcp/singleton.ts +21 -0
  74. package/packages/core/src/mcp/transports/index.ts +67 -0
  75. package/packages/core/src/mcp/transports/sse.ts +241 -0
  76. package/packages/core/src/mcp/transports/websocket.ts +159 -0
  77. package/packages/core/src/memory/Scratchpad.test.ts +47 -0
  78. package/packages/core/src/memory/Scratchpad.ts +37 -0
  79. package/packages/core/src/memory/Storage.ts +6 -0
  80. package/packages/core/src/memory/index.ts +2 -0
  81. package/packages/core/src/multimodal/VisionService.ts +293 -0
  82. package/packages/core/src/multimodal/index.ts +2 -0
  83. package/packages/core/src/multimodal/types.ts +28 -0
  84. package/packages/core/src/security/Pairing.ts +250 -0
  85. package/packages/core/src/security/RateLimit.ts +270 -0
  86. package/packages/core/src/security/index.ts +4 -0
  87. package/packages/core/src/skills/SkillLoader.ts +388 -0
  88. package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
  89. package/packages/core/src/skills/defineSkill.ts +18 -0
  90. package/packages/core/src/skills/index.ts +4 -0
  91. package/packages/core/src/state/index.ts +2 -0
  92. package/packages/core/src/state/store.ts +312 -0
  93. package/packages/core/src/storage/SQLiteStorage.ts +407 -0
  94. package/packages/core/src/storage/crypto.ts +101 -0
  95. package/packages/core/src/storage/index.ts +10 -0
  96. package/packages/core/src/storage/onboarding.ts +1603 -0
  97. package/packages/core/src/storage/schema.ts +689 -0
  98. package/packages/core/src/storage/seed.ts +740 -0
  99. package/packages/core/src/storage/usage.ts +374 -0
  100. package/packages/core/src/swarm/AgentBus.ts +460 -0
  101. package/packages/core/src/swarm/AgentExecutor.ts +53 -0
  102. package/packages/core/src/swarm/Coordinator.ts +251 -0
  103. package/packages/core/src/swarm/EventBridge.ts +122 -0
  104. package/packages/core/src/swarm/EventBus.ts +169 -0
  105. package/packages/core/src/swarm/TaskGraph.ts +192 -0
  106. package/packages/core/src/swarm/TaskNode.ts +97 -0
  107. package/packages/core/src/swarm/TaskResult.ts +22 -0
  108. package/packages/core/src/swarm/WorkerPool.ts +236 -0
  109. package/packages/core/src/swarm/errors.ts +37 -0
  110. package/packages/core/src/swarm/index.ts +30 -0
  111. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
  112. package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
  113. package/packages/core/src/swarm/presets/index.ts +4 -0
  114. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
  115. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
  116. package/packages/core/src/swarm/strategies/index.ts +3 -0
  117. package/packages/core/src/swarm/types.ts +164 -0
  118. package/packages/core/src/tools/ToolExecutor.ts +58 -0
  119. package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
  120. package/packages/core/src/tools/ToolRegistry.ts +61 -0
  121. package/packages/core/src/tools/agents/get-available-models.ts +118 -0
  122. package/packages/core/src/tools/agents/index.ts +715 -0
  123. package/packages/core/src/tools/bridge-events.ts +26 -0
  124. package/packages/core/src/tools/canvas/index.ts +375 -0
  125. package/packages/core/src/tools/cli/index.ts +142 -0
  126. package/packages/core/src/tools/codebridge/index.ts +342 -0
  127. package/packages/core/src/tools/core/index.ts +476 -0
  128. package/packages/core/src/tools/cron/index.ts +626 -0
  129. package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
  130. package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
  131. package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
  132. package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
  133. package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
  134. package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
  135. package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
  136. package/packages/core/src/tools/filesystem/index.ts +34 -0
  137. package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
  138. package/packages/core/src/tools/index.ts +231 -0
  139. package/packages/core/src/tools/meeting/index.ts +363 -0
  140. package/packages/core/src/tools/office/index.ts +47 -0
  141. package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
  142. package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
  143. package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
  144. package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
  145. package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
  146. package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
  147. package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
  148. package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
  149. package/packages/core/src/tools/projects/index.ts +37 -0
  150. package/packages/core/src/tools/projects/project-create.ts +94 -0
  151. package/packages/core/src/tools/projects/project-done.ts +66 -0
  152. package/packages/core/src/tools/projects/project-fail.ts +66 -0
  153. package/packages/core/src/tools/projects/project-list.ts +96 -0
  154. package/packages/core/src/tools/projects/project-update.ts +72 -0
  155. package/packages/core/src/tools/projects/task-create.ts +68 -0
  156. package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
  157. package/packages/core/src/tools/projects/task-update.ts +93 -0
  158. package/packages/core/src/tools/types.ts +39 -0
  159. package/packages/core/src/tools/voice/index.ts +104 -0
  160. package/packages/core/src/tools/web/browser-click.ts +78 -0
  161. package/packages/core/src/tools/web/browser-extract.ts +139 -0
  162. package/packages/core/src/tools/web/browser-navigate.ts +106 -0
  163. package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
  164. package/packages/core/src/tools/web/browser-script.ts +88 -0
  165. package/packages/core/src/tools/web/browser-service.ts +554 -0
  166. package/packages/core/src/tools/web/browser-type.ts +101 -0
  167. package/packages/core/src/tools/web/browser-wait.ts +136 -0
  168. package/packages/core/src/tools/web/index.ts +41 -0
  169. package/packages/core/src/tools/web/web-fetch.ts +78 -0
  170. package/packages/core/src/tools/web/web-search.ts +123 -0
  171. package/packages/core/src/utils/benchmark.ts +80 -0
  172. package/packages/core/src/utils/crypto.ts +73 -0
  173. package/packages/core/src/utils/date.ts +42 -0
  174. package/packages/core/src/utils/index.ts +10 -0
  175. package/packages/core/src/utils/logger.ts +389 -0
  176. package/packages/core/src/utils/retry.ts +70 -0
  177. package/packages/core/src/utils/toon.ts +253 -0
  178. package/packages/core/src/voice/index.ts +656 -0
  179. package/test/setup-db.ts +216 -0
  180. package/tsconfig.json +39 -0
  181. package/src/agents.ts +0 -1
  182. package/src/canvas.ts +0 -1
  183. package/src/channels.ts +0 -1
  184. package/src/config.ts +0 -1
  185. package/src/events.ts +0 -1
  186. package/src/gateway.ts +0 -1
  187. package/src/index.ts +0 -304
  188. package/src/mcp.ts +0 -1
  189. package/src/multimodal.ts +0 -1
  190. package/src/scheduler.ts +0 -1
  191. package/src/security.ts +0 -1
  192. package/src/skills.ts +0 -1
  193. package/src/state.ts +0 -1
  194. package/src/storage.ts +0 -1
  195. package/src/tools.ts +0 -1
  196. package/src/tts.ts +0 -1
  197. package/src/types.ts +0 -82
  198. package/src/utils.ts +0 -1
  199. package/src/voice.ts +0 -1
@@ -0,0 +1,147 @@
1
+ /**
2
+ * FTS5-based Playbook Rules Selector (ACE Curator)
3
+ *
4
+ * This module allows the Context Compiler to inject relevant evolved rules
5
+ * into the agent prompt based on semantic relevance to the current message.
6
+ */
7
+
8
+ import { getDb } from "../../storage/SQLiteStorage.ts"
9
+ import { logger } from "../../utils/logger.ts"
10
+
11
+ const log = logger.child("playbook-selector")
12
+
13
+ // ─── Types ───────────────────────────────────────────────────────────────────────
14
+
15
+ export interface PlaybookRule {
16
+ id: number
17
+ rule: string
18
+ category: string
19
+ applicable_to?: string
20
+ }
21
+
22
+ // ─── Configuration ─────────────────────────────────────────────────────────────
23
+
24
+ /** Maximum rules to inject per context window */
25
+ const MAX_RULES_PER_TURN = 5
26
+
27
+ /** Minimum bm25 score threshold for rules */
28
+ const MIN_RELEVANCE_THRESHOLD = -10 // Relaxed for better matching
29
+
30
+ // ─── Selection Logic ───────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Select relevant rules from the Playbook based on semantic matching
34
+ */
35
+ export function selectPlaybookRules(message: string): PlaybookRule[] {
36
+ const db = getDb()
37
+ const startTime = performance.now()
38
+
39
+ // Clean query — use prefix matching for consistency with skill-selector and tool-selector
40
+ const keywords = message
41
+ .toLowerCase()
42
+ // Keep only letters, numbers, spaces (strips ALL FTS5 special syntax)
43
+ .replace(/[^\p{L}\p{N}\s]/gu, " ")
44
+ .split(/\s+/)
45
+ .filter(w => w.length > 3)
46
+ .slice(0, 5)
47
+
48
+ if (keywords.length === 0) return []
49
+
50
+ // Use prefix matching for better recall (e.g., "program*" matches "programar", "programación")
51
+ const ftsQuery = keywords.map(w => `${w}*`).join(" OR ")
52
+
53
+ try {
54
+ // Query FTS table
55
+ const ftsResults = db.query(`
56
+ SELECT rowid, bm25(playbook_fts) as score
57
+ FROM playbook_fts
58
+ WHERE playbook_fts MATCH ?
59
+ ORDER BY score ASC
60
+ LIMIT ?
61
+ `).all(ftsQuery, MAX_RULES_PER_TURN) as Array<{ rowid: number; score: number }>
62
+
63
+ const relevantIds = ftsResults
64
+ .filter(r => r.score >= MIN_RELEVANCE_THRESHOLD)
65
+ .map(r => r.rowid)
66
+
67
+ if (relevantIds.length === 0) return []
68
+
69
+ // Fetch full rules
70
+ const rules = db.query(`
71
+ SELECT id, rule, category, applicable_to
72
+ FROM playbook
73
+ WHERE id IN (${relevantIds.map(() => '?').join(',')})
74
+ AND active = 1
75
+ `).all(...relevantIds) as PlaybookRule[]
76
+
77
+ const timing = performance.now() - startTime
78
+ log.info(`[playbook-selector] Selected ${rules.length} rules in ${timing.toFixed(2)}ms`)
79
+ if (rules.length > 0) {
80
+ log.debug(`[playbook-selector] Rules: ${rules.map(r => `[${r.id}] ${r.rule.substring(0, 60)}`).join(', ')}`)
81
+ }
82
+
83
+ return rules
84
+ } catch (err) {
85
+ log.error(`[playbook-selector] Failed to select rules:`, err)
86
+ return []
87
+ }
88
+ }
89
+
90
+ // ─── Sync Logic ───────────────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * Sync active playbook rules to FTS5 virtual table
94
+ */
95
+ export async function syncPlaybookToFTS(): Promise<void> {
96
+ const db = getDb()
97
+
98
+ try {
99
+ // Step 1: Get active rules
100
+ const rules = db.query(`
101
+ SELECT id, rule, category, applicable_to
102
+ FROM playbook
103
+ WHERE active = 1
104
+ `).all() as Array<{
105
+ id: number
106
+ rule: string
107
+ category: string
108
+ applicable_to: string
109
+ }>
110
+
111
+ if (rules.length === 0) {
112
+ log.debug(`[playbook-selector] No rules in playbook to sync`)
113
+ }
114
+
115
+ // Step 2: Atomic transaction for FTS5 sync
116
+ const syncTransaction = db.transaction(() => {
117
+ // Verify table exists
118
+ const tableCheck = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='playbook_fts'").get()
119
+ if (!tableCheck) {
120
+ throw new Error("playbook_fts table does not exist!")
121
+ }
122
+
123
+ // A: Clear existing data
124
+ db.run("DELETE FROM playbook_fts")
125
+
126
+ // B: Prepare insertion
127
+ const insert = db.prepare(`
128
+ INSERT INTO playbook_fts(rowid, rule, category, applicable_to)
129
+ VALUES (?, ?, ?, ?)
130
+ `)
131
+
132
+ // C: Re-populate
133
+ for (const item of rules) {
134
+ insert.run(item.id, item.rule, item.category, item.applicable_to)
135
+ }
136
+ })
137
+
138
+ // Execute transaction
139
+ syncTransaction()
140
+
141
+ log.info(`[playbook-selector] Atomic sync complete: ${rules.length} rules indexed in FTS5`)
142
+
143
+ } catch (err) {
144
+ log.error(`[playbook-selector] Transactional sync failed:`, err)
145
+ throw err
146
+ }
147
+ }
@@ -0,0 +1,478 @@
1
+ /**
2
+ * FTS5-based Dynamic Skill Selector Module
3
+ *
4
+ * Context Compiler Level 4 - Intelligent Skill Selection
5
+ *
6
+ * This module uses SQLite FTS5 bm25() scoring to select the most relevant
7
+ * skills (0-5) based on the user message, similar to tool selection.
8
+ *
9
+ * DESIGN DECISIONS:
10
+ *
11
+ * 1. Reads from skills table in database (not hardcoded catalog)
12
+ * 2. Maximum 5 skills per turn for balanced context injection
13
+ * 3. Relevance threshold for conversational messages
14
+ * 4. Uses skill descriptions for FTS5 matching
15
+ * 5. Returns skill content for injection into system prompt
16
+ */
17
+
18
+ import { getDb } from "../../storage/SQLiteStorage.ts"
19
+ import { logger } from "../../utils/logger.ts"
20
+
21
+ const log = logger.child("skill-selector")
22
+
23
+ // ─── Minimal Skill Set ─────────────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Skills mínimas que SIEMPRE están disponibles (asociadas a las 4 tools iniciales)
27
+ * - memory_manager: usa save_note (notas persistentes)
28
+ * - canvas_report: usa report_progress (reportes de progreso)
29
+ * - task_orchestrator: usa notify (comunicación entre agentes)
30
+ */
31
+ export const MINIMAL_SKILL_NAMES = new Set([
32
+ "memory_manager", // Asociada a save_note
33
+ "canvas_report", // Asociada a report_progress
34
+ "task_orchestrator", // Asociada a notify y agent coordination
35
+ ])
36
+
37
+ // ─── Types ───────────────────────────────────────────────────────────────────────
38
+
39
+ export interface SkillDescriptor {
40
+ id: string
41
+ name: string
42
+ description: string
43
+ category: string
44
+ tools: string
45
+ triggers: string
46
+ preferred_agents: string
47
+ body: string
48
+ version: string
49
+ version_num: number
50
+ active: number
51
+ }
52
+
53
+ export interface SelectedSkill {
54
+ id: string
55
+ name: string
56
+ score: number
57
+ category: string
58
+ description: string
59
+ body: string
60
+ }
61
+
62
+ export interface SkillSelectorResult {
63
+ skills: SkillDescriptor[]
64
+ selected: SelectedSkill[]
65
+ reasoning: string
66
+ timingMs: number
67
+ }
68
+
69
+ // ─── Configuration ─────────────────────────────────────────────────────────
70
+
71
+ /** Maximum skills to return per message */
72
+ const MAX_SKILLS_PER_TURN = 4 // Increased from 2 to allow more skills
73
+
74
+ /**
75
+ * Minimum bm25 score threshold. Below this = conversational, no skills needed.
76
+ *
77
+ * CRITICAL: bm25() returns NEGATIVE scores where closer to 0 = more relevant.
78
+ * - Score of -5 is MORE relevant than -20
79
+ * - We use -15 as threshold to allow reasonable matching while filtering noise
80
+ */
81
+ const MIN_RELEVANCE_THRESHOLD = -15 // Increased from -5 to allow more matches
82
+
83
+ /** Stopwords to filter out before FTS5 query construction */
84
+ const STOPWORDS = new Set([
85
+ "que", "con", "para", "por", "una", "uno", "los", "las", "del",
86
+ "como", "esta", "esto", "ese", "eso", "the", "and", "for",
87
+ "with", "this", "that", "have", "will", "also", "de", "en",
88
+ "el", "la", "se", "su", "sus", "al", "es", "son", "pero",
89
+ "más", "mas", "ya", "yo", "tu", "te", "ti", "mi", "me",
90
+ "hola", "hi", "hello", "hey", "gracias", "thank", "please",
91
+ "ok", "okay", "yes", "si", "no", "bien", "good", "great",
92
+ "puedes", "necesito", "quiero", "podés", "necesitás", "querés",
93
+ ])
94
+
95
+ /** Conversational patterns that should return empty skill list */
96
+ const CONVERSATIONAL_PATTERNS = [
97
+ /^(hola|hi|hello|hey|buenos? días?|buenas? noches?|qué tal|howdy)/i,
98
+ /^(gracias|thank you|thanks|muchas gracias|muchas thanks)/i,
99
+ /^(cómo estás?|how are you?|qué流水|you doing|qué cuentas)/i,
100
+ /^(sí|yes|ok|okay|de acuerdo|perfecto|claro|por supuesto)/i,
101
+ /^(adiós|bye|nos vemos|see you|later|chau)/i,
102
+ /^(entiendo|understand|i see|ya veo|got it)/i,
103
+ /^(bien|good|great|excelente|awesome|perfect)/i,
104
+ /^(?:\?|¿)$/, // Just a question mark
105
+ ]
106
+
107
+ // ─── Helper Functions ───────────────────────────────────────────────────────
108
+
109
+ /**
110
+ * Check if message is purely conversational (no skills needed)
111
+ */
112
+ function isConversational(message: string): boolean {
113
+ const trimmed = message.trim()
114
+
115
+ // Empty or very short messages
116
+ if (trimmed.length < 2) return true
117
+
118
+ // Check conversational patterns
119
+ for (const pattern of CONVERSATIONAL_PATTERNS) {
120
+ if (pattern.test(trimmed)) {
121
+ log.debug(`[skill-selector] Message matched conversational pattern: ${pattern}`)
122
+ return true
123
+ }
124
+ }
125
+
126
+ // Check if all words are stopwords (likely conversational)
127
+ const words = trimmed.toLowerCase().split(/\s+/)
128
+ const meaningfulWords = words.filter(w => w.length > 2 && !STOPWORDS.has(w))
129
+ if (meaningfulWords.length === 0) {
130
+ log.debug(`[skill-selector] All words are stopwords - conversational`)
131
+ return true
132
+ }
133
+
134
+ return false
135
+ }
136
+
137
+ /**
138
+ * Build FTS5 query from user message
139
+ *
140
+ * Uses prefix matching for better recall:
141
+ * - "generar" matches "generando", "generación", "genera"
142
+ * - "código" matches "codigos", "codificar"
143
+ */
144
+ function buildFTSQuery(message: string): string {
145
+ const words = message
146
+ .toLowerCase()
147
+ .replace(/[^\p{L}\p{N}\s]/gu, " ")
148
+ .split(/\s+/)
149
+ .filter((w) => w.length > 2 && !STOPWORDS.has(w))
150
+ .slice(0, 8)
151
+
152
+ if (words.length === 0) return ""
153
+
154
+ // Use prefix matching for better recall (e.g., "gener*" matches "generar", "generando", "generación")
155
+ return words.map(w => `${w}*`).join(" OR ")
156
+ }
157
+
158
+ /**
159
+ * Check if message matches explicit triggers from a skill
160
+ */
161
+ function matchTriggers(message: string, triggersJson: string | null): boolean {
162
+ if (!triggersJson) return false
163
+
164
+ try {
165
+ // Triggers are stored as comma-separated string in DB (e.g., "trigger1,trigger2")
166
+ const triggers: string[] = triggersJson.split(",").map(t => t.trim()).filter(t => t.length > 0)
167
+ if (triggers.length === 0) return false
168
+
169
+ const lowerMessage = message.toLowerCase()
170
+ return triggers.some(trigger =>
171
+ lowerMessage.includes(trigger.toLowerCase())
172
+ )
173
+ } catch (err) {
174
+ log.warn(`[skill-selector] Failed to parse triggers: ${(err as Error).message}`)
175
+ return false
176
+ }
177
+ }
178
+
179
+ // ─── Main Selection Function ─────────────────────────────────────────────────
180
+
181
+ /**
182
+ * Select skills for a given user message using hybrid matching:
183
+ * 1. First check explicit triggers (high confidence match)
184
+ * 2. Fallback to FTS5 bm25() scoring for semantic matching
185
+ *
186
+ * @param userMessage - The raw user message
187
+ * @returns Array of 0-5 selected skills with scores
188
+ *
189
+ * ALGORITHM:
190
+ * 1. If conversational → return []
191
+ * 2. Check explicit triggers from all enabled skills
192
+ * 3. If trigger match found → return matching skill immediately
193
+ * 4. Build FTS5 query from message keywords
194
+ * 5. Query skills_fts with bm25() scoring
195
+ * 6. Filter results below MIN_RELEVANCE_THRESHOLD
196
+ * 7. Return top MAX_SKILLS_PER_TURN results
197
+ */
198
+ export function selectSkills(userMessage: string): SkillDescriptor[] {
199
+ const startTime = performance.now()
200
+
201
+ log.debug(`[skill-selector] Processing user message: "${userMessage.substring(0, 100)}"`)
202
+
203
+ // Step 1: Check if conversational
204
+ if (isConversational(userMessage)) {
205
+ log.debug(`[skill-selector] Conversational message, returning empty array`)
206
+ return []
207
+ }
208
+
209
+ // Step 2: Check explicit triggers first (high priority)
210
+ const db = getDb()
211
+ const allSkills = db.query(`
212
+ SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
213
+ FROM skills
214
+ WHERE active = 1
215
+ `).all() as SkillDescriptor[]
216
+
217
+ // Check trigger match - if found, return immediately with high confidence
218
+ for (const skill of allSkills) {
219
+ if (skill.triggers && matchTriggers(userMessage, skill.triggers)) {
220
+ log.info(`[skill-selector] Trigger match found: ${skill.name}`)
221
+ return [skill]
222
+ }
223
+ }
224
+
225
+ // Step 3: Build FTS5 query for semantic matching
226
+ const ftsQuery = buildFTSQuery(userMessage)
227
+ if (!ftsQuery) {
228
+ log.debug(`[skill-selector] No valid FTS query terms, returning empty array`)
229
+ return []
230
+ }
231
+
232
+ log.debug(`[skill-selector] FTS query: "${ftsQuery}"`)
233
+
234
+ // Step 4: Execute FTS5 query with bm25 scoring
235
+ // Use bm25() with column weights for relevance scoring
236
+ // FTS5 table columns: id, name, description, category, tools, triggers, body
237
+ // Weights: id=1.0, name=4.0, description=5.0, category=1.0, tools=1.0, triggers=5.0, body=2.0
238
+ // Higher weight on description (5.0) and triggers (5.0) for best semantic matching
239
+ const ftsResults = db.query(`
240
+ SELECT id, bm25(skills_fts, 1.0, 4.0, 5.0, 1.0, 1.0, 5.0, 2.0) as bm25_score
241
+ FROM skills_fts
242
+ WHERE skills_fts MATCH ?
243
+ ORDER BY bm25_score ASC
244
+ LIMIT 20
245
+ `).all(ftsQuery) as { id: string; bm25_score: number }[]
246
+
247
+ if (ftsResults.length === 0) {
248
+ log.debug(`[skill-selector] No FTS matches, returning empty array`)
249
+ return []
250
+ }
251
+
252
+ // Log raw scores for debugging
253
+ log.info(`[skill-selector] Raw FTS scores: ${ftsResults.slice(0, 10).map(r => `id=${r.id}, score=${r.bm25_score.toFixed(2)}`).join(", ")}`)
254
+
255
+ // Step 5: Apply relevance threshold filter
256
+ const relevantResults = ftsResults.filter(r => r.bm25_score >= MIN_RELEVANCE_THRESHOLD)
257
+
258
+ if (relevantResults.length === 0) {
259
+ log.debug(`[skill-selector] All results below threshold ${MIN_RELEVANCE_THRESHOLD}, returning empty`)
260
+ return []
261
+ }
262
+
263
+ // Step 6: Fetch full skill details from database
264
+ const skillIds = relevantResults.map(r => r.id)
265
+
266
+ let dbSkills: SkillDescriptor[] = []
267
+ try {
268
+ const db = getDb()
269
+ dbSkills = db.query(`
270
+ SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
271
+ FROM skills
272
+ WHERE id IN (${skillIds.map(() => '?').join(',')})
273
+ AND active = 1
274
+ `).all(...skillIds) as SkillDescriptor[]
275
+ } catch (err) {
276
+ log.warn(`[skill-selector] Failed to fetch skills from DB:`, err)
277
+ return []
278
+ }
279
+
280
+ // Map scores to skills
281
+ const skillMap = new Map(dbSkills.map(s => [s.id, s]))
282
+ const scoredSkills: SelectedSkill[] = []
283
+
284
+ for (const result of relevantResults) {
285
+ const skill = skillMap.get(result.id)
286
+ if (skill) {
287
+ scoredSkills.push({
288
+ id: skill.id,
289
+ name: skill.name,
290
+ score: result.bm25_score,
291
+ category: skill.category,
292
+ description: skill.description || "",
293
+ body: skill.body,
294
+ })
295
+ }
296
+ }
297
+
298
+ // Step 7: Take top N skills
299
+ const topSkills = scoredSkills.slice(0, MAX_SKILLS_PER_TURN)
300
+
301
+ // Step 8: Return as SkillDescriptor array
302
+ const result = topSkills.map(t => skillMap.get(t.id)!).filter(Boolean)
303
+
304
+ const timing = performance.now() - startTime
305
+
306
+ if (result.length > 0) {
307
+ log.info(`[skill-selector] Selected ${result.length} skills in ${timing.toFixed(2)}ms:`,
308
+ result.map(s => ({ name: s.name, category: s.category })))
309
+ } else {
310
+ log.debug(`[skill-selector] No skills selected, returning empty array in ${timing.toFixed(2)}ms`)
311
+ }
312
+
313
+ return result
314
+ }
315
+
316
+ // ─── Minimal Skills Loader ───────────────────────────────────────────────────
317
+
318
+ /**
319
+ * Load minimal skills that are ALWAYS available (associated with MINIMAL_TOOLS)
320
+ * These are loaded at startup, not via FTS5 search.
321
+ *
322
+ * @returns Array of minimal skills (memory_manager, canvas_report, task_orchestrator)
323
+ */
324
+ export function getMinimalSkills(): SkillDescriptor[] {
325
+ const db = getDb()
326
+
327
+ try {
328
+ const placeholders = Array.from(MINIMAL_SKILL_NAMES).map(() => "?").join(",")
329
+ const skills = db.query(`
330
+ SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
331
+ FROM skills
332
+ WHERE name IN (${placeholders})
333
+ AND active = 1
334
+ `).all(...MINIMAL_SKILL_NAMES) as SkillDescriptor[]
335
+
336
+ log.info(`[skill-selector] Loaded ${skills.length} minimal skills: ${skills.map(s => s.name).join(", ")}`)
337
+ return skills
338
+ } catch (err) {
339
+ log.error(`[skill-selector] Failed to load minimal skills:`, err)
340
+ return []
341
+ }
342
+ }
343
+
344
+ // ─── Sync Skills to FTS5 ───────────────────────────────────────────────────
345
+
346
+ /**
347
+ * Sync all enabled skills from database to FTS5
348
+ * Should be called on initialization from gateway/initializer.ts
349
+ * The skills_fts table is created by schema.ts (v0.0.28 includes description)
350
+ */
351
+ export async function syncSkillsToFTS(): Promise<void> {
352
+ const db = getDb()
353
+
354
+ try {
355
+ // Step 1: Get all enabled skills from database (v0.0.28 schema with description)
356
+ const dbSkills = db.query(`
357
+ SELECT id, name, description, category, tools, triggers, body
358
+ FROM skills
359
+ WHERE active = 1
360
+ `).all() as Array<{
361
+ id: string
362
+ name: string
363
+ description: string
364
+ category: string
365
+ tools: string
366
+ triggers: string
367
+ body: string
368
+ }>
369
+
370
+ if (dbSkills.length === 0) {
371
+ log.debug(`[skill-selector] No skills found in DB to sync`)
372
+ }
373
+
374
+ // Step 2: Atomic transaction for FTS5 sync
375
+ const syncTransaction = db.transaction(() => {
376
+ // Verify table exists
377
+ const tableCheck = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='skills_fts'").get()
378
+ if (!tableCheck) {
379
+ throw new Error("skills_fts table does not exist!")
380
+ }
381
+
382
+ // A: Clear existing data
383
+ db.run("DELETE FROM skills_fts")
384
+
385
+ // B: Prepare insertion (v0.0.28 schema with description)
386
+ const insert = db.prepare(`
387
+ INSERT INTO skills_fts(id, name, description, category, tools, triggers, body)
388
+ VALUES (?, ?, ?, ?, ?, ?, ?)
389
+ `)
390
+
391
+ // C: Re-populate
392
+ for (const skill of dbSkills) {
393
+ insert.run(
394
+ skill.id,
395
+ skill.name,
396
+ skill.description || "",
397
+ skill.category,
398
+ skill.tools,
399
+ skill.triggers,
400
+ skill.body
401
+ )
402
+ }
403
+ })
404
+
405
+ // Execute transaction
406
+ syncTransaction()
407
+
408
+ log.info(`[skill-selector] Atomic sync complete: ${dbSkills.length} skills indexed in FTS5`)
409
+
410
+ } catch (err) {
411
+ log.error(`[skill-selector] Transactional sync failed:`, err)
412
+ throw err // Re-throw to inform initializer
413
+ }
414
+ }
415
+ // ─── Initialization ───────────────────────────────────────────────────────
416
+
417
+ /**
418
+ * Initialize the skill selector
419
+ * DEPRECATED: syncSkillsToFTS() is now called from gateway/initializer.ts
420
+ * This function is kept for backward compatibility but is no longer needed
421
+ */
422
+ export function initializeSkillSelector(): void {
423
+ log.info(`[skill-selector] Initializing skill selector (deprecated - sync is done in gateway/initializer.ts)`)
424
+ // syncSkillsToFTS() - No longer needed here, done in gateway/initializer.ts
425
+ }
426
+
427
+ // ─── Debug/Test Helpers ─────────────────────────────────────────────────────
428
+
429
+ /**
430
+ * Get all enabled skills from database (for debugging/testing)
431
+ */
432
+ export function getAllSkillsFromDB(): SkillDescriptor[] {
433
+ try {
434
+ const db = getDb()
435
+ return db.query(`
436
+ SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
437
+ FROM skills
438
+ WHERE active = 1
439
+ `).all() as SkillDescriptor[]
440
+ } catch (err) {
441
+ log.error(`[skill-selector] Failed to fetch skills:`, err)
442
+ return []
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Get skill by name
448
+ */
449
+ export function getSkillByName(name: string): SkillDescriptor | undefined {
450
+ try {
451
+ const db = getDb()
452
+ return db.query(`
453
+ SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
454
+ FROM skills
455
+ WHERE name = ? AND active = 1
456
+ `).get(name) as SkillDescriptor | undefined
457
+ } catch (err) {
458
+ log.error(`[skill-selector] Failed to fetch skill by name:`, err)
459
+ return undefined
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Get skills by category
465
+ */
466
+ export function getSkillsByCategory(category: string): SkillDescriptor[] {
467
+ try {
468
+ const db = getDb()
469
+ return db.query(`
470
+ SELECT id, name, description, category, tools, triggers, preferred_agents, body, version, version_num, active
471
+ FROM skills
472
+ WHERE category = ? AND active = 1
473
+ `).all(category) as SkillDescriptor[]
474
+ } catch (err) {
475
+ log.error(`[skill-selector] Failed to fetch skills by category:`, err)
476
+ return []
477
+ }
478
+ }