nexo-brain 2.4.0 → 2.5.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 (81) hide show
  1. package/README.md +80 -4
  2. package/bin/nexo-brain.js +238 -12
  3. package/bin/nexo.js +55 -0
  4. package/community/skills/.gitkeep +1 -0
  5. package/package.json +11 -3
  6. package/src/auto_update.py +193 -9
  7. package/src/cli.py +719 -0
  8. package/src/cognitive/_ingest.py +1 -1
  9. package/src/cognitive/_memory.py +4 -4
  10. package/src/crons/manifest.json +8 -0
  11. package/src/dashboard/app.py +700 -35
  12. package/src/dashboard/templates/adaptive.html +112 -218
  13. package/src/dashboard/templates/artifacts.html +133 -0
  14. package/src/dashboard/templates/backups.html +136 -0
  15. package/src/dashboard/templates/base.html +413 -0
  16. package/src/dashboard/templates/calendar.html +523 -654
  17. package/src/dashboard/templates/chat.html +356 -0
  18. package/src/dashboard/templates/claims.html +259 -0
  19. package/src/dashboard/templates/cortex.html +262 -0
  20. package/src/dashboard/templates/credentials.html +128 -0
  21. package/src/dashboard/templates/crons.html +370 -0
  22. package/src/dashboard/templates/dashboard.html +383 -578
  23. package/src/dashboard/templates/dreams.html +252 -0
  24. package/src/dashboard/templates/email.html +160 -0
  25. package/src/dashboard/templates/evolution.html +189 -0
  26. package/src/dashboard/templates/feed.html +249 -0
  27. package/src/dashboard/templates/followup_health.html +170 -0
  28. package/src/dashboard/templates/graph.html +191 -269
  29. package/src/dashboard/templates/guard.html +259 -0
  30. package/src/dashboard/templates/inbox.html +220 -346
  31. package/src/dashboard/templates/memory.html +317 -197
  32. package/src/dashboard/templates/operations.html +521 -698
  33. package/src/dashboard/templates/plugins.html +185 -0
  34. package/src/dashboard/templates/rules.html +246 -0
  35. package/src/dashboard/templates/sentiment.html +247 -0
  36. package/src/dashboard/templates/sessions.html +215 -182
  37. package/src/dashboard/templates/skills.html +329 -0
  38. package/src/dashboard/templates/somatic.html +68 -172
  39. package/src/dashboard/templates/triggers.html +133 -0
  40. package/src/dashboard/templates/trust.html +360 -0
  41. package/src/db/__init__.py +5 -0
  42. package/src/db/_schema.py +16 -1
  43. package/src/db/_sessions.py +22 -0
  44. package/src/db/_skills.py +980 -274
  45. package/src/doctor/__init__.py +1 -0
  46. package/src/doctor/formatters.py +52 -0
  47. package/src/doctor/models.py +44 -0
  48. package/src/doctor/orchestrator.py +42 -0
  49. package/src/doctor/providers/__init__.py +1 -0
  50. package/src/doctor/providers/boot.py +206 -0
  51. package/src/doctor/providers/deep.py +292 -0
  52. package/src/doctor/providers/runtime.py +686 -0
  53. package/src/evolution_cycle.py +86 -6
  54. package/src/hooks/post-compact.sh +5 -1
  55. package/src/hooks/pre-compact.sh +1 -1
  56. package/src/plugins/doctor.py +36 -0
  57. package/src/plugins/evolution.py +11 -3
  58. package/src/plugins/skills.py +135 -175
  59. package/src/requirements.txt +1 -0
  60. package/src/script_registry.py +322 -0
  61. package/src/scripts/deep-sleep/apply_findings.py +63 -48
  62. package/src/scripts/deep-sleep/extract-prompt.md +14 -0
  63. package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
  64. package/src/scripts/deep-sleep/synthesize.py +37 -1
  65. package/src/scripts/nexo-dashboard.sh +29 -0
  66. package/src/scripts/nexo-day-orchestrator.sh +139 -0
  67. package/src/scripts/nexo-evolution-run.py +141 -54
  68. package/src/scripts/nexo-learning-housekeep.py +1 -1
  69. package/src/scripts/nexo-watchdog.sh +1 -1
  70. package/src/server.py +9 -5
  71. package/src/skills/run-runtime-doctor/guide.md +12 -0
  72. package/src/skills/run-runtime-doctor/script.py +21 -0
  73. package/src/skills/run-runtime-doctor/skill.json +25 -0
  74. package/src/skills_runtime.py +347 -0
  75. package/src/tools_menu.py +3 -2
  76. package/src/tools_sessions.py +126 -0
  77. package/src/user_context.py +46 -0
  78. package/templates/nexo_helper.py +45 -0
  79. package/templates/script-template.py +44 -0
  80. package/templates/skill-script-template.py +39 -0
  81. package/templates/skill-template.md +33 -0
