nexo-brain 2.1.0 → 2.3.0

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 (297) hide show
  1. package/README.md +7 -7
  2. package/bin/nexo-brain.js +53 -26
  3. package/package.json +1 -1
  4. package/scripts/migrate-to-unified 2.sh +813 -0
  5. package/scripts/migrate-v1.5-to-v1.6 2.py +778 -0
  6. package/scripts/migrate-v1.7-to-v1.8 2.py +214 -0
  7. package/scripts/migrate-v1.7-to-v1.8.py +2 -2
  8. package/scripts/nexo-preflight.sh +236 -0
  9. package/scripts/pre-commit-check 2.sh +55 -0
  10. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  11. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  12. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  13. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  14. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  15. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  16. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  17. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  18. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  19. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  20. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  21. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  22. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  23. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  24. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  25. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  26. package/src/auto_close_sessions 2.py +159 -0
  27. package/src/auto_update 2.py +634 -0
  28. package/src/auto_update.py +25 -0
  29. package/src/claim_graph 2.py +323 -0
  30. package/src/cognitive/__init__ 2.py +62 -0
  31. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  32. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  33. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  34. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  35. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  36. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  37. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  38. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  39. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  40. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  41. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  42. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  43. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  44. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  45. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  46. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  47. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  48. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  49. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  50. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  51. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  52. package/src/cognitive/_core 2.py +567 -0
  53. package/src/cognitive/_decay 2.py +382 -0
  54. package/src/cognitive/_ingest 2.py +892 -0
  55. package/src/cognitive/_memory 2.py +912 -0
  56. package/src/cognitive/_search 2.py +949 -0
  57. package/src/cognitive/_trust 2.py +464 -0
  58. package/src/cognitive/_trust.py +10 -36
  59. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  60. package/src/crons/manifest 2.json +106 -0
  61. package/src/crons/manifest.json +6 -13
  62. package/src/crons/sync 2.py +217 -0
  63. package/src/crons/sync.py +151 -6
  64. package/src/dashboard/__init__ 2.py +0 -0
  65. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  66. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  67. package/src/dashboard/app 2.py +789 -0
  68. package/src/db/__init__ 2.py +89 -0
  69. package/src/db/__init__.py +13 -0
  70. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  71. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  72. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  73. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  74. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  75. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  76. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  77. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  78. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  79. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  80. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  81. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  82. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  83. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  84. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  85. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  86. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  87. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  90. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  91. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  92. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  93. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  94. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  95. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  96. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  98. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  99. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  110. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  111. package/src/db/_core 2.py +417 -0
  112. package/src/db/_credentials 2.py +124 -0
  113. package/src/db/_cron_runs.py +74 -0
  114. package/src/db/_entities 2.py +178 -0
  115. package/src/db/_episodic 2.py +738 -0
  116. package/src/db/_episodic.py +40 -6
  117. package/src/db/_evolution 2.py +54 -0
  118. package/src/db/_fts 2.py +406 -0
  119. package/src/db/_learnings 2.py +168 -0
  120. package/src/db/_reminders 2.py +338 -0
  121. package/src/db/_schema 2.py +364 -0
  122. package/src/db/_schema.py +64 -0
  123. package/src/db/_sessions 2.py +300 -0
  124. package/src/db/_skills.py +514 -0
  125. package/src/db/_tasks 2.py +91 -0
  126. package/src/evolution_cycle 2.py +266 -0
  127. package/src/hnsw_index 2.py +254 -0
  128. package/src/hooks/auto_capture 2.py +208 -0
  129. package/src/hooks/caffeinate-guard 2.sh +8 -0
  130. package/src/hooks/capture-session 2.sh +21 -0
  131. package/src/hooks/capture-session.sh +2 -0
  132. package/src/hooks/capture-tool-logs 2.sh +127 -0
  133. package/src/hooks/capture-tool-logs.sh +3 -2
  134. package/src/hooks/daily-briefing-check 2.sh +33 -0
  135. package/src/hooks/inbox-hook 2.sh +76 -0
  136. package/src/hooks/inbox-hook.sh +3 -2
  137. package/src/hooks/post-compact 2.sh +148 -0
  138. package/src/hooks/post-compact.sh +1 -1
  139. package/src/hooks/pre-compact 2.sh +151 -0
  140. package/src/hooks/pre-compact.sh +1 -1
  141. package/src/hooks/session-start 2.sh +268 -0
  142. package/src/hooks/session-start.sh +6 -3
  143. package/src/hooks/session-stop 2.sh +140 -0
  144. package/src/hooks/session-stop.sh +14 -102
  145. package/src/kg_populate 2.py +290 -0
  146. package/src/knowledge_graph 2.py +257 -0
  147. package/src/maintenance 2.py +59 -0
  148. package/src/migrate_embeddings 2.py +122 -0
  149. package/src/plugin_loader 2.py +202 -0
  150. package/src/plugins/__init__ 2.py +0 -0
  151. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  152. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  154. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  155. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  156. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  157. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  160. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  163. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  183. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  185. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  187. package/src/plugins/adaptive_mode 2.py +805 -0
  188. package/src/plugins/agents 2.py +52 -0
  189. package/src/plugins/artifact_registry 2.py +450 -0
  190. package/src/plugins/backup 2.py +104 -0
  191. package/src/plugins/cognitive_memory 2.py +564 -0
  192. package/src/plugins/core_rules 2.py +252 -0
  193. package/src/plugins/cortex 2.py +299 -0
  194. package/src/plugins/entities 2.py +67 -0
  195. package/src/plugins/episodic_memory 2.py +533 -0
  196. package/src/plugins/episodic_memory.py +5 -3
  197. package/src/plugins/evolution 2.py +115 -0
  198. package/src/plugins/guard 2.py +746 -0
  199. package/src/plugins/knowledge_graph_tools 2.py +105 -0
  200. package/src/plugins/preferences 2.py +47 -0
  201. package/src/plugins/schedule.py +212 -0
  202. package/src/plugins/skills.py +264 -0
  203. package/src/plugins/update 2.py +256 -0
  204. package/src/requirements 2.txt +12 -0
  205. package/src/rules/__init__ 2.py +0 -0
  206. package/src/rules/core-rules 2.json +331 -0
  207. package/src/rules/migrate 2.py +207 -0
  208. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  209. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  210. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  211. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  212. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  213. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  214. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  215. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  216. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  217. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  218. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  219. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  220. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  221. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  222. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  223. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  229. package/src/scripts/check-context 2.py +264 -0
  230. package/src/scripts/deep-sleep/apply_findings.py +168 -8
  231. package/src/scripts/deep-sleep/collect.py +33 -11
  232. package/src/scripts/deep-sleep/extract-prompt.md +38 -0
  233. package/src/scripts/deep-sleep/extract.py +80 -8
  234. package/src/scripts/deep-sleep/synthesize-prompt.md +59 -2
  235. package/src/scripts/deep-sleep/synthesize.py +3 -1
  236. package/src/scripts/nexo-auto-update 2.py +6 -0
  237. package/src/scripts/nexo-backup 2.sh +25 -0
  238. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  239. package/src/scripts/nexo-catchup 2.py +242 -0
  240. package/src/scripts/nexo-catchup.py +65 -29
  241. package/src/scripts/nexo-cognitive-decay 2.py +182 -0
  242. package/src/scripts/nexo-cron-wrapper.sh +53 -0
  243. package/src/scripts/nexo-daily-self-audit 2.py +552 -0
  244. package/src/scripts/nexo-daily-self-audit.py +4 -2
  245. package/src/scripts/nexo-deep-sleep 2.sh +97 -0
  246. package/src/scripts/nexo-deep-sleep.sh +66 -77
  247. package/src/scripts/nexo-evolution-run 2.py +597 -0
  248. package/src/scripts/nexo-evolution-run.py +13 -0
  249. package/src/scripts/nexo-followup-hygiene 2.py +112 -0
  250. package/src/scripts/nexo-immune 2.py +927 -0
  251. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  252. package/src/scripts/nexo-install 2.py +6 -0
  253. package/src/scripts/nexo-learning-housekeep 2.py +245 -0
  254. package/src/scripts/nexo-learning-housekeep.py +156 -1
  255. package/src/scripts/nexo-learning-validator 2.py +207 -0
  256. package/src/scripts/nexo-learning-validator.py +19 -0
  257. package/src/scripts/nexo-migrate 2.py +232 -0
  258. package/src/scripts/nexo-postmortem-consolidator 2.py +421 -0
  259. package/src/scripts/nexo-postmortem-consolidator.py +3 -2
  260. package/src/scripts/nexo-pre-commit 2.py +120 -0
  261. package/src/scripts/nexo-prevent-sleep 2.sh +29 -0
  262. package/src/scripts/nexo-proactive-dashboard 2.py +345 -0
  263. package/src/scripts/nexo-reflection 2.py +253 -0
  264. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  265. package/src/scripts/nexo-send-email 2.py +25 -0
  266. package/src/scripts/nexo-send-reply 2.py +178 -0
  267. package/src/scripts/nexo-sleep 2.py +592 -0
  268. package/src/scripts/nexo-sleep.py +16 -11
  269. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  270. package/src/scripts/nexo-synthesis 2.py +253 -0
  271. package/src/scripts/nexo-synthesis.py +46 -3
  272. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  273. package/src/scripts/nexo-update 2.sh +161 -0
  274. package/src/scripts/nexo-watchdog 2.sh +878 -0
  275. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  276. package/src/scripts/nexo-watchdog.sh +72 -19
  277. package/src/server 2.py +733 -0
  278. package/src/server.py +11 -2
  279. package/src/storage_router 2.py +32 -0
  280. package/src/tools_coordination 2.py +102 -0
  281. package/src/tools_credentials 2.py +68 -0
  282. package/src/tools_learnings 2.py +220 -0
  283. package/src/tools_menu 2.py +227 -0
  284. package/src/tools_reminders 2.py +86 -0
  285. package/src/tools_reminders_crud 2.py +159 -0
  286. package/src/tools_reminders_crud.py +7 -0
  287. package/src/tools_sessions 2.py +476 -0
  288. package/src/tools_task_history 2.py +57 -0
  289. package/templates/CLAUDE.md 2.template +63 -0
  290. package/templates/openclaw 2.json +13 -0
  291. package/tests/__init__ 2.py +0 -0
  292. package/tests/conftest 2.py +71 -0
  293. package/tests/test_cognitive 2.py +205 -0
  294. package/tests/test_knowledge_graph 2.py +140 -0
  295. package/tests/test_migrations 2.py +137 -0
  296. package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
  297. /package/src/scripts/{nexo-github-monitor.py → nexo-github-monitor 2.py} +0 -0
