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.
- package/README.md +7 -7
- package/bin/nexo-brain.js +53 -26
- package/package.json +1 -1
- package/scripts/migrate-to-unified 2.sh +813 -0
- package/scripts/migrate-v1.5-to-v1.6 2.py +778 -0
- package/scripts/migrate-v1.7-to-v1.8 2.py +214 -0
- package/scripts/migrate-v1.7-to-v1.8.py +2 -2
- package/scripts/nexo-preflight.sh +236 -0
- package/scripts/pre-commit-check 2.sh +55 -0
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +159 -0
- package/src/auto_update 2.py +634 -0
- package/src/auto_update.py +25 -0
- package/src/claim_graph 2.py +323 -0
- package/src/cognitive/__init__ 2.py +62 -0
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/cognitive/_core 2.py +567 -0
- package/src/cognitive/_decay 2.py +382 -0
- package/src/cognitive/_ingest 2.py +892 -0
- package/src/cognitive/_memory 2.py +912 -0
- package/src/cognitive/_search 2.py +949 -0
- package/src/cognitive/_trust 2.py +464 -0
- package/src/cognitive/_trust.py +10 -36
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest 2.json +106 -0
- package/src/crons/manifest.json +6 -13
- package/src/crons/sync 2.py +217 -0
- package/src/crons/sync.py +151 -6
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +789 -0
- package/src/db/__init__ 2.py +89 -0
- package/src/db/__init__.py +13 -0
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core 2.py +417 -0
- package/src/db/_credentials 2.py +124 -0
- package/src/db/_cron_runs.py +74 -0
- package/src/db/_entities 2.py +178 -0
- package/src/db/_episodic 2.py +738 -0
- package/src/db/_episodic.py +40 -6
- package/src/db/_evolution 2.py +54 -0
- package/src/db/_fts 2.py +406 -0
- package/src/db/_learnings 2.py +168 -0
- package/src/db/_reminders 2.py +338 -0
- package/src/db/_schema 2.py +364 -0
- package/src/db/_schema.py +64 -0
- package/src/db/_sessions 2.py +300 -0
- package/src/db/_skills.py +514 -0
- package/src/db/_tasks 2.py +91 -0
- package/src/evolution_cycle 2.py +266 -0
- package/src/hnsw_index 2.py +254 -0
- package/src/hooks/auto_capture 2.py +208 -0
- package/src/hooks/caffeinate-guard 2.sh +8 -0
- package/src/hooks/capture-session 2.sh +21 -0
- package/src/hooks/capture-session.sh +2 -0
- package/src/hooks/capture-tool-logs 2.sh +127 -0
- package/src/hooks/capture-tool-logs.sh +3 -2
- package/src/hooks/daily-briefing-check 2.sh +33 -0
- package/src/hooks/inbox-hook 2.sh +76 -0
- package/src/hooks/inbox-hook.sh +3 -2
- package/src/hooks/post-compact 2.sh +148 -0
- package/src/hooks/post-compact.sh +1 -1
- package/src/hooks/pre-compact 2.sh +151 -0
- package/src/hooks/pre-compact.sh +1 -1
- package/src/hooks/session-start 2.sh +268 -0
- package/src/hooks/session-start.sh +6 -3
- package/src/hooks/session-stop 2.sh +140 -0
- package/src/hooks/session-stop.sh +14 -102
- package/src/kg_populate 2.py +290 -0
- package/src/knowledge_graph 2.py +257 -0
- package/src/maintenance 2.py +59 -0
- package/src/migrate_embeddings 2.py +122 -0
- package/src/plugin_loader 2.py +202 -0
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +805 -0
- package/src/plugins/agents 2.py +52 -0
- package/src/plugins/artifact_registry 2.py +450 -0
- package/src/plugins/backup 2.py +104 -0
- package/src/plugins/cognitive_memory 2.py +564 -0
- package/src/plugins/core_rules 2.py +252 -0
- package/src/plugins/cortex 2.py +299 -0
- package/src/plugins/entities 2.py +67 -0
- package/src/plugins/episodic_memory 2.py +533 -0
- package/src/plugins/episodic_memory.py +5 -3
- package/src/plugins/evolution 2.py +115 -0
- package/src/plugins/guard 2.py +746 -0
- package/src/plugins/knowledge_graph_tools 2.py +105 -0
- package/src/plugins/preferences 2.py +47 -0
- package/src/plugins/schedule.py +212 -0
- package/src/plugins/skills.py +264 -0
- package/src/plugins/update 2.py +256 -0
- package/src/requirements 2.txt +12 -0
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +331 -0
- package/src/rules/migrate 2.py +207 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/check-context 2.py +264 -0
- package/src/scripts/deep-sleep/apply_findings.py +168 -8
- package/src/scripts/deep-sleep/collect.py +33 -11
- package/src/scripts/deep-sleep/extract-prompt.md +38 -0
- package/src/scripts/deep-sleep/extract.py +80 -8
- package/src/scripts/deep-sleep/synthesize-prompt.md +59 -2
- package/src/scripts/deep-sleep/synthesize.py +3 -1
- package/src/scripts/nexo-auto-update 2.py +6 -0
- package/src/scripts/nexo-backup 2.sh +25 -0
- package/src/scripts/nexo-brain-activation 2.sh +140 -0
- package/src/scripts/nexo-catchup 2.py +242 -0
- package/src/scripts/nexo-catchup.py +65 -29
- package/src/scripts/nexo-cognitive-decay 2.py +182 -0
- package/src/scripts/nexo-cron-wrapper.sh +53 -0
- package/src/scripts/nexo-daily-self-audit 2.py +552 -0
- package/src/scripts/nexo-daily-self-audit.py +4 -2
- package/src/scripts/nexo-deep-sleep 2.sh +97 -0
- package/src/scripts/nexo-deep-sleep.sh +66 -77
- package/src/scripts/nexo-evolution-run 2.py +597 -0
- package/src/scripts/nexo-evolution-run.py +13 -0
- package/src/scripts/nexo-followup-hygiene 2.py +112 -0
- package/src/scripts/nexo-immune 2.py +927 -0
- package/src/scripts/nexo-inbox-hook 2.sh +74 -0
- package/src/scripts/nexo-install 2.py +6 -0
- package/src/scripts/nexo-learning-housekeep 2.py +245 -0
- package/src/scripts/nexo-learning-housekeep.py +156 -1
- package/src/scripts/nexo-learning-validator 2.py +207 -0
- package/src/scripts/nexo-learning-validator.py +19 -0
- package/src/scripts/nexo-migrate 2.py +232 -0
- package/src/scripts/nexo-postmortem-consolidator 2.py +421 -0
- package/src/scripts/nexo-postmortem-consolidator.py +3 -2
- package/src/scripts/nexo-pre-commit 2.py +120 -0
- package/src/scripts/nexo-prevent-sleep 2.sh +29 -0
- package/src/scripts/nexo-proactive-dashboard 2.py +345 -0
- package/src/scripts/nexo-reflection 2.py +253 -0
- package/src/scripts/nexo-runtime-preflight 2.py +274 -0
- package/src/scripts/nexo-send-email 2.py +25 -0
- package/src/scripts/nexo-send-reply 2.py +178 -0
- package/src/scripts/nexo-sleep 2.py +592 -0
- package/src/scripts/nexo-sleep.py +16 -11
- package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
- package/src/scripts/nexo-synthesis 2.py +253 -0
- package/src/scripts/nexo-synthesis.py +46 -3
- package/src/scripts/nexo-tcc-approve 2.sh +79 -0
- package/src/scripts/nexo-update 2.sh +161 -0
- package/src/scripts/nexo-watchdog 2.sh +878 -0
- package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
- package/src/scripts/nexo-watchdog.sh +72 -19
- package/src/server 2.py +733 -0
- package/src/server.py +11 -2
- package/src/storage_router 2.py +32 -0
- package/src/tools_coordination 2.py +102 -0
- package/src/tools_credentials 2.py +68 -0
- package/src/tools_learnings 2.py +220 -0
- package/src/tools_menu 2.py +227 -0
- package/src/tools_reminders 2.py +86 -0
- package/src/tools_reminders_crud 2.py +159 -0
- package/src/tools_reminders_crud.py +7 -0
- package/src/tools_sessions 2.py +476 -0
- package/src/tools_task_history 2.py +57 -0
- package/templates/CLAUDE.md 2.template +63 -0
- package/templates/openclaw 2.json +13 -0
- package/tests/__init__ 2.py +0 -0
- package/tests/conftest 2.py +71 -0
- package/tests/test_cognitive 2.py +205 -0
- package/tests/test_knowledge_graph 2.py +140 -0
- package/tests/test_migrations 2.py +137 -0
- package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
- /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
|
+
|