@@ -34,14 +34,83 @@ PROMPT_FILE = _resolve_evolution_file("evolution-prompt.md")
34
34
  MAX_SNAPSHOTS = 8
35
35
 
36
36
 
37
+ def _normalize_dimensions(raw: dict | None) -> dict:
38
+ normalized = {}
39
+ for key, value in (raw or {}).items():
40
+ canonical_key = "agi" if key == "agi_readiness" else key
41
+ if isinstance(value, dict):
42
+ normalized[canonical_key] = {
43
+ "current": int(value.get("current", 0) or 0),
44
+ "target": int(value.get("target", 0) or 0),
45
+ }
46
+ else:
47
+ normalized[canonical_key] = {
48
+ "current": 0,
49
+ "target": int(value or 0),
50
+ }
51
+ return normalized
52
+
53
+
54
+ def normalize_objective(obj: dict | None) -> dict:
55
+ """Upgrade legacy objective files to the canonical schema."""
56
+ source = dict(obj or {})
57
+
58
+ if "evolution_mode" in source:
59
+ mode = str(source.get("evolution_mode") or "auto").strip().lower()
60
+ else:
61
+ legacy_mode = str(source.get("review_mode") or "").strip().lower()
62
+ if legacy_mode in {"manual", "review"}:
63
+ mode = "review"
64
+ elif legacy_mode in {"managed", "hybrid", "owner", "core"}:
65
+ mode = "managed"
66
+ else:
67
+ mode = "auto"
68
+
69
+ if mode not in {"auto", "review", "managed"}:
70
+ mode = "auto"
71
+
72
+ dimensions = source.get("dimensions")
73
+ if not isinstance(dimensions, dict) or not dimensions:
74
+ dimensions = _normalize_dimensions(source.get("dimension_targets"))
75
+ else:
76
+ dimensions = _normalize_dimensions(dimensions)
77
+
78
+ defaults = {
79
+ "episodic_memory": {"current": 0, "target": 90},
80
+ "autonomy": {"current": 0, "target": 80},
81
+ "proactivity": {"current": 0, "target": 70},
82
+ "self_improvement": {"current": 0, "target": 60},
83
+ "agi": {"current": 0, "target": 20},
84
+ }
85
+ merged_dimensions = dict(defaults)
86
+ merged_dimensions.update(dimensions)
87
+
88
+ normalized = dict(source)
89
+ normalized["evolution_mode"] = mode
90
+ normalized["dimensions"] = merged_dimensions
91
+ normalized["total_evolutions"] = int(source.get("total_evolutions", source.get("cycles_completed", 0)) or 0)
92
+ normalized["last_evolution"] = source.get("last_evolution", source.get("last_cycle"))
93
+ normalized["total_proposals_made"] = int(source.get("total_proposals_made", 0) or 0)
94
+ normalized["total_auto_applied"] = int(source.get("total_auto_applied", 0) or 0)
95
+ normalized["consecutive_failures"] = int(source.get("consecutive_failures", 0) or 0)
96
+ normalized["history"] = source.get("history", []) if isinstance(source.get("history"), list) else []
97
+ normalized["evolution_enabled"] = bool(source.get("evolution_enabled", True))
98
+ normalized.pop("review_mode", None)
99
+ normalized.pop("dimension_targets", None)
100
+ normalized.pop("cycles_completed", None)
101
+ normalized.pop("last_cycle", None)
102
+ return normalized
103
+
104
+
37
105
  def load_objective() -> dict:
