nexo-brain 2.2.0 → 2.3.1

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 (256) hide show
  1. package/README.md +5 -5
  2. package/package.json +6 -3
  3. package/src/auto_update.py +26 -0
  4. package/src/crons/manifest.json +6 -13
  5. package/src/crons/sync.py +150 -6
  6. package/src/db/__init__.py +13 -0
  7. package/src/db/_core.py +1 -0
  8. package/src/db/_cron_runs.py +74 -0
  9. package/src/db/_entities.py +1 -0
  10. package/src/db/_episodic.py +41 -6
  11. package/src/db/_learnings.py +1 -0
  12. package/src/db/_reminders.py +1 -0
  13. package/src/db/_schema.py +64 -0
  14. package/src/db/_sessions.py +1 -0
  15. package/src/db/_skills.py +515 -0
  16. package/src/hooks/session-stop.sh +13 -101
  17. package/src/plugin_loader.py +1 -0
  18. package/src/plugins/episodic_memory.py +5 -3
  19. package/src/plugins/schedule.py +212 -0
  20. package/src/plugins/skills.py +264 -0
  21. package/src/plugins/update.py +1 -0
  22. package/src/scripts/deep-sleep/apply_findings.py +111 -8
  23. package/src/scripts/deep-sleep/collect.py +34 -11
  24. package/src/scripts/deep-sleep/extract-prompt.md +38 -0
  25. package/src/scripts/deep-sleep/extract.py +81 -8
  26. package/src/scripts/deep-sleep/synthesize-prompt.md +29 -1
  27. package/src/scripts/deep-sleep/synthesize.py +4 -1
  28. package/src/scripts/nexo-catchup.py +65 -29
  29. package/src/scripts/nexo-cron-wrapper.sh +53 -0
  30. package/src/scripts/nexo-daily-self-audit.py +4 -2
  31. package/src/scripts/nexo-deep-sleep.sh +66 -77
  32. package/src/scripts/nexo-evolution-run.py +13 -0
  33. package/src/scripts/nexo-learning-housekeep.py +157 -1
  34. package/src/scripts/nexo-learning-validator.py +19 -0
  35. package/src/scripts/nexo-postmortem-consolidator.py +3 -2
  36. package/src/scripts/nexo-sleep.py +16 -11
  37. package/src/scripts/nexo-synthesis.py +46 -3
  38. package/src/scripts/nexo-watchdog.sh +91 -30
  39. package/src/server.py +6 -1
  40. package/src/tools_coordination.py +1 -0
  41. package/src/tools_sessions.py +1 -0
  42. package/scripts/migrate-to-unified 2.sh +0 -813
  43. package/scripts/migrate-to-unified.sh +0 -813
  44. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  45. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  46. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  47. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  48. package/scripts/pre-commit-check 2.sh +0 -55
  49. package/scripts/pre-commit-check.sh +0 -55
  50. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  51. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  52. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  53. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  54. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  55. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  56. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  57. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  58. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  59. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  60. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  61. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  62. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  63. package/src/auto_close_sessions 2.py +0 -159
  64. package/src/auto_update 2.py +0 -634
  65. package/src/claim_graph 2.py +0 -323
  66. package/src/cognitive/__init__ 2.py +0 -62
  67. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  69. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  70. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  72. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  73. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  74. package/src/cognitive/_core 2.py +0 -567
  75. package/src/cognitive/_decay 2.py +0 -382
  76. package/src/cognitive/_ingest 2.py +0 -892
  77. package/src/cognitive/_memory 2.py +0 -912
  78. package/src/cognitive/_search 2.py +0 -949
  79. package/src/cognitive/_trust 2.py +0 -464
  80. package/src/crons/manifest 2.json +0 -106
  81. package/src/crons/sync 2.py +0 -217
  82. package/src/dashboard/__init__ 2.py +0 -0
  83. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  84. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  85. package/src/dashboard/app 2.py +0 -789
  86. package/src/db/__init__ 2.py +0 -89
  87. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  90. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  91. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  92. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  93. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  94. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  95. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  96. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  98. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  99. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  110. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  111. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  112. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  113. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  114. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  115. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  116. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  117. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  118. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  119. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  120. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  121. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  122. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  123. package/src/db/_core 2.py +0 -417
  124. package/src/db/_credentials 2.py +0 -124
  125. package/src/db/_entities 2.py +0 -178
  126. package/src/db/_episodic 2.py +0 -738
  127. package/src/db/_evolution 2.py +0 -54
  128. package/src/db/_fts 2.py +0 -406
  129. package/src/db/_learnings 2.py +0 -168
  130. package/src/db/_reminders 2.py +0 -338
  131. package/src/db/_schema 2.py +0 -364
  132. package/src/db/_sessions 2.py +0 -300
  133. package/src/db/_tasks 2.py +0 -91
  134. package/src/evolution_cycle 2.py +0 -266
  135. package/src/hnsw_index 2.py +0 -254
  136. package/src/hooks/auto_capture 2.py +0 -208
  137. package/src/hooks/caffeinate-guard 2.sh +0 -8
  138. package/src/hooks/capture-session 2.sh +0 -21
  139. package/src/hooks/capture-tool-logs 2.sh +0 -127
  140. package/src/hooks/daily-briefing-check 2.sh +0 -33
  141. package/src/hooks/inbox-hook 2.sh +0 -76
  142. package/src/hooks/post-compact 2.sh +0 -148
  143. package/src/hooks/pre-compact 2.sh +0 -151
  144. package/src/hooks/session-start 2.sh +0 -268
  145. package/src/hooks/session-stop 2.sh +0 -140
  146. package/src/kg_populate 2.py +0 -290
  147. package/src/knowledge_graph 2.py +0 -257
  148. package/src/maintenance 2.py +0 -59
  149. package/src/migrate_embeddings 2.py +0 -122
  150. package/src/plugin_loader 2.py +0 -202
  151. package/src/plugins/__init__ 2.py +0 -0
  152. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-310.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__/agents 2.cpython-310.pyc +0 -0
  157. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  160. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  163. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  182. package/src/plugins/adaptive_mode 2.py +0 -805
  183. package/src/plugins/agents 2.py +0 -52
  184. package/src/plugins/artifact_registry 2.py +0 -450
  185. package/src/plugins/backup 2.py +0 -104
  186. package/src/plugins/cognitive_memory 2.py +0 -564
  187. package/src/plugins/core_rules 2.py +0 -252
  188. package/src/plugins/cortex 2.py +0 -299
  189. package/src/plugins/entities 2.py +0 -67
  190. package/src/plugins/episodic_memory 2.py +0 -533
  191. package/src/plugins/evolution 2.py +0 -115
  192. package/src/plugins/guard 2.py +0 -746
  193. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  194. package/src/plugins/preferences 2.py +0 -47
  195. package/src/plugins/update 2.py +0 -256
  196. package/src/requirements 2.txt +0 -12
  197. package/src/rules/__init__ 2.py +0 -0
  198. package/src/rules/core-rules 2.json +0 -331
  199. package/src/rules/migrate 2.py +0 -207
  200. package/src/scripts/check-context 2.py +0 -264
  201. package/src/scripts/nexo-auto-update 2.py +0 -6
  202. package/src/scripts/nexo-backup 2.sh +0 -25
  203. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  204. package/src/scripts/nexo-catchup 2.py +0 -242
  205. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  206. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  207. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  208. package/src/scripts/nexo-evolution-run 2.py +0 -597
  209. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  210. package/src/scripts/nexo-github-monitor 2.py +0 -256
  211. package/src/scripts/nexo-github-monitor.py +0 -256
  212. package/src/scripts/nexo-immune 2.py +0 -927
  213. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  214. package/src/scripts/nexo-install 2.py +0 -6
  215. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  216. package/src/scripts/nexo-learning-validator 2.py +0 -207
  217. package/src/scripts/nexo-migrate 2.py +0 -232
  218. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  219. package/src/scripts/nexo-pre-commit 2.py +0 -120
  220. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  221. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  222. package/src/scripts/nexo-reflection 2.py +0 -253
  223. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  224. package/src/scripts/nexo-send-email 2.py +0 -25
  225. package/src/scripts/nexo-send-email.py +0 -25
  226. package/src/scripts/nexo-send-reply 2.py +0 -178
  227. package/src/scripts/nexo-send-reply.py +0 -178
  228. package/src/scripts/nexo-sleep 2.py +0 -592
  229. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  230. package/src/scripts/nexo-synthesis 2.py +0 -253
  231. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  232. package/src/scripts/nexo-update 2.sh +0 -161
  233. package/src/scripts/nexo-watchdog 2.sh +0 -878
  234. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  235. package/src/server 2.py +0 -733
  236. package/src/storage_router 2.py +0 -32
  237. package/src/tools_coordination 2.py +0 -102
  238. package/src/tools_credentials 2.py +0 -68
  239. package/src/tools_learnings 2.py +0 -220
  240. package/src/tools_menu 2.py +0 -227
  241. package/src/tools_reminders 2.py +0 -86
  242. package/src/tools_reminders_crud 2.py +0 -159
  243. package/src/tools_sessions 2.py +0 -476
  244. package/src/tools_task_history 2.py +0 -57
  245. package/templates/CLAUDE.md 2.template +0 -63
  246. package/templates/openclaw 2.json +0 -13
  247. package/tests/__init__ 2.py +0 -0
  248. package/tests/__init__.py +0 -0
  249. package/tests/conftest 2.py +0 -71
  250. package/tests/conftest.py +0 -71
  251. package/tests/test_cognitive 2.py +0 -205
  252. package/tests/test_cognitive.py +0 -205
  253. package/tests/test_knowledge_graph 2.py +0 -140
  254. package/tests/test_knowledge_graph.py +0 -140
  255. package/tests/test_migrations 2.py +0 -137
  256. package/tests/test_migrations.py +0 -137