@@ -0,0 +1,514 @@
1
+ """NEXO DB — Skills module.
2
+
3
+ Skill Auto-Creation system: reusable procedures extracted from complex tasks.
4
+ Skills are procedural (step-by-step how-tos) vs learnings which are declarative.
5
+
6
+ Pipeline: trace → draft → published → archived, fully autonomous.
7
+ Trust score with decay controls quality — no human approval gates.
8
+
9
+ Promotion: draft + 2 successful uses in distinct contexts → published.
10
+ Degradation: trust < 20 → archived. Archived + 60 days unused → purge.
11
+ """
12
+ import json
13
+ import datetime
14
+ from db._core import get_db
15
+ from db._fts import fts_upsert, fts_search
16
+
17
+
18
+ # ── Constants ──────────────────────────────────────────────────────
19
+
20
+ VALID_LEVELS = {'trace', 'draft', 'published', 'archived'}
21
+ TRUST_ON_SUCCESS = 5
22
+ TRUST_ON_FAILURE = -10
23
+ TRUST_INITIAL = 50
24
+ TRUST_ARCHIVE_THRESHOLD = 20
25
+ PROMOTION_USES_REQUIRED = 2
26
+
27
+
28
+ # ── CRUD ───────────────────────────────────────────────────────────
29
+
30
+ def create_skill(
31
+ skill_id: str,
32
+ name: str,
33
+ description: str = '',
34
+ level: str = 'trace',
35
+ tags: list | str = '[]',
36
+ trigger_patterns: list | str = '[]',
37
+ source_sessions: list | str = '[]',
38
+ linked_learnings: list | str = '[]',
39
+ file_path: str = '',
40
+ trust_score: int = TRUST_INITIAL,
41
+ ) -> dict:
42
+ """Create a new skill entry."""
43
+ if level not in VALID_LEVELS:
44
+ return {"error": f"level must be one of: {', '.join(sorted(VALID_LEVELS))}"}
45
+
46
+ tags_json = json.dumps(tags) if isinstance(tags, list) else tags
47
+ trigger_json = json.dumps(trigger_patterns) if isinstance(trigger_patterns, list) else trigger_patterns
48
+ sessions_json = json.dumps(source_sessions) if isinstance(source_sessions, list) else source_sessions
49
+ learnings_json = json.dumps(linked_learnings) if isinstance(linked_learnings, list) else linked_learnings
50
+
51
+ conn = get_db()
52
+ conn.execute(
53
+ """INSERT INTO skills
54
+ (id, name, description, level, trust_score, file_path, tags,
55
+ trigger_patterns, source_sessions, linked_learnings)
56
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
57
+ (skill_id, name, description, level, trust_score, file_path,
58
+ tags_json, trigger_json, sessions_json, learnings_json),
59
+ )
60
+ conn.commit()
61
+
62
+ # FTS index
63
+ body = f"{description} {tags_json} {trigger_json}"
64
+ fts_upsert("skill", skill_id, name, body, "skill", commit=False)
65
+
66
+ row = conn.execute("SELECT * FROM skills WHERE id = ?", (skill_id,)).fetchone()
67
+ return dict(row) if row else {"id": skill_id, "status": "created"}
68
+
69
+
70
+ def get_skill(skill_id: str) -> dict | None:
71
+ """Get a skill by ID."""
72
+ conn = get_db()
73
+ row = conn.execute("SELECT * FROM skills WHERE id = ?", (skill_id,)).fetchone()
74
+ return dict(row) if row else None
75
+
76
+
77
+ def list_skills(level: str = '', tag: str = '') -> list[dict]:
78
+ """List skills, optionally filtered by level or tag."""
79
+ conn = get_db()
80
+ conditions = []
81
+ params = []
82
+
83
+ if level:
84
+ conditions.append("level = ?")
85
+ params.append(level)
86
+ if tag:
87
+ conditions.append("tags LIKE ?")
88
+ params.append(f'%"{tag}"%')
89
+
90
+ where = "WHERE " + " AND ".join(conditions) if conditions else ""
91
+ rows = conn.execute(
92
+ f"SELECT * FROM skills {where} ORDER BY trust_score DESC, last_used_at DESC",
93
+ tuple(params),
94
+ ).fetchall()
95
+ return [dict(r) for r in rows]
96
+
97
+
98
+ def search_skills(query: str, level: str = '') -> list[dict]:
99
+ """Search skills using FTS5 for ranked results. Falls back to LIKE."""
100
+ fts_results = fts_search(query, source_filter="skill", limit=20)
101
+ if fts_results:
102
+ conn = get_db()
103
+ ids = [r['source_id'] for r in fts_results]
104
+ placeholders = ','.join('?' * len(ids))
105
+ sql = f"SELECT * FROM skills WHERE id IN ({placeholders})"
106
+ params = list(ids)
107
+ if level:
108
+ sql += " AND level = ?"
109
+ params.append(level)
110
+ sql += " ORDER BY trust_score DESC"
111
+ rows = conn.execute(sql, params).fetchall()
112
+ return [dict(r) for r in rows]
113
+
114
+ # Fallback to LIKE
115
+ conn = get_db()
116
+ words = query.strip().split()
117
+ if not words:
118
+ return []
119
+ conditions = []
120
+ params = []
121
+ for word in words:
122
+ p = f"%{word}%"
123
+ conditions.append("(name LIKE ? OR description LIKE ? OR tags LIKE ? OR trigger_patterns LIKE ?)")
124
+ params.extend([p, p, p, p])
125
+ where = " AND ".join(conditions)
126
+ if level:
127
+ where = f"level = ? AND ({where})"
128
+ params.insert(0, level)
129
+ rows = conn.execute(
130
+ f"SELECT * FROM skills WHERE {where} ORDER BY trust_score DESC",
131
+ params,
132
+ ).fetchall()
133
+ return [dict(r) for r in rows]
134
+
135
+
136
+ def update_skill(skill_id: str, **kwargs) -> dict:
137
+ """Update any fields of a skill."""
138
+ conn = get_db()
139
+ row = conn.execute("SELECT * FROM skills WHERE id = ?", (skill_id,)).fetchone()
140
+ if not row:
141
+ return {"error": f"Skill {skill_id} not found"}
142
+
143
+ allowed = {
144
+ "name", "description", "level", "trust_score", "file_path",
145
+ "tags", "trigger_patterns", "source_sessions", "linked_learnings",
146
+ }
147
+ updates = {}
148
+ for k, v in kwargs.items():
149
+ if k in allowed:
150
+ if isinstance(v, (list, dict)):
151
+ updates[k] = json.dumps(v)
152
+ else:
153
+ updates[k] = v
154
+
155
+ if not updates:
156
+ return dict(row)
157
+
158
+ updates["updated_at"] = datetime.datetime.now().isoformat(timespec='seconds')
159
+ set_clause = ", ".join(f"{k} = ?" for k in updates)
160
+ values = list(updates.values()) + [skill_id]
161
+ conn.execute(f"UPDATE skills SET {set_clause} WHERE id = ?", values)
162
+ conn.commit()
163
+
164
+ # Update FTS
165
+ row = conn.execute("SELECT * FROM skills WHERE id = ?", (skill_id,)).fetchone()
166
+ r = dict(row)
167
+ body = f"{r.get('description', '')} {r.get('tags', '[]')} {r.get('trigger_patterns', '[]')}"
168
+ fts_upsert("skill", skill_id, r.get("name", ""), body, "skill", commit=False)
169
+ return r
170
+
171
+
172
+ def delete_skill(skill_id: str) -> bool:
173
+ """Delete a skill and its usage history."""
174
+ conn = get_db()
175
+ conn.execute("DELETE FROM skill_usage WHERE skill_id = ?", (skill_id,))
176
+ result = conn.execute("DELETE FROM skills WHERE id = ?", (skill_id,))
177
+ conn.execute("DELETE FROM unified_search WHERE source = 'skill' AND source_id = ?", (skill_id,))
178
+ conn.commit()
179
+ return result.rowcount > 0
180
+
181
+
182
+ # ── Usage tracking & auto-promotion ────────────────────────────────
183
+
184
+ def record_usage(skill_id: str, session_id: str = '', success: bool = True,
185
+ context: str = '', notes: str = '') -> dict:
186
+ """Record a skill usage and auto-promote/degrade based on trust rules.
187
+
188
+ Returns the updated skill dict with promotion info.
189
+ """
190
+ conn = get_db()
191
+ row = conn.execute("SELECT * FROM skills WHERE id = ?", (skill_id,)).fetchone()
192
+ if not row:
193
+ return {"error": f"Skill {skill_id} not found"}
194
+
195
+ skill = dict(row)
196
+
197
+ # Record usage
198
+ conn.execute(
199
+ "INSERT INTO skill_usage (skill_id, session_id, success, context, notes) VALUES (?, ?, ?, ?, ?)",
200
+ (skill_id, session_id, 1 if success else 0, context, notes),
201
+ )
202
+
203
+ # Update counters
204
+ delta = TRUST_ON_SUCCESS if success else TRUST_ON_FAILURE
205
+ new_trust = max(0, min(100, skill['trust_score'] + delta))
206
+ count_field = "success_count" if success else "fail_count"
207
+
208
+ conn.execute(
209
+ f"""UPDATE skills SET
210
+ use_count = use_count + 1,
211
+ {count_field} = {count_field} + 1,
212
+ trust_score = ?,
213
+ last_used_at = datetime('now'),
214
+ updated_at = datetime('now')
215
+ WHERE id = ?""",
216
+ (new_trust, skill_id),
217
+ )
218
+ conn.commit()
219
+
220
+ # Auto-promotion: draft → published if 2+ successful uses in distinct contexts
221
+ promotion = None
222
+ if skill['level'] == 'draft' and success:
223
+ distinct_contexts = conn.execute(
224
+ """SELECT COUNT(DISTINCT context) FROM skill_usage
225
+ WHERE skill_id = ? AND success = 1 AND context != ''""",
226
+ (skill_id,),
227
+ ).fetchone()[0]
228
+ if distinct_contexts >= PROMOTION_USES_REQUIRED:
229
+ conn.execute(
230
+ "UPDATE skills SET level = 'published', updated_at = datetime('now') WHERE id = ?",
231
+ (skill_id,),
232
+ )
233
+ conn.commit()
234
+ promotion = "draft → published"
235
+
236
+ # Auto-archive: trust < 20 → archived
237
+ if new_trust < TRUST_ARCHIVE_THRESHOLD and skill['level'] in ('draft', 'published'):
238
+ conn.execute(
239
+ "UPDATE skills SET level = 'archived', updated_at = datetime('now') WHERE id = ?",
240
+ (skill_id,),
241
+ )
242
+ conn.commit()
243
+ promotion = f"{skill['level']} → archived (trust={new_trust})"
244
+
245
+ result = dict(conn.execute("SELECT * FROM skills WHERE id = ?", (skill_id,)).fetchone())
246
+ if promotion:
247
+ result['_promotion'] = promotion
248
+ return result
249
+
250
+
251
+ def match_skills(task: str, level: str = '', top_n: int = 3) -> list[dict]:
252
+ """Find skills matching a task description.
253
+
254
+ Search strategy:
255
+ 1. FTS5 on skill name/description/tags
256
+ 2. Trigger pattern matching
257
+ 3. Keyword overlap
258
+
259
+ Returns top-N matches sorted by relevance × trust.
260
+ """
261
+ if not task or not task.strip():
262
+ return []
263
+
264
+ conn = get_db()
265
+ seen = set()
266
+ results = []
267
+
268
+ # Level filter
269
+ level_filter = "AND level = ?" if level else "AND level IN ('draft', 'published')"
270
+ level_params = (level,) if level else ()
271
+
272
+ # Strategy 1: FTS5 search
273
+ fts_results = fts_search(task, source_filter="skill", limit=10)
274
+ if fts_results:
275
+ ids = [r['source_id'] for r in fts_results]
276
+ placeholders = ','.join('?' * len(ids))
277
+ rows = conn.execute(
278
+ f"SELECT * FROM skills WHERE id IN ({placeholders}) {level_filter} ORDER BY trust_score DESC",
279
+ tuple(ids) + level_params,
280
+ ).fetchall()
281
+ for r in rows:
282
+ d = dict(r)
283
+ d['_match'] = 'fts'
284
+ if d['id'] not in seen:
285
+ seen.add(d['id'])
286
+ results.append(d)
287
+
288
+ # Strategy 2: Trigger pattern matching
289
+ task_lower = task.lower()
290
+ rows = conn.execute(
291
+ f"SELECT * FROM skills WHERE trigger_patterns != '[]' {level_filter}",
292
+ level_params,
293
+ ).fetchall()
294
+ for r in rows:
295
+ if r['id'] in seen:
296
+ continue
297
+ try:
298
+ patterns = json.loads(r['trigger_patterns'])
299
+ for pattern in patterns:
300
+ if pattern.lower() in task_lower or task_lower in pattern.lower():
301
+ d = dict(r)
302
+ d['_match'] = f'trigger:{pattern}'
303
+ seen.add(d['id'])
304
+ results.append(d)
305
+ break
306
+ except (json.JSONDecodeError, TypeError):
307
+ pass
308
+
309
+ # Strategy 3: Tag keyword overlap
310
+ task_words = set(task_lower.split())
311
+ rows = conn.execute(
312
+ f"SELECT * FROM skills WHERE tags != '[]' {level_filter}",
313
+ level_params,
314
+ ).fetchall()
315
+ for r in rows:
316
+ if r['id'] in seen:
317
+ continue
318
+ try:
319
+ tags = json.loads(r['tags'])
320
+ tag_words = set(t.lower() for t in tags)
321
+ overlap = task_words & tag_words
322
+ if overlap:
323
+ d = dict(r)
324
+ d['_match'] = f'tags:{",".join(overlap)}'
325
+ seen.add(d['id'])
326
+ results.append(d)
327
+ except (json.JSONDecodeError, TypeError):
328
+ pass
329
+
330
+ # Sort by trust_score descending, then return top N
331
+ results.sort(key=lambda x: x.get('trust_score', 0), reverse=True)
332
+ return results[:top_n]
333
+
334
+
335
+ def merge_skills(id1: str, id2: str, keep_id: str = '') -> dict:
336
+ """Merge two similar skills into one. The survivor gets combined metadata.
337
+
338
+ Args:
339
+ id1: First skill ID
340
+ id2: Second skill ID
341
+ keep_id: Which one to keep (default: higher trust). The other is deleted.
342
+ """
343
+ conn = get_db()
344
+ s1 = conn.execute("SELECT * FROM skills WHERE id = ?", (id1,)).fetchone()
345
+ s2 = conn.execute("SELECT * FROM skills WHERE id = ?", (id2,)).fetchone()
346
+ if not s1:
347
+ return {"error": f"Skill {id1} not found"}
348
+ if not s2:
349
+ return {"error": f"Skill {id2} not found"}
350
+
351
+ s1, s2 = dict(s1), dict(s2)
352
+
353
+ # Decide which to keep
354
+ if not keep_id:
355
+ keep_id = id1 if s1['trust_score'] >= s2['trust_score'] else id2
356
+ survivor = s1 if keep_id == id1 else s2
357
+ donor = s2 if keep_id == id1 else s1
358
+ donor_id = donor['id']
359
+
360
+ # Merge tags
361
+ try:
362
+ tags1 = set(json.loads(survivor.get('tags', '[]')))
363
+ tags2 = set(json.loads(donor.get('tags', '[]')))
364
+ merged_tags = json.dumps(sorted(tags1 | tags2))
365
+ except (json.JSONDecodeError, TypeError):
366
+ merged_tags = survivor.get('tags', '[]')
367
+
368
+ # Merge trigger patterns
369
+ try:
370
+ tp1 = set(json.loads(survivor.get('trigger_patterns', '[]')))
371
+ tp2 = set(json.loads(donor.get('trigger_patterns', '[]')))
372
+ merged_tp = json.dumps(sorted(tp1 | tp2))
373
+ except (json.JSONDecodeError, TypeError):
374
+ merged_tp = survivor.get('trigger_patterns', '[]')
375
+
376
+ # Merge source sessions
377
+ try:
378
+ ss1 = set(json.loads(survivor.get('source_sessions', '[]')))
379
+ ss2 = set(json.loads(donor.get('source_sessions', '[]')))
380
+ merged_ss = json.dumps(sorted(ss1 | ss2, key=str))
381
+ except (json.JSONDecodeError, TypeError):
382
+ merged_ss = survivor.get('source_sessions', '[]')
383
+
384
+ # Merge linked learnings
385
+ try:
386
+ ll1 = set(json.loads(survivor.get('linked_learnings', '[]')))
387
+ ll2 = set(json.loads(donor.get('linked_learnings', '[]')))
388
+ merged_ll = json.dumps(sorted(ll1 | ll2, key=str))
389
+ except (json.JSONDecodeError, TypeError):
390
+ merged_ll = survivor.get('linked_learnings', '[]')
391
+
392
+ # Merge counters
393
+ merged_use = survivor['use_count'] + donor['use_count']
394
+ merged_success = survivor['success_count'] + donor['success_count']
395
+ merged_fail = survivor['fail_count'] + donor['fail_count']
396
+ merged_trust = max(survivor['trust_score'], donor['trust_score'])
397
+
398
+ # Update survivor
399
+ conn.execute(
400
+ """UPDATE skills SET
401
+ tags = ?, trigger_patterns = ?, source_sessions = ?, linked_learnings = ?,
402
+ use_count = ?, success_count = ?, fail_count = ?, trust_score = ?,
403
+ updated_at = datetime('now')
404
+ WHERE id = ?""",
405
+ (merged_tags, merged_tp, merged_ss, merged_ll,
406
+ merged_use, merged_success, merged_fail, merged_trust, keep_id),
407
+ )
408
+
409
+ # Move usage records from donor to survivor
410
+ conn.execute("UPDATE skill_usage SET skill_id = ? WHERE skill_id = ?", (keep_id, donor_id))
411
+
412
+ # Delete donor
413
+ conn.execute("DELETE FROM skills WHERE id = ?", (donor_id,))
414
+ conn.execute("DELETE FROM unified_search WHERE source = 'skill' AND source_id = ?", (donor_id,))
415
+ conn.commit()
416
+
417
+ result = dict(conn.execute("SELECT * FROM skills WHERE id = ?", (keep_id,)).fetchone())
418
+ result['_merged_from'] = donor_id
419
+ return result
420
+
421
+
422
+ def get_skill_stats() -> dict:
423
+ """Get aggregate skill statistics."""
424
+ conn = get_db()
425
+ total = conn.execute("SELECT COUNT(*) FROM skills").fetchone()[0]
426
+ by_level = {}
427
+ for row in conn.execute("SELECT level, COUNT(*) as cnt FROM skills GROUP BY level").fetchall():
428
+ by_level[row['level']] = row['cnt']
429
+
430
+ avg_trust = conn.execute(
431
+ "SELECT AVG(trust_score) FROM skills WHERE level != 'archived'"
432
+ ).fetchone()[0] or 0
433
+
434
+ total_uses = conn.execute("SELECT COUNT(*) FROM skill_usage").fetchone()[0]
435
+ success_rate = 0
436
+ if total_uses > 0:
437
+ successes = conn.execute("SELECT COUNT(*) FROM skill_usage WHERE success = 1").fetchone()[0]
438
+ success_rate = round(successes / total_uses * 100, 1)
439
+
440
+ recent_uses = conn.execute(
441
+ "SELECT COUNT(*) FROM skill_usage WHERE created_at >= datetime('now', '-7 days')"
442
+ ).fetchone()[0]
443
+
444
+ return {
445
+ "total": total,
446
+ "by_level": by_level,
447
+ "avg_trust": round(avg_trust, 1),
448
+ "total_uses": total_uses,
449
+ "success_rate": success_rate,
450
+ "uses_last_7d": recent_uses,
451
+ }
452
+
453
+
454
+ def decay_unused_skills(dry_run: bool = False) -> dict:
455
+ """Decay and purge unused skills. Called by immune.py or maintenance cron.
456
+
457
+ Rules:
458
+ - draft: no use in 30 days → trust = 0 → archived
459
+ - published: no use in 90 days → trust -= 5
460
+ - archived: no use in 60 days → purge (delete)
461
+ """
462
+ conn = get_db()
463
+ actions = {"decayed": [], "archived": [], "purged": []}
464
+
465
+ # Draft: 30 days no use → archive
466
+ rows = conn.execute("""
467
+ SELECT * FROM skills WHERE level = 'draft'
468
+ AND (last_used_at IS NULL OR last_used_at < datetime('now', '-30 days'))
469
+ AND created_at < datetime('now', '-30 days')
470
+ """).fetchall()
471
+ for r in rows:
472
+ if not dry_run:
473
+ conn.execute(
474
+ "UPDATE skills SET level = 'archived', trust_score = 0, updated_at = datetime('now') WHERE id = ?",
475
+ (r['id'],),
476
+ )
477
+ actions["archived"].append(r['id'])
478
+
479
+ # Published: 90 days no use → trust -= 5
480
+ rows = conn.execute("""
481
+ SELECT * FROM skills WHERE level = 'published'
482
+ AND (last_used_at IS NULL OR last_used_at < datetime('now', '-90 days'))
483
+ """).fetchall()
484
+ for r in rows:
485
+ new_trust = max(0, r['trust_score'] - 5)
486
+ if not dry_run:
487
+ conn.execute(
488
+ "UPDATE skills SET trust_score = ?, updated_at = datetime('now') WHERE id = ?",
489
+ (new_trust, r['id']),
490
+ )
491
+ if new_trust < TRUST_ARCHIVE_THRESHOLD:
492
+ conn.execute(
493
+ "UPDATE skills SET level = 'archived', updated_at = datetime('now') WHERE id = ?",
494
+ (r['id'],),
495
+ )
496
+ actions["archived"].append(r['id'])
497
+ actions["decayed"].append({"id": r['id'], "trust": f"{r['trust_score']} → {new_trust}"})
498
+
499
+ # Archived: 60 days → purge
500
+ rows = conn.execute("""
501
+ SELECT * FROM skills WHERE level = 'archived'
502
+ AND (last_used_at IS NULL OR last_used_at < datetime('now', '-60 days'))
503
+ AND updated_at < datetime('now', '-60 days')
504
+ """).fetchall()
505
+ for r in rows:
506
+ if not dry_run:
507
+ conn.execute("DELETE FROM skill_usage WHERE skill_id = ?", (r['id'],))
508
+ conn.execute("DELETE FROM skills WHERE id = ?", (r['id'],))
509
+ conn.execute("DELETE FROM unified_search WHERE source = 'skill' AND source_id = ?", (r['id'],))
510
+ actions["purged"].append(r['id'])
511
+
512
+ if not dry_run:
513
+ conn.commit()
514
+ return actions
@@ -0,0 +1,91 @@
1
+ """NEXO DB — Tasks module."""
2
+ from db._core import get_db, now_epoch
3
+
4
+ # ── Task History & Frequencies ─────────────────────────────────────
5
+
6
+ def log_task(task_num: str, task_name: str, notes: str = '', reasoning: str = '') -> dict:
7
+ """Log a task execution with optional reasoning."""
8
+ conn = get_db()
9
+ now = now_epoch()
10
+ cursor = conn.execute(
11
+ "INSERT INTO task_history (task_num, task_name, executed_at, notes, reasoning) "
12
+ "VALUES (?, ?, ?, ?, ?)",
13
+ (task_num, task_name, now, notes, reasoning)
14
+ )
15
+ conn.commit()
16
+ row = conn.execute(
17
+ "SELECT * FROM task_history WHERE id = ?", (cursor.lastrowid,)
18
+ ).fetchone()
19
+ return dict(row)
20
+
21
+
22
+ def list_task_history(task_num: str = None, days: int = 30) -> list[dict]:
23
+ """List task execution history, optionally filtered by task_num."""
24
+ conn = get_db()
25
+ cutoff = now_epoch() - (days * 86400)
26
+ if task_num:
27
+ rows = conn.execute(
28
+ "SELECT * FROM task_history WHERE task_num = ? AND executed_at >= ? "
29
+ "ORDER BY executed_at DESC",
30
+ (task_num, cutoff)
31
+ ).fetchall()
32
+ else:
33
+ rows = conn.execute(
34
+ "SELECT * FROM task_history WHERE executed_at >= ? "
35
+ "ORDER BY executed_at DESC",
36
+ (cutoff,)
37
+ ).fetchall()
38
+ return [dict(r) for r in rows]
39
+
40
+
41
+ def set_task_frequency(task_num: str, task_name: str,
42
+ frequency_days: int, description: str = '') -> dict:
43
+ """Set or update the expected frequency for a task."""
44
+ conn = get_db()
45
+ conn.execute(
46
+ "INSERT OR REPLACE INTO task_frequencies (task_num, task_name, frequency_days, description) "
47
+ "VALUES (?, ?, ?, ?)",
48
+ (task_num, task_name, frequency_days, description)
49
+ )
50
+ conn.commit()
51
+ row = conn.execute(
52
+ "SELECT * FROM task_frequencies WHERE task_num = ?", (task_num,)
53
+ ).fetchone()
54
+ return dict(row)
55
+
56
+
57
+ def get_overdue_tasks() -> list[dict]:
58
+ """Get tasks where last execution exceeds the configured frequency."""
59
+ conn = get_db()
60
+ freqs = conn.execute("SELECT * FROM task_frequencies").fetchall()
61
+ now = now_epoch()
62
+ overdue = []
63
+ for f in freqs:
64
+ last = conn.execute(
65
+ "SELECT MAX(executed_at) as last_exec FROM task_history WHERE task_num = ?",
66
+ (f["task_num"],)
67
+ ).fetchone()
68
+ last_exec = last["last_exec"] if last and last["last_exec"] else None
69
+ threshold = f["frequency_days"] * 86400
70
+ if last_exec is None or (now - last_exec) > threshold:
71
+ days_ago = round((now - last_exec) / 86400, 1) if last_exec else None
72
+ overdue.append({
73
+ "task_num": f["task_num"],
74
+ "task_name": f["task_name"],
75
+ "frequency_days": f["frequency_days"],
76
+ "last_executed": last_exec,
77
+ "days_since_last": days_ago,
78
+ "description": f["description"]
79
+ })
80
+ return overdue
81
+
82
+
83
+ def get_task_frequencies() -> list[dict]:
84
+ """Get all configured task frequencies."""
85
+ conn = get_db()
86
+ rows = conn.execute(
87
+ "SELECT * FROM task_frequencies ORDER BY task_num ASC"
88
+ ).fetchall()
89
+ return [dict(r) for r in rows]
90
+
91
+