38
106
  if OBJECTIVE_FILE.exists():
39
- return json.loads(OBJECTIVE_FILE.read_text())
40
- return {}
107
+ return normalize_objective(json.loads(OBJECTIVE_FILE.read_text()))
108
+ return normalize_objective({})
41
109
 
42
110
 
43
111
  def save_objective(obj: dict):
44
- OBJECTIVE_FILE.write_text(json.dumps(obj, indent=2, ensure_ascii=False))
112
+ OBJECTIVE_FILE.parent.mkdir(parents=True, exist_ok=True)
113
+ OBJECTIVE_FILE.write_text(json.dumps(normalize_objective(obj), indent=2, ensure_ascii=False))
45
114
 
46
115
 
47
116
  def get_week_data(db_path: str) -> dict:
@@ -196,9 +265,18 @@ def build_evolution_prompt(week_data: dict, objective: dict) -> str:
196
265
  "current_scores": {dim: m["score"] for dim, m in week_data.get("current_metrics", {}).items()},
197
266
  }
198
267
 
199
- mode = objective.get("evolution_mode", "auto")
268
+ mode = normalize_objective(objective).get("evolution_mode", "auto")
200
269
  total = objective.get("total_evolutions", 0)
201
270
  max_auto = max_auto_changes(total)
271
+ if mode == "review":
272
+ mode_desc = "review-only, nothing executes automatically"
273
+ safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/"
274
+ elif mode == "managed":
275
+ mode_desc = f"owner-managed, max {max_auto} auto-applied changes with rollback and followups"
276
+ safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/, NEXO_CODE/src, repo bin/docs/templates/tests"
277
+ else:
278
+ mode_desc = f"public auto, max {max_auto} auto-applied changes in personal safe zones"
279
+ safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/"
202
280
 