@@ -0,0 +1,212 @@
1
+ """NEXO Schedule — Cron execution history, status, and management tools."""
2
+
3
+ import json
4
+ import os
5
+ import platform
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ from db import cron_runs_recent, cron_runs_summary
10
+
11
+
12
+ def handle_schedule_status(hours: int = 24, cron_id: str = '') -> str:
13
+ """Show cron execution status — what ran, what failed, durations.
14
+
15
+ Args:
16
+ hours: How far back to look (default 24h).
17
+ cron_id: Filter to a specific cron (optional). E.g. 'deep-sleep', 'immune'.
18
+ """
19
+ if cron_id:
20
+ runs = cron_runs_recent(hours, cron_id)
21
+ if not runs:
22
+ return f"No runs for '{cron_id}' in the last {hours}h."
23
+ lines = [f"CRON RUNS — {cron_id} (last {hours}h): {len(runs)} executions"]
24
+ for r in runs:
25
+ status = "✅" if r.get("exit_code") == 0 else "❌"
26
+ dur = f"{r['duration_secs']:.0f}s" if r.get("duration_secs") else "running"
27
+ summary = f" — {r['summary'][:100]}" if r.get("summary") else ""
28
+ error = f" ERROR: {r['error'][:100]}" if r.get("error") else ""
29
+ lines.append(f" {status} {r['started_at']} ({dur}){summary}{error}")
30
+ return "\n".join(lines)
31
+
32
+ # Summary view — one line per cron
33
+ summary = cron_runs_summary(hours)
34
+ if not summary:
35
+ return f"No cron executions recorded in the last {hours}h."
36
+
37
+ lines = [f"CRON STATUS (last {hours}h):"]
38
+ for s in summary:
39
+ status = "✅" if s.get("last_exit_code") == 0 else "❌"
40
+ rate = f"{s['succeeded']}/{s['total_runs']}"
41
+ dur = f"{s['avg_duration']:.0f}s avg" if s.get("avg_duration") else ""
42
+ summary_txt = f" — {s['last_summary'][:80]}" if s.get("last_summary") else ""
43
+ lines.append(f" {status} {s['cron_id']}: {rate} OK, {dur}{summary_txt}")
44
+
45
+ return "\n".join(lines)
46
+
47
+
48
+ def handle_schedule_add(cron_id: str, script: str, schedule: str = '',
49
+ interval_seconds: int = 0, description: str = '',
50
+ script_type: str = 'python') -> str:
51
+ """Add a new personal cron job. Generates and installs the LaunchAgent (macOS) or systemd timer (Linux).
52
+
53
+ Args:
54
+ cron_id: Unique ID for this cron (e.g. 'my-backup', 'report-daily'). Must be lowercase with hyphens.
55
+ script: Path to the script to run (absolute or relative to NEXO_HOME/scripts/).
56
+ schedule: Time-based schedule as 'HH:MM' (daily) or 'HH:MM:weekday' (e.g. '08:00:1' for Monday 8AM). Mutually exclusive with interval_seconds.
57
+ interval_seconds: Run every N seconds (e.g. 300 for every 5 min). Mutually exclusive with schedule.
58
+ description: What this cron does (for logs and status).
59
+ script_type: 'python' (default) or 'shell'.
60
+ """
61
+ if not cron_id or not script:
62
+ return "ERROR: cron_id and script are required."
63
+ if not schedule and not interval_seconds:
64
+ return "ERROR: either schedule (e.g. '08:00') or interval_seconds (e.g. 300) is required."
65
+
66
+ nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
67
+ script_path = Path(script)
68
+ if not script_path.is_absolute():
69
+ script_path = nexo_home / "scripts" / script
70
+ if not script_path.exists():
71
+ return f"ERROR: script not found: {script_path}"
72
+
73
+ wrapper_path = nexo_home / "scripts" / "nexo-cron-wrapper.sh"
74
+ if not wrapper_path.exists():
75
+ return f"ERROR: wrapper not found at {wrapper_path}. Run crons/sync.py first."
76
+
77
+ system = platform.system()
78
+
79
+ if system == "Darwin":
80
+ return _add_launchagent(cron_id, str(script_path), str(wrapper_path),
81
+ schedule, interval_seconds, description, script_type, nexo_home)
82
+ elif system == "Linux":
83
+ return _add_systemd_timer(cron_id, str(script_path), str(wrapper_path),
84
+ schedule, interval_seconds, description, script_type, nexo_home)
85
+ else:
86
+ return f"ERROR: unsupported platform: {system}"
87
+
88
+
89
+ def _add_launchagent(cron_id, script_path, wrapper_path, schedule, interval_seconds,
90
+ description, script_type, nexo_home):
91
+ """Create and load a macOS LaunchAgent."""
92
+ import plistlib
93
+
94
+ label = f"com.nexo.{cron_id}"
95
+ plist_path = Path.home() / "Library" / "LaunchAgents" / f"{label}.plist"
96
+
97
+ if plist_path.exists():
98
+ return f"ERROR: cron '{cron_id}' already exists at {plist_path}. Use a different ID or remove it first."
99
+
100
+ python_bin = "/opt/homebrew/bin/python3"
101
+ for p in ["/opt/homebrew/bin/python3", "/usr/local/bin/python3", "/usr/bin/python3"]:
102
+ if Path(p).exists():
103
+ python_bin = p
104
+ break
105
+
106
+ if script_type == "shell":
107
+ program_args = ["/bin/bash", wrapper_path, cron_id, "/bin/bash", script_path]
108
+ else:
109
+ program_args = ["/bin/bash", wrapper_path, cron_id, python_bin, script_path]
110
+
111
+ plist = {
112
+ "Label": label,
113
+ "ProgramArguments": program_args,
114
+ "StandardOutPath": str(nexo_home / "logs" / f"{cron_id}-stdout.log"),
115
+ "StandardErrorPath": str(nexo_home / "logs" / f"{cron_id}-stderr.log"),
116
+ "EnvironmentVariables": {
117
+ "HOME": str(Path.home()),
118
+ "NEXO_HOME": str(nexo_home),
119
+ "PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:" + str(Path.home() / ".local/bin"),
120
+ },
121
+ }
122
+
123
+ if interval_seconds:
124
+ plist["StartInterval"] = interval_seconds
125
+ elif schedule:
126
+ parts = schedule.split(":")
127
+ cal = {"Hour": int(parts[0]), "Minute": int(parts[1])}
128
+ if len(parts) > 2:
129
+ cal["Weekday"] = int(parts[2])
130
+ plist["StartCalendarInterval"] = cal
131
+
132
+ with open(plist_path, "wb") as f:
133
+ plistlib.dump(plist, f)
134
+
135
+ subprocess.run(["launchctl", "bootstrap", f"gui/{os.getuid()}", str(plist_path)], capture_output=True)
136
+
137
+ return f"Cron '{cron_id}' installed at {plist_path} and loaded.{' Schedule: ' + schedule if schedule else f' Interval: {interval_seconds}s'}"
138
+
139
+
140
+ def _add_systemd_timer(cron_id, script_path, wrapper_path, schedule, interval_seconds,
141
+ description, script_type, nexo_home):
142
+ """Create and enable a systemd user timer (Linux)."""
143
+ unit_dir = Path.home() / ".config" / "systemd" / "user"
144
+ unit_dir.mkdir(parents=True, exist_ok=True)
145
+
146
+ python_bin = "/usr/bin/python3"
147
+ for p in ["/usr/bin/python3", "/usr/local/bin/python3"]:
148
+ if Path(p).exists():
149
+ python_bin = p
150
+ break
151
+
152
+ if script_type == "shell":
153
+ exec_cmd = f"/bin/bash {wrapper_path} {cron_id} /bin/bash {script_path}"
154
+ else:
155
+ exec_cmd = f"/bin/bash {wrapper_path} {cron_id} {python_bin} {script_path}"
156
+
157
+ # Service unit
158
+ service_content = f"""[Unit]
159
+ Description=NEXO: {description or cron_id}
160
+
161
+ [Service]
162
+ Type=oneshot
163
+ ExecStart={exec_cmd}
164
+ Environment=NEXO_HOME={nexo_home}
165
+ Environment=HOME={Path.home()}
166
+ """
167
+ service_path = unit_dir / f"nexo-{cron_id}.service"
168
+ service_path.write_text(service_content)
169
+
170
+ # Timer unit
171
+ if interval_seconds:
172
+ timer_spec = f"OnUnitActiveSec={interval_seconds}s\nOnBootSec=60s"
173
+ elif schedule:
174
+ parts = schedule.split(":")
175
+ hour, minute = int(parts[0]), int(parts[1])
176
+ if len(parts) > 2:
177
+ days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
178
+ day = days[int(parts[2])]
179
+ timer_spec = f"OnCalendar={day} *-*-* {hour:02d}:{minute:02d}:00"
180
+ else:
181
+ timer_spec = f"OnCalendar=*-*-* {hour:02d}:{minute:02d}:00"
182
+ else:
183
+ return "ERROR: no schedule or interval"
184
+
185
+ timer_content = f"""[Unit]
186
+ Description=NEXO timer: {description or cron_id}
187
+
188
+ [Timer]
189
+ {timer_spec}
190
+ Persistent=true
191
+
192
+ [Install]
193
+ WantedBy=timers.target
194
+ """
195
+ timer_path = unit_dir / f"nexo-{cron_id}.timer"
196
+ timer_path.write_text(timer_content)
197
+
198
+ subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
199
+ subprocess.run(["systemctl", "--user", "enable", "--now", f"nexo-{cron_id}.timer"], capture_output=True)
200
+
201
+ return f"Cron '{cron_id}' installed as systemd timer and enabled. Service: {service_path}, Timer: {timer_path}"
202
+
203
+
204
+ TOOLS = [
205
+ (handle_schedule_status, "nexo_schedule_status",
206
+ "Show cron execution status: what ran overnight, what failed, durations. "
207
+ "Use at startup to give the user a quick health overview of autonomous processes."),
208
+
209
+ (handle_schedule_add, "nexo_schedule_add",
210
+ "Add a new personal cron job. Creates LaunchAgent (macOS) or systemd timer (Linux) "
211
+ "automatically, wrapped with execution tracking."),
212
+ ]
@@ -0,0 +1,264 @@
1
+ """Skills plugin — reusable procedures extracted from complex tasks.
2
+
3
+ Skills are procedural knowledge (step-by-step how-tos) vs learnings which are
4
+ declarative (don't do X). Created automatically by Deep Sleep or manually.
5
+
6
+ Pipeline: trace → draft → published, fully autonomous.
7
+ Trust score with decay controls quality — no human approval gates.
8
+ """
9
+
10
+ import json
11
+ from db import (
12
+ create_skill, get_skill, list_skills, search_skills,
13
+ update_skill, delete_skill,
14
+ record_skill_usage, match_skills, merge_skills, get_skill_stats,
15
+ )
16
+
17
+
18
+ def handle_skill_create(
19
+ id: str,
20
+ name: str,
21
+ description: str = '',
22
+ level: str = 'draft',
23
+ tags: str = '[]',
24
+ trigger_patterns: str = '[]',
25
+ source_sessions: str = '[]',
26
+ linked_learnings: str = '[]',
27
+ file_path: str = '',
28
+ ) -> str:
29
+ """Create a new skill (reusable procedure).
30
+
31
+ Skills are procedural knowledge — step-by-step instructions for complex tasks.
32
+ Created by Deep Sleep (auto-extraction) or manually during sessions.
33
+
34
+ Pipeline levels: trace → draft → published → archived.
35
+ Promotion is automatic: 2+ successful uses in distinct contexts → published.
36
+
37
+ Args:
38
+ id: Unique ID starting with 'SK-' (e.g., SK-DEPLOY-CHROME-EXT).
39
+ name: Human-readable name (e.g., 'Deploy Chrome Extension').
40
+ description: What this skill does (1-2 sentences).
41
+ level: Starting level — trace, draft (default), published, archived.
42
+ tags: JSON array of tags for discovery (e.g., '["chrome", "extension", "deploy"]').
43
+ trigger_patterns: JSON array of phrases that should trigger this skill
44
+ (e.g., '["deploy extension", "publish chrome"]').
45
+ source_sessions: JSON array of diary IDs where this skill was observed.
46
+ linked_learnings: JSON array of learning IDs related to this skill.
47
+ file_path: Path to the .md file with full procedure (if stored as file).
48
+ """
49
+ if not id.startswith('SK-'):
50
+ return "ERROR: Skill ID must start with 'SK-' (e.g., SK-DEPLOY-CHROME-EXT)"
51
+
52
+ existing = get_skill(id)
53
+ if existing:
54
+ return f"ERROR: Skill {id} already exists. Use nexo_skill_update to modify."
55
+
56
+ result = create_skill(
57
+ skill_id=id, name=name, description=description, level=level,
58
+ tags=tags, trigger_patterns=trigger_patterns,
59
+ source_sessions=source_sessions, linked_learnings=linked_learnings,
60
+ file_path=file_path,
61
+ )
62
+ if "error" in result:
63
+ return f"ERROR: {result['error']}"
64
+
65
+ return (
66
+ f"Skill {id} created ({level}, trust={result.get('trust_score', 50)}).\n"
67
+ f" Name: {name}\n"
68
+ f" Tags: {tags}\n"
69
+ f" Triggers: {trigger_patterns}"
70
+ )
71
+
72
+
73
+ def handle_skill_match(task: str, level: str = '') -> str:
74
+ """Find skills matching a task description. Call BEFORE starting multi-step tasks.
75
+
76
+ Searches by: FTS5 relevance, trigger pattern matching, tag keyword overlap.
77
+ Returns top-3 matches sorted by trust score.
78
+
79
+ Args:
80
+ task: Description of what you're about to do (e.g., 'deploy chrome extension to CWS').
81
+ level: Filter by level (optional). Default: draft + published.
82
+ """
83
+ matches = match_skills(task, level=level)
84
+ if not matches:
85
+ return f"No skills found for: '{task}'"
86
+
87
+ lines = [f"SKILLS MATCHED ({len(matches)}) for '{task}':"]
88
+ for m in matches:
89
+ match_method = m.pop('_match', 'unknown')
90
+ fp = f" → {m['file_path']}" if m.get('file_path') else ""
91
+ lines.append(
92
+ f" [{m['id']}] {m['name']} ({m['level']}, trust={m['trust_score']}, "
93
+ f"used={m['use_count']}x) via {match_method}{fp}\n"
94
+ f" {m['description'][:120]}"
95
+ )
96
+ try:
97
+ triggers = json.loads(m.get('trigger_patterns', '[]'))
98
+ if triggers:
99
+ lines.append(f" Triggers: {', '.join(triggers[:5])}")
100
+ except (json.JSONDecodeError, TypeError):
101
+ pass
102
+ return "\n".join(lines)
103
+
104
+
105
+ def handle_skill_get(id: str) -> str:
106
+ """Get a skill's full details including usage history.
107
+
108
+ Args:
109
+ id: Skill ID (e.g., SK-DEPLOY-CHROME-EXT).
110
+ """
111
+ skill = get_skill(id)
112
+ if not skill:
113
+ return f"ERROR: Skill {id} not found."
114
+
115
+ from db import get_db
116
+ conn = get_db()
117
+ recent_uses = conn.execute(
118
+ "SELECT * FROM skill_usage WHERE skill_id = ? ORDER BY created_at DESC LIMIT 5",
119
+ (id,),
120
+ ).fetchall()
121
+
122
+ lines = [
123
+ f"SKILL: {skill['id']}",
124
+ f" Name: {skill['name']}",
125
+ f" Description: {skill['description']}",
126
+ f" Level: {skill['level']}",
127
+ f" Trust: {skill['trust_score']}",
128
+ f" File: {skill['file_path'] or '(none)'}",
129
+ f" Tags: {skill['tags']}",
130
+ f" Triggers: {skill['trigger_patterns']}",
131
+ f" Source sessions: {skill['source_sessions']}",
132
+ f" Linked learnings: {skill['linked_learnings']}",
133
+ f" Stats: {skill['use_count']} uses, {skill['success_count']} success, {skill['fail_count']} fail",
134
+ f" Created: {skill['created_at']}",
135
+ f" Last used: {skill['last_used_at'] or 'never'}",
136
+ ]
137
+
138
+ if recent_uses:
139
+ lines.append("\n RECENT USAGE:")
140
+ for u in recent_uses:
141
+ u = dict(u)
142
+ status = "✓" if u['success'] else "✗"
143
+ lines.append(f" {status} {u['created_at']} — {u['context'][:60] or '(no context)'}")
144
+ if u.get('notes'):
145
+ lines.append(f" Notes: {u['notes'][:80]}")
146
+
147
+ return "\n".join(lines)
148
+
149
+
150
+ def handle_skill_result(id: str, success: bool = True, context: str = '', notes: str = '') -> str:
151
+ """Record the result of using a skill. Auto-promotes/degrades based on trust rules.
152
+
153
+ Call this AFTER following a skill's procedure to record whether it worked.
154
+ - Success: trust +5. After 2+ successes in distinct contexts: draft → published.
155
+ - Failure: trust -10. If trust < 20: → archived.
156
+
157
+ Args:
158
+ id: Skill ID.
159
+ success: Whether the skill's procedure worked correctly.
160
+ context: What task you were doing (used for distinct-context promotion).
161
+ notes: Additional notes (especially useful for failures — what went wrong).
162
+ """
163
+ result = record_skill_usage(skill_id=id, success=success, context=context, notes=notes)
164
+ if "error" in result:
165
+ return f"ERROR: {result['error']}"
166
+
167
+ promotion = result.pop('_promotion', None)
168
+ status = "SUCCESS" if success else "FAILURE"
169
+ msg = f"Skill {id} usage recorded: {status} (trust={result['trust_score']})"
170
+ if promotion:
171
+ msg += f"\n ⚡ PROMOTION: {promotion}"
172
+ return msg
173
+
174
+
175
+ def handle_skill_list(level: str = '', tag: str = '') -> str:
176
+ """List all skills, optionally filtered by level or tag.
177
+
178
+ Args:
179
+ level: Filter by level — trace, draft, published, archived.
180
+ tag: Filter by tag (e.g., 'chrome', 'deploy', 'shopify').
181
+ """
182
+ skills = list_skills(level=level, tag=tag)
183
+ if not skills:
184
+ filters = []
185
+ if level: filters.append(f"level={level}")
186
+ if tag: filters.append(f"tag={tag}")
187
+ return f"No skills found{' (' + ', '.join(filters) + ')' if filters else ''}."
188
+
189
+ lines = [f"SKILLS ({len(skills)}):"]
190
+ for s in skills:
191
+ fp = f" → {s['file_path']}" if s.get('file_path') else ""
192
+ used = f", last={s['last_used_at'][:10]}" if s.get('last_used_at') else ""
193
+ lines.append(
194
+ f" [{s['id']}] {s['name']} ({s['level']}, trust={s['trust_score']}, "
195
+ f"used={s['use_count']}x{used}){fp}"
196
+ )
197
+ return "\n".join(lines)
198
+
199
+
200
+ def handle_skill_merge(id1: str, id2: str, keep_id: str = '') -> str:
201
+ """Merge two similar skills into one. Combines tags, triggers, usage history.
202
+
203
+ The survivor keeps the higher trust score and all combined metadata.
204
+ The donor is deleted.
205
+
206
+ Args:
207
+ id1: First skill ID.
208
+ id2: Second skill ID.
209
+ keep_id: Which one to keep (default: higher trust score).
210
+ """
211
+ result = merge_skills(id1, id2, keep_id=keep_id)
212
+ if "error" in result:
213
+ return f"ERROR: {result['error']}"
214
+
215
+ merged_from = result.pop('_merged_from', '?')
216
+ return (
217
+ f"Skills merged. Kept {result['id']}, deleted {merged_from}.\n"
218
+ f" Trust: {result['trust_score']}, Uses: {result['use_count']}, "
219
+ f"Tags: {result['tags']}"
220
+ )
221
+
222
+
223
+ def handle_skill_stats() -> str:
224
+ """Show aggregate skill statistics: total count, by level, avg trust, usage rates."""
225
+ stats = get_skill_stats()
226
+ levels = stats.get('by_level', {})
227
+ lines = [
228
+ "SKILL STATS:",
229
+ f" Total: {stats['total']}",
230
+ f" By level: {', '.join(f'{k}={v}' for k, v in sorted(levels.items()))}",
231
+ f" Avg trust: {stats['avg_trust']}",
232
+ f" Total uses: {stats['total_uses']} (success rate: {stats['success_rate']}%)",
233
+ f" Uses last 7d: {stats['uses_last_7d']}",
234
+ ]
235
+ return "\n".join(lines)
236
+
237
+
238
+ # Plugin registration — TOOLS array consumed by plugin_loader.py
239
+ TOOLS = [
240
+ (handle_skill_create, "nexo_skill_create",
241
+ "Create a new skill (reusable procedure). Skills are step-by-step instructions for complex tasks. "
242
+ "Auto-promoted from draft→published after 2+ successful uses. ID must start with 'SK-'."),
243
+
244
+ (handle_skill_match, "nexo_skill_match",
245
+ "Find skills matching a task description. Call BEFORE starting multi-step tasks "
246
+ "to check if a reusable procedure exists. Returns top-3 matches by trust score."),
247
+
248
+ (handle_skill_get, "nexo_skill_get",
249
+ "Get a skill's full details including procedure, tags, triggers, and usage history."),
250
+
251
+ (handle_skill_result, "nexo_skill_result",
252
+ "Record the result of using a skill (success/failure). Auto-promotes draft→published "
253
+ "after 2+ successes, auto-archives if trust drops below 20."),
254
+
255
+ (handle_skill_list, "nexo_skill_list",
256
+ "List all skills, optionally filtered by level (trace/draft/published/archived) or tag."),
257
+
258
+ (handle_skill_merge, "nexo_skill_merge",
259
+ "Merge two similar skills into one. Combines tags, triggers, and usage history. "
260
+ "Survivor keeps the higher trust score."),
261
+
262
+ (handle_skill_stats, "nexo_skill_stats",
263
+ "Show aggregate skill statistics: count by level, average trust, usage rates."),
264
+ ]
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """Update plugin — pull latest code, backup DBs, run migrations, verify."""
2
3
  import json
3
4
  import os
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ from __future__ import annotations
2
3
  """
3
4
  Deep Sleep v2 -- Phase 4: Apply synthesized findings.
4
5
 
@@ -135,21 +136,52 @@ def update_calibration_mood(synthesis: dict) -> dict:
135
136
  # Keep last 30 days
136
137
  cal["mood_history"] = cal["mood_history"][-30:]
137
138
 
138
- # Apply calibration recommendation if any
139
+ # Apply calibration recommendation automatically
139
140
  rec = emotional_day.get("calibration_recommendation")
140
141
  if rec and rec != "null":
141
- if "calibration_notes" not in cal:
142
- cal["calibration_notes"] = []
143
- cal["calibration_notes"].append({
142
+ applied_changes = []
143
+
144
+ # Parse and apply known calibration adjustments
145
+ rec_lower = rec.lower()
146
+ personality = cal.get("personality", {})
147
+
148
+ # Autonomy adjustments
149
+ if "autonomy" in rec_lower or "autonomía" in rec_lower:
150
+ if any(w in rec_lower for w in ["full", "más autonomía", "subir", "increase"]):
151
+ personality["autonomy"] = "full"
152
+ applied_changes.append("autonomy → full")
153
+ elif any(w in rec_lower for w in ["conservative", "reducir", "bajar"]):
154
+ personality["autonomy"] = "conservative"
155
+ applied_changes.append("autonomy → conservative")
156
+
157
+ # Communication adjustments
158
+ if any(w in rec_lower for w in ["concis", "breve", "shorter", "telegráf"]):
159
+ personality["communication"] = "concise"
160
+ applied_changes.append("communication → concise")
161
+ elif any(w in rec_lower for w in ["detail", "explicar más", "más contexto"]):
162
+ personality["communication"] = "detailed"
163
+ applied_changes.append("communication → detailed")
164
+
165
+ # Proactivity adjustments
166
+ if any(w in rec_lower for w in ["más proactiv", "proactive", "anticipar"]):
167
+ personality["proactivity"] = "proactive"
168
+ applied_changes.append("proactivity → proactive")
169
+
170
+ cal["personality"] = personality
171
+
172
+ # Log the recommendation and what was applied
173
+ if "calibration_log" not in cal:
174
+ cal["calibration_log"] = []
175
+ cal["calibration_log"].append({
144
176
  "date": synthesis.get("date", ""),
145
177
  "recommendation": rec,
146
- "applied": False,
178
+ "applied": applied_changes if applied_changes else ["noted, no auto-applicable changes"],
147
179
  })
148
- # Keep last 10
149
- cal["calibration_notes"] = cal["calibration_notes"][-10:]
180
+ cal["calibration_log"] = cal["calibration_log"][-20:]
150
181
 
151
182
  calibration_file.write_text(json.dumps(cal, indent=2, ensure_ascii=False))
152
- return {"success": True, "mood_score": emotional_day.get("mood_score")}
183
+ changes_str = ", ".join(applied_changes) if rec and applied_changes else "none"
184
+ return {"success": True, "mood_score": emotional_day.get("mood_score"), "calibration_applied": changes_str}
153
185
  except Exception as e:
154
186
  return {"success": False, "error": str(e)}
155
187
 
@@ -203,6 +235,52 @@ def calibrate_trust_score(synthesis: dict, target_date: str) -> dict:
203
235
  return {"success": False, "error": str(e)}
204
236
 
205
237
 
238
+ def create_skill(skill_data: dict) -> dict:
239
+ """Create a skill in nexo.db from Deep Sleep extraction."""
240
+ if not NEXO_DB.exists():
241
+ return {"success": False, "error": "nexo.db not found"}
242
+ try:
243
+ import hashlib
244
+ skill_id = skill_data.get("id", "")
245
+ if not skill_id:
246
+ skill_id = "SK-DS-" + hashlib.md5(
247
+ skill_data.get("name", "").encode()
248
+ ).hexdigest()[:8].upper()
249
+
250
+ name = skill_data.get("name", "")
251
+ description = skill_data.get("description", "")
252
+ tags = json.dumps(skill_data.get("tags", []))
253
+ trigger_patterns = json.dumps(skill_data.get("trigger_patterns", []))
254
+ source_sessions = json.dumps(skill_data.get("source_sessions", []))
255
+ steps = skill_data.get("steps", [])
256
+ gotchas = skill_data.get("gotchas", [])
257
+
258
+ # Build file content for the skill .md file
259
+ steps_md = "\n".join(f"{i+1}. {s}" for i, s in enumerate(steps))
260
+ gotchas_md = "\n".join(f"- {g}" for g in gotchas) if gotchas else "None"
261
+
262
+ conn = sqlite3.connect(str(NEXO_DB))
263
+ # Check if skill already exists
264
+ existing = conn.execute("SELECT id FROM skills WHERE id = ?", (skill_id,)).fetchone()
265
+ if existing:
266
+ conn.close()
267
+ return {"success": False, "error": f"Skill {skill_id} already exists", "id": skill_id}
268
+
269
+ now = datetime.now().isoformat(timespec='seconds')
270
+ conn.execute(
271
+ """INSERT INTO skills
272
+ (id, name, description, level, trust_score, tags, trigger_patterns,
273
+ source_sessions, linked_learnings, created_at, updated_at)
274
+ VALUES (?, ?, ?, 'draft', 50, ?, ?, ?, '[]', ?, ?)""",
275
+ (skill_id, name, description, tags, trigger_patterns, source_sessions, now, now),
276
+ )
277
+ conn.commit()
278
+ conn.close()
279
+ return {"success": True, "id": skill_id, "name": name}
280
+ except Exception as e:
281
+ return {"success": False, "error": str(e)}
282
+
283
+
206
284
  def create_abandoned_followups(synthesis: dict) -> list[dict]:
207
285
  """Create followups for truly abandoned projects."""
208
286
  results = []
@@ -494,6 +572,11 @@ def apply_action(action: dict, run_id: str) -> dict:
494
572
  log_entry["status"] = "applied" if result.get("success") else "error"
495
573
  log_entry["details"] = result
496
574
 
575
+ elif action_type == "skill_create":
576
+ result = create_skill(content)
577
+ log_entry["status"] = "applied" if result.get("success") else "error"
578
+ log_entry["details"] = result
579
+
497
580
  elif action_type == "morning_briefing_item":
498
581
  # These are included in the briefing file, not applied separately
499
582
  log_entry["status"] = "included_in_briefing"
@@ -585,6 +668,26 @@ def main():
585
668
  else:
586
669
  print(f" Trust skip: {trust_result.get('error', '?')}")
587
670
 
671
+ # Create skills from synthesis
672
+ skills_data = synthesis.get("skills", [])
673
+ if skills_data:
674
+ print(f"[apply] Creating {len(skills_data)} skill(s)...")
675
+ for skill_data in skills_data:
676
+ if skill_data.get("confidence", 0) < 0.7:
677
+ continue
678
+ if skill_data.get("merge_with"):
679
+ print(f" Skip {skill_data.get('id', '?')}: merge candidate (needs runtime merge)")
680
+ continue
681
+ result = create_skill(skill_data)
682
+ if result.get("success"):
683
+ stats["applied"] += 1
684
+ print(f" Skill created: {result['id']} — {result.get('name', '')[:50]}")
685
+ elif "already exists" in result.get("error", ""):
686
+ stats["skipped_dedupe"] += 1
687
+ else:
688
+ stats["errors"] += 1
689
+ print(f" Skill error: {result.get('error', 'unknown')}", file=sys.stderr)
690
+
588
691
  # Create followups for abandoned projects
589
692
  abandoned_results = create_abandoned_followups(synthesis)
590
693
  for r in abandoned_results: