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