203
281
  prompt = f"""You are NEXO Evolution — the weekly self-improvement cycle.
204
282
 
@@ -212,7 +290,7 @@ WEEK SUMMARY:
212
290
  - {stats['evolution_history']} past evolution proposals
213
291
  - Current scores: {json.dumps(stats['current_scores'])}
214
292
 
215
- MODE: {mode} ({"proposals only, owner reviews" if mode == "review" else f"max {max_auto} auto-applied changes"})
293
+ MODE: {mode} ({mode_desc})
216
294
  CYCLE: #{total + 1}
217
295
 
218
296
  INVESTIGATE using these tools:
@@ -232,9 +310,11 @@ LOOK FOR:
232
310
  - Patterns in self-critique that suggest systemic issues
233
311
 
234
312
  SAFETY:
235
- - Safe zones for auto changes: ~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/
313
+ - Safe zones for this mode: {safe_zones}
236
314
  - IMMUTABLE files (never touch): db.py, server.py, plugin_loader.py, cognitive.py, CLAUDE.md
237
315
  - Every change needs: what file, what to change, why, risk, how to verify
316
+ - AUTO changes must be deterministic. If the edit is ambiguous, risky, or needs human taste, mark it as "propose".
317
+ - In managed mode, failed AUTO changes will be rolled back automatically and turned into followups with evidence.
238
318
 
239
319
  OUTPUT FORMAT (JSON):
240
320
  {{
@@ -18,8 +18,12 @@ fi
18
18
  # PreCompact writes the SID to /tmp/nexo-compacting-sid
19
19
  TARGET_SID=""
20
20
  if [ -f /tmp/nexo-compacting-sid ]; then
21
- TARGET_SID=$(cat /tmp/nexo-compacting-sid 2>/dev/null || echo "")
21
+ RAW_SID=$(cat /tmp/nexo-compacting-sid 2>/dev/null || echo "")
22
22
  rm -f /tmp/nexo-compacting-sid
23
+ # Validate SID format: must be nexo-DIGITS-DIGITS
24
+ if [[ "$RAW_SID" =~ ^nexo-[0-9]+-[0-9]+$ ]]; then
25
+ TARGET_SID="$RAW_SID"
26
+ fi
23
27
  fi
24
28
 
25
29
  CHECKPOINT=""
@@ -22,7 +22,7 @@ if [ -f "$NEXO_DB" ]; then
22
22
  SELECT sid FROM sessions ORDER BY last_update_epoch DESC LIMIT 1
23
23
  " 2>/dev/null || echo "")
24
24
 
25
- if [ -n "$LATEST_SID" ]; then
25
+ if [ -n "$LATEST_SID" ] && [[ "$LATEST_SID" =~ ^nexo-[0-9]+-[0-9]+$ ]]; then
26
26
  # Write SID to temp file so PostCompact knows which session compacted
27
27
  echo "$LATEST_SID" > /tmp/nexo-compacting-sid
28
28
  # Pull diary draft data into checkpoint
@@ -0,0 +1,36 @@
1
+ """Doctor plugin — exposes nexo_doctor as MCP tool."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ import os
6
+ from pathlib import Path
7
+
8
+ # Ensure src/ is importable
9
+ SRC_DIR = Path(__file__).resolve().parent.parent
10
+ if str(SRC_DIR) not in sys.path:
11
+ sys.path.insert(0, str(SRC_DIR))
12
+
13
+
14
+ def handle_doctor(tier: str = "boot", fix: bool = False, output: str = "text") -> str:
15
+ """Unified diagnostic report for boot/runtime/deep health.
16
+
17
+ Args:
18
+ tier: Diagnostic tier — boot, runtime, deep, or all (default: boot)
19
+ fix: Apply deterministic fixes (default: False)
20
+ output: Output format — text or json (default: text)
21
+ """
22
+ from doctor.orchestrator import run_doctor
23
+ from doctor.formatters import format_report
24
+
25
+ if tier not in ("boot", "runtime", "deep", "all"):
26
+ return f"Invalid tier '{tier}'. Use: boot, runtime, deep, all"
27
+ if output not in ("text", "json"):
28
+ return f"Invalid output '{output}'. Use: text, json"
29
+
30
+ report = run_doctor(tier=tier, fix=fix)
31
+ return format_report(report, fmt=output)
32
+
33
+
34
+ TOOLS = [
35
+ (handle_doctor, "nexo_doctor", "Unified diagnostic report for boot/runtime/deep health."),
36
+ ]
@@ -18,7 +18,8 @@ def handle_evolution_status() -> str:
18
18
  "agi": "AGI",
19
19
  }
20
20
 
21
- lines = ["NEXO EVOLUTION STATUS:"]
21
+ from user_context import get_context
22
+ lines = [f"{get_context().assistant_name} EVOLUTION STATUS:"]
22
23
  for key, label in BARS.items():
23
24
  m = metrics.get(key)
24
25
  if m:
@@ -42,8 +43,15 @@ def handle_evolution_history(limit: int = 10) -> str:
42
43
 
43
44
  lines = [f"EVOLUTION HISTORY ({len(history)} entries):"]
44
45
  for h in history:
45
- status_icon = {"applied": "✓", "rolled_back": "✗", "proposed": "?",
46
- "accepted": "✓✓", "rejected": "✗✗"}.get(h["status"], "·")
46
+ status_icon = {
47
+ "applied": "",
48
+ "rolled_back": "↺",
49
+ "blocked": "⛔",
50
+ "proposed": "?",
51
+ "pending_review": "…",
52
+ "accepted": "✓✓",
53
+ "rejected": "✗✗",
54
+ }.get(h["status"], "·")
47
55
  lines.append(f" {status_icon} #{h['id']} [{h['classification']}] {h['dimension']}")
48
56
  lines.append(f" {h['proposal'][:100]}")
49
57
  if h.get("test_result"):
@@ -1,264 +1,224 @@
1
- """Skills plugin — reusable procedures extracted from complex tasks.
1
+ """Skills plugin — reusable procedures, executable skills, and feedback loops."""
2
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
- """
3
+ from __future__ import annotations
9
4
 
10
5
  import json
6
+
11
7
  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,
8
+ create_skill,
9
+ delete_skill,
10
+ get_skill,
11
+ get_skill_stats,
12
+ list_skills,
13
+ match_skills,
14
+ merge_skills,
15
+ record_skill_usage,
16
+ search_skills,
17
+ update_skill,
18
+ )
19
+ from skills_runtime import (
20
+ apply_skill,
21
+ approve_skill_execution,
22
+ get_featured_skill_summaries,
23
+ list_evolution_candidates,
24
+ sync_skills,
15
25
  )
16
26
 
17
27
 
18
28
  def handle_skill_create(
19
29
  id: str,
20
30
  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 = '',
31
+ description: str = "",
32
+ level: str = "draft",
33
+ tags: str = "[]",
34
+ trigger_patterns: str = "[]",
35
+ source_sessions: str = "[]",
36
+ linked_learnings: str = "[]",
37
+ file_path: str = "",
38
+ mode: str = "",
39
+ source_kind: str = "personal",
40
+ execution_level: str = "none",
41
+ approval_required: bool = False,
42
+ params_schema: str = "{}",
43
+ command_template: str = "{}",
44
+ executable_entry: str = "",
28
45
  ) -> 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-'):
46
+ if not id.startswith("SK-"):
50
47
  return "ERROR: Skill ID must start with 'SK-' (e.g., SK-DEPLOY-CHROME-EXT)"
51
-
52
- existing = get_skill(id)
53
- if existing:
48
+ if get_skill(id):
54
49
  return f"ERROR: Skill {id} already exists. Use nexo_skill_update to modify."
55
50
 
56
51
  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,
52
+ skill_id=id,
53
+ name=name,
54
+ description=description,
55
+ level=level,
56
+ tags=tags,
57
+ trigger_patterns=trigger_patterns,
58
+ source_sessions=source_sessions,
59
+ linked_learnings=linked_learnings,
60
60
  file_path=file_path,
61
+ mode=mode,
62
+ source_kind=source_kind,
63
+ execution_level=execution_level,
64
+ approval_required=approval_required,
65
+ params_schema=params_schema,
66
+ command_template=command_template,
67
+ executable_entry=executable_entry,
61
68
  )
62
69
  if "error" in result:
63
70
  return f"ERROR: {result['error']}"
64
71
 
65
72
  return (
66
- f"Skill {id} created ({level}, trust={result.get('trust_score', 50)}).\n"
73
+ f"Skill {id} created ({result['level']}, {result.get('mode', 'guide')}, trust={result.get('trust_score', 50)}).\n"
67
74
  f" Name: {name}\n"
68
- f" Tags: {tags}\n"
69
- f" Triggers: {trigger_patterns}"
75
+ f" Source: {result.get('source_kind', source_kind)}\n"
76
+ f" Execution: {result.get('execution_level', execution_level)}"
70
77
  )
71
78
 
72
79
 
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
- """
80
+ def handle_skill_match(task: str, level: str = "") -> str:
83
81
  matches = match_skills(task, level=level)
84
82
  if not matches:
85
83
  return f"No skills found for: '{task}'"
86
84
 
87
85
  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 ""
86
+ for match in matches:
87
+ match_method = match.pop("_match", "unknown")
91
88
  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]}"
89
+ f" [{match['id']}] {match['name']} ({match['level']}, {match.get('mode', 'guide')}, "
90
+ f"{match.get('source_kind', 'personal')}, trust={match['trust_score']}, used={match['use_count']}x) "
91
+ f"via {match_method}"
95
92
  )
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
93
+ lines.append(f" {match['description'][:140]}")
102
94
  return "\n".join(lines)
103
95
 
104
96
 
105
97
  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
98
  skill = get_skill(id)
112
99
  if not skill:
113
100
  return f"ERROR: Skill {id} not found."
114
101
 
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
102
  lines = [
123
103
  f"SKILL: {skill['id']}",
124
104
  f" Name: {skill['name']}",
125
105
  f" Description: {skill['description']}",
126
106
  f" Level: {skill['level']}",
107
+ f" Mode: {skill.get('mode', 'guide')}",
108
+ f" Source: {skill.get('source_kind', 'personal')}",
127
109
  f" Trust: {skill['trust_score']}",
128
- f" File: {skill['file_path'] or '(none)'}",
129
- f" Tags: {skill['tags']}",
110
+ f" Execution level: {skill.get('execution_level', 'none')}",
111
+ f" Approval required: {bool(skill.get('approval_required', 0))}",
112
+ f" Approved at: {skill.get('approved_at') or 'no'}",
113
+ f" Definition: {skill.get('definition_path') or '(none)'}",
114
+ f" File: {skill.get('file_path') or '(none)'}",
115
+ f" Params schema: {skill.get('params_schema', '{}')}",
130
116
  f" Triggers: {skill['trigger_patterns']}",
131
- f" Source sessions: {skill['source_sessions']}",
132
- f" Linked learnings: {skill['linked_learnings']}",
133
117
  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
118
  ]
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
119
  return "\n".join(lines)
148
120
 
149
121
 
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
- """
122
+ def handle_skill_result(id: str, success: bool = True, context: str = "", notes: str = "") -> str:
163
123
  result = record_skill_usage(skill_id=id, success=success, context=context, notes=notes)
164
124
  if "error" in result:
165
125
  return f"ERROR: {result['error']}"
166
126
 
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']})"
127
+ promotion = result.get("_promotion")
128
+ msg = f"Skill {id} usage recorded: {'SUCCESS' if success else 'FAILURE'} (trust={result['trust_score']})"
170
129
  if promotion:
171
130
  msg += f"\n ⚡ PROMOTION: {promotion}"
172
131
  return msg
173
132
 
174
133
 
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)
134
+ def handle_skill_list(level: str = "", tag: str = "", source_kind: str = "") -> str:
135
+ skills = list_skills(level=level, tag=tag, source_kind=source_kind)
183
136
  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 ''}."
137
+ return "No skills found."
188
138
 
189
139
  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 ""
140
+ for skill in skills:
193
141
  lines.append(
194
- f" [{s['id']}] {s['name']} ({s['level']}, trust={s['trust_score']}, "
195
- f"used={s['use_count']}x{used}){fp}"
142
+ f" [{skill['id']}] {skill['name']} ({skill['level']}, {skill.get('mode', 'guide')}, "
143
+ f"{skill.get('source_kind', 'personal')}, trust={skill['trust_score']}, used={skill['use_count']}x)"
196
144
  )
197
145
  return "\n".join(lines)
198
146
 
199
147
 
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
- """
148
+ def handle_skill_merge(id1: str, id2: str, keep_id: str = "") -> str:
211
149
  result = merge_skills(id1, id2, keep_id=keep_id)
212
150
  if "error" in result:
213
151
  return f"ERROR: {result['error']}"
214
-
215
- merged_from = result.pop('_merged_from', '?')
216
152
  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']}"
153
+ f"Skills merged. Kept {result['id']}, deleted {result['_merged_from']}.\n"
154
+ f" Trust: {result['trust_score']}, Uses: {result['use_count']}"
220
155
  )
221
156
 
222
157
 
223
158
  def handle_skill_stats() -> str:
224
- """Show aggregate skill statistics: total count, by level, avg trust, usage rates."""
225
159
  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)
160
+ return (
161
+ "SKILL STATS:\n"
162
+ f" Total: {stats['total']}\n"
163
+ f" By level: {', '.join(f'{k}={v}' for k, v in sorted(stats['by_level'].items()))}\n"
164
+ f" Avg trust: {stats['avg_trust']}\n"
165
+ f" Total uses: {stats['total_uses']} (success rate: {stats['success_rate']}%)\n"
166
+ f" Uses last 7d: {stats['uses_last_7d']}"
167
+ )
168
+
169
+
170
+ def handle_skill_apply(id: str, params: str = "{}", mode: str = "auto", dry_run: bool = False, context: str = "") -> str:
171
+ return json.dumps(apply_skill(id, params=params, mode=mode, dry_run=dry_run, context=context), ensure_ascii=False)
172
+
173
+
174
+ def handle_skill_approve(id: str, execution_level: str = "", approved_by: str = "") -> str:
175
+ result = approve_skill_execution(id, execution_level=execution_level, approved_by=approved_by)
176
+ if "error" in result:
177
+ return f"ERROR: {result['error']}"
178
+ return (
179
+ f"Skill {id} approved.\n"
180
+ f" Execution level: {result.get('execution_level', 'none')}\n"
181
+ f" Approved at: {result.get('approved_at', '')}\n"
182
+ f" Approved by: {result.get('approved_by', '')}"
183
+ )
184
+
185
+
186
+ def handle_skill_sync() -> str:
187
+ result = sync_skills()
188
+ return json.dumps(result, ensure_ascii=False)
189
+
190
+
191
+ def handle_skill_featured(limit: int = 5) -> str:
192
+ return json.dumps(get_featured_skill_summaries(limit=limit), ensure_ascii=False)
193
+
194
+
195
+ def handle_skill_evolution_candidates() -> str:
196
+ return json.dumps(list_evolution_candidates(), ensure_ascii=False)
236
197
 
237
198
 
238
- # Plugin registration — TOOLS array consumed by plugin_loader.py
239
199
  TOOLS = [
240
200
  (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
-
201
+ "Create a new skill with guide/execute/hybrid metadata, triggers, params schema, and execution level."),
244
202
  (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
-
203
+ "Find skills matching a task description. Call before multi-step tasks."),
248
204
  (handle_skill_get, "nexo_skill_get",
249
- "Get a skill's full details including procedure, tags, triggers, and usage history."),
250
-
205
+ "Get a skill's full details, including execution metadata and approval state."),
251
206
  (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
-
207
+ "Record the result of using a skill. Updates trust and promotions."),
255
208
  (handle_skill_list, "nexo_skill_list",
256
- "List all skills, optionally filtered by level (trace/draft/published/archived) or tag."),
257
-
209
+ "List skills, optionally filtered by level, tag, or source kind."),
258
210
  (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
-
211
+ "Merge two similar skills into one."),
262
212
  (handle_skill_stats, "nexo_skill_stats",
263
- "Show aggregate skill statistics: count by level, average trust, usage rates."),
213
+ "Show aggregate skill statistics."),
214
+ (handle_skill_apply, "nexo_skill_apply",
215
+ "Apply a skill in guide, execute, or hybrid mode. Execution goes through the stable nexo scripts runtime."),
216
+ (handle_skill_approve, "nexo_skill_approve",
217
+ "Approve a local/remote executable skill so it can run."),
218
+ (handle_skill_sync, "nexo_skill_sync",
219
+ "Sync filesystem skill definitions from personal/core/community directories into SQLite."),
220
+ (handle_skill_featured, "nexo_skill_featured",
221
+ "Return featured published/stable skills for startup discovery."),
222
+ (handle_skill_evolution_candidates, "nexo_skill_evolution_candidates",
223
+ "Return candidates for skill improvement or text-to-script evolution."),
264
224
  ]
@@ -10,3 +10,4 @@ fastembed
10
10
  fastapi
11
11
  uvicorn
12
12
  pydantic
13
+ jinja2