nexo-brain 2.2.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 +4 -4
- package/package.json +1 -1
- package/scripts/migrate-v1.7-to-v1.8.py +2 -2
- package/scripts/nexo-preflight.sh +236 -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-314.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/auto_update.py +25 -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-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.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-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.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-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/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest.json +6 -13
- package/src/crons/sync.py +151 -6
- 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__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.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__/_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__/_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/_cron_runs.py +74 -0
- package/src/db/_episodic.py +40 -6
- package/src/db/_schema.py +64 -0
- package/src/db/_skills.py +514 -0
- package/src/hooks/session-stop.sh +13 -101
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.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/episodic_memory.py +5 -3
- package/src/plugins/schedule.py +212 -0
- package/src/plugins/skills.py +264 -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/deep-sleep/apply_findings.py +110 -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 +29 -1
- package/src/scripts/deep-sleep/synthesize.py +3 -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 +156 -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 +72 -19
- package/src/server.py +5 -1
- package/src/scripts/nexo-github-monitor.py +0 -256
|
@@ -166,7 +166,8 @@ def handle_session_diary_write(decisions: str, summary: str,
|
|
|
166
166
|
user_signals: str = '',
|
|
167
167
|
domain: str = '',
|
|
168
168
|
session_id: str = '',
|
|
169
|
-
self_critique: str = ''
|
|
169
|
+
self_critique: str = '',
|
|
170
|
+
source: str = 'claude') -> str:
|
|
170
171
|
"""Write session diary entry at end of session. OBLIGATORIO antes de cerrar.
|
|
171
172
|
|
|
172
173
|
Args:
|
|
@@ -179,13 +180,14 @@ def handle_session_diary_write(decisions: str, summary: str,
|
|
|
179
180
|
user_signals: Observable signals from user during session — response speed (fast='s' vs detailed explanations), tone (direct, frustrated, exploratory, excited), corrections given, topics he initiated vs topics NEXO initiated. Factual observations only, not interpretations.
|
|
180
181
|
domain: Project context: ecommerce, project-a, nexo, project-b, server, other
|
|
181
182
|
session_id: Current session ID
|
|
182
|
-
self_critique: REQUIRED. Honest post-mortem
|
|
183
|
+
self_critique: REQUIRED. Honest post-mortem.
|
|
184
|
+
source: Session type. 'claude' for human-interactive sessions (default), 'cron' for automated cron jobs. Affects visibility at startup.
|
|
183
185
|
"""
|
|
184
186
|
sid = session_id or 'unknown'
|
|
185
187
|
# Clean up draft — manual diary supersedes it
|
|
186
188
|
from db import delete_diary_draft
|
|
187
189
|
delete_diary_draft(sid)
|
|
188
|
-
result = write_session_diary(sid, decisions, summary, discarded, pending, context_next, mental_state, domain=domain, user_signals=user_signals, self_critique=self_critique)
|
|
190
|
+
result = write_session_diary(sid, decisions, summary, discarded, pending, context_next, mental_state, domain=domain, user_signals=user_signals, self_critique=self_critique, source=source)
|
|
189
191
|
if "error" in result:
|
|
190
192
|
return f"ERROR: {result['error']}"
|
|
191
193
|
_cognitive_ingest_safe(summary, "diary", f"diary#{result.get('id','')}", f"Session {sid} summary", domain)
|
|
@@ -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
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|