nexo-brain 2.4.0 → 2.5.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.
Files changed (80) hide show
  1. package/README.md +65 -2
  2. package/bin/nexo-brain.js +208 -11
  3. package/bin/nexo.js +55 -0
  4. package/community/skills/.gitkeep +1 -0
  5. package/package.json +5 -2
  6. package/src/auto_update.py +158 -8
  7. package/src/cli.py +605 -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/hooks/post-compact.sh +5 -1
  54. package/src/hooks/pre-compact.sh +1 -1
  55. package/src/plugins/doctor.py +36 -0
  56. package/src/plugins/evolution.py +2 -1
  57. package/src/plugins/skills.py +135 -175
  58. package/src/requirements.txt +1 -0
  59. package/src/script_registry.py +322 -0
  60. package/src/scripts/deep-sleep/apply_findings.py +63 -48
  61. package/src/scripts/deep-sleep/extract-prompt.md +14 -0
  62. package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
  63. package/src/scripts/deep-sleep/synthesize.py +37 -1
  64. package/src/scripts/nexo-dashboard.sh +29 -0
  65. package/src/scripts/nexo-day-orchestrator.sh +139 -0
  66. package/src/scripts/nexo-evolution-run.py +2 -1
  67. package/src/scripts/nexo-learning-housekeep.py +1 -1
  68. package/src/scripts/nexo-watchdog.sh +1 -1
  69. package/src/server.py +9 -5
  70. package/src/skills/run-runtime-doctor/guide.md +12 -0
  71. package/src/skills/run-runtime-doctor/script.py +21 -0
  72. package/src/skills/run-runtime-doctor/skill.json +25 -0
  73. package/src/skills_runtime.py +347 -0
  74. package/src/tools_menu.py +3 -2
  75. package/src/tools_sessions.py +126 -0
  76. package/src/user_context.py +46 -0
  77. package/templates/nexo_helper.py +45 -0
  78. package/templates/script-template.py +44 -0
  79. package/templates/skill-script-template.py +39 -0
  80. package/templates/skill-template.md +33 -0
@@ -0,0 +1,139 @@
1
+ #!/bin/bash
2
+ # ============================================================================
3
+ # NEXO Day Orchestrator — autonomous NEXO cycle every 15 min
4
+ # Schedule: keepAlive, self-enforced operating hours (default 8:00-23:00)
5
+ #
6
+ # This is NOT a Python script that simulates intelligence.
7
+ # This launches Claude Code as NEXO with full MCP access.
8
+ # NEXO thinks, acts, and reports — like any interactive session.
9
+ # ============================================================================
10
+ set -euo pipefail
11
+
12
+ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
13
+ LOG_DIR="$NEXO_HOME/logs"
14
+ mkdir -p "$LOG_DIR" "$NEXO_HOME/operations"
15
+
16
+ # --- Configuration ---
17
+ CYCLE_INTERVAL=900 # 15 minutes between cycles
18
+ CYCLE_TIMEOUT=600 # 10 min max per cycle
19
+ MAX_TURNS=30 # Claude max turns per cycle
20
+ HOUR_START=8
21
+ HOUR_END=23
22
+
23
+ # --- Find Claude CLI ---
24
+ find_claude() {
25
+ for candidate in \
26
+ "$(command -v claude 2>/dev/null)" \
27
+ "$HOME/.claude/local/claude" \
28
+ "/opt/homebrew/bin/claude" \
29
+ "/usr/local/bin/claude"; do
30
+ if [ -n "$candidate" ] && [ -x "$candidate" ]; then
31
+ echo "$candidate"
32
+ return 0
33
+ fi
34
+ done
35
+ return 1
36
+ }
37
+
38
+ CLAUDE=$(find_claude) || {
39
+ echo "$(date '+%Y-%m-%d %H:%M') ERROR: claude CLI not found" >&2
40
+ exit 1
41
+ }
42
+
43
+ # --- Prevent overlapping cycles ---
44
+ LOCKFILE="$NEXO_HOME/operations/.orchestrator.lock"
45
+ acquire_lock() {
46
+ if [ -f "$LOCKFILE" ]; then
47
+ local pid
48
+ pid=$(cat "$LOCKFILE" 2>/dev/null || echo "")
49
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
50
+ return 1 # Still running
51
+ fi
52
+ fi
53
+ echo $$ > "$LOCKFILE"
54
+ return 0
55
+ }
56
+ release_lock() { rm -f "$LOCKFILE"; }
57
+
58
+ # --- The orchestrator prompt ---
59
+ PROMPT='You are NEXO in autonomous orchestrator mode. The user is NOT present. You have 5 minutes max.
60
+
61
+ ABSOLUTE PRIORITY: act, do not list. If you can do something, do it. If you need the user, send email.
62
+
63
+ CHECKLIST (in this order):
64
+
65
+ 1. OVERDUE FOLLOWUPS: nexo_reminders(filter="due") + nexo_reminders(filter="followups")
66
+ - NEXO tasks (verify, check, monitor) → DO THEM NOW
67
+ - Tasks needing user decision → accumulate for email
68
+ - Completed ones → nexo_followup_complete
69
+
70
+ 2. EMAIL: nexo_email_inbox(unread_only=true, limit=10)
71
+ - Emails you can process → process them
72
+ - Important emails for user → accumulate for email
73
+
74
+ 3. INFRASTRUCTURE: nexo_doctor(tier="runtime")
75
+ - If degraded/critical → try to fix
76
+
77
+ 4. EMAIL TO USER (only if there is something to report):
78
+ - nexo_email_send with clean HTML summary
79
+ - Only what needs attention or decision
80
+ - Include what you ALREADY DID (not just pending items)
81
+ - If nothing relevant → DO NOT send email
82
+ - Max 1 email per cycle
83
+
84
+ 5. DIARY: nexo_session_diary_write with what you did
85
+
86
+ RULES:
87
+ - DO NOT ask permission. autonomy=full
88
+ - DO NOT send empty or "all ok" emails
89
+ - DO NOT list things without acting
90
+ - If a followup is executable → execute it before reporting
91
+ - Use nexo_heartbeat at start
92
+ - Clean close: diary + nexo_stop'
93
+
94
+ # --- Main loop ---
95
+ echo "$(date '+%Y-%m-%d %H:%M') NEXO Day Orchestrator starting (PID $$)"
96
+ echo " Claude: $CLAUDE"
97
+ echo " Cycle: every ${CYCLE_INTERVAL}s, ${HOUR_START}:00-${HOUR_END}:00"
98
+ echo " Timeout: ${CYCLE_TIMEOUT}s, max turns: $MAX_TURNS"
99
+
100
+ while true; do
101
+ HOUR=$(date +%H | sed 's/^0//')
102
+
103
+ # Outside operating hours — sleep and check again
104
+ if [ "$HOUR" -lt "$HOUR_START" ] || [ "$HOUR" -ge "$HOUR_END" ]; then
105
+ sleep 300 # Check every 5 min if we're back in hours
106
+ continue
107
+ fi
108
+
109
+ # Try to acquire lock
110
+ if ! acquire_lock; then
111
+ echo "$(date '+%Y-%m-%d %H:%M') Previous cycle still running. Skipping."
112
+ sleep "$CYCLE_INTERVAL"
113
+ continue
114
+ fi
115
+
116
+ TIMESTAMP=$(date '+%Y-%m-%d_%H%M')
117
+ LOGFILE="$LOG_DIR/orchestrator-$TIMESTAMP.log"
118
+ echo "$(date '+%Y-%m-%d %H:%M') Cycle starting..."
119
+
120
+ # Launch Claude Code as NEXO
121
+ set +e
122
+ timeout "$CYCLE_TIMEOUT" "$CLAUDE" \
123
+ --dangerously-skip-permissions \
124
+ -p "$PROMPT" \
125
+ --max-turns "$MAX_TURNS" \
126
+ >>"$LOGFILE" 2>&1
127
+ EXIT_CODE=$?
128
+ set -e
129
+
130
+ echo "$(date '+%Y-%m-%d %H:%M') Cycle finished (exit $EXIT_CODE)" | tee -a "$LOGFILE"
131
+
132
+ release_lock
133
+
134
+ # Clean old logs (keep 7 days)
135
+ find "$LOG_DIR" -name "orchestrator-*.log" -mtime +7 -delete 2>/dev/null || true
136
+
137
+ # Sleep until next cycle
138
+ sleep "$CYCLE_INTERVAL"
139
+ done
@@ -45,9 +45,10 @@ AUTO_SAFE_PREFIXES = [
45
45
  str(CLAUDE_DIR / "coordination") + "/",
46
46
  ]
47
47
 
48
- # Public mode: only user-created scripts — NEVER core, cortex, or plugins
48
+ # Public mode: user scripts and plugins only — NEVER core code
49
49
  AUTO_SAFE_PREFIXES_PUBLIC = [
50
50
  str(CLAUDE_DIR / "scripts") + "/",
51
+ str(CLAUDE_DIR / "plugins") + "/",
51
52
  ]
52
53
 
53
54
  # ── Immutable files — NEVER touch (applies to ALL modes) ────────────────
@@ -70,7 +70,7 @@ def adjust_weights(conn):
70
70
  priority = l["priority"] or "medium"
71
71
 
72
72
  # Priority floor — critical learnings never drop below 0.5
73
- priority_floor = {"critical": 0.5, "high": 0.3, "medium": 0.1, "low": 0.05}[priority]
73
+ priority_floor = {"critical": 0.5, "high": 0.3, "medium": 0.1, "low": 0.05}.get(priority, 0.1)
74
74
 
75
75
  new_weight = old_weight
76
76
 
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # ============================================================================
3
3
  # NEXO Watchdog — Comprehensive health monitor for all NEXO services
4
- # Cron: */5 * * * * NEXO_HOME/scripts/nexo-watchdog.sh
4
+ # Schedule: every 30 minutes (interval_seconds: 1800)
5
5
  # ============================================================================
6
6
  # Monitors ALL LaunchAgents, cron jobs, and background processes.
7
7
  # Outputs: watchdog-status.json (machine), watchdog-report.txt (human),
package/src/server.py CHANGED
@@ -8,6 +8,7 @@ import sys
8
8
  from fastmcp import FastMCP
9
9
  from db import init_db, rebuild_fts_index, get_db, close_db, fts_add_dir, fts_remove_dir, fts_list_dirs
10
10
  from tools_sessions import handle_startup, handle_heartbeat, handle_status, handle_context_packet, handle_smart_startup_query
11
+ from user_context import get_context as _get_ctx
11
12
  from tools_coordination import (
12
13
  handle_track, handle_untrack, handle_files,
13
14
  handle_send, handle_ask, handle_answer, handle_check_answer,
@@ -154,12 +155,17 @@ def _server_init():
154
155
  mcp = FastMCP(
155
156
  name="nexo",
156
157
  instructions=(
157
- "NEXO — cognitive co-operator. Save important info from tool results before they clear.\n\n"
158
+ f"{_get_ctx().assistant_name} — cognitive co-operator. Save important info from tool results before they clear.\n\n"
159
+ "## CRITICAL — do these or you WILL get corrected\n"
160
+ "- **Guard (MANDATORY before ANY code edit):** `nexo_guard_check(files='...', area='...')` BEFORE editing code. "
161
+ "No exceptions. Blocking rules→resolve first. `nexo_track(sid=SID, paths=[...])` before shared files\n"
162
+ "- **Skills (MANDATORY before multi-step tasks):** `nexo_skill_match(task)` to find reusable procedures. "
163
+ "If match found, read it and follow the steps. After completion, `nexo_skill_result(id, success, context)` to record outcome.\n"
164
+ "- **Learnings (MANDATORY on corrections):** When you discover a bug, pattern, or get corrected→`nexo_learning_add` IMMEDIATELY. "
165
+ "Do NOT batch. Do NOT wait until end of session.\n\n"
158
166
  "## Rules\n"
159
167
  "- **Heartbeat:** `nexo_heartbeat(sid=SID, task='...', context_hint='...')` every user msg. "
160
168
  "React: DIARY REMINDER→write diary, VIBE:NEGATIVE→ultra-concise, AUTO-PRIME→read learnings\n"
161
- "- **Guard:** `nexo_guard_check(files='...', area='...')` BEFORE editing code. "
162
- "Blocking rules→resolve first. `nexo_track(sid=SID, paths=[...])` before shared files\n"
163
169
  "- **Followups:** NEXO tasks, execute silently. 'done'/'all set'→`nexo_followup_complete` NOW. "
164
170
  "Reminders=user's, alert when due\n"
165
171
  "- **Observe:** correction→learning. 'tomorrow'→followup. person→entity. open topic→followup 3d\n"
@@ -175,8 +181,6 @@ mcp = FastMCP(
175
181
  "write `nexo_session_diary_write(...)` with self_critique BEFORE responding. "
176
182
  "Detect intent, not keywords. If session closes without diary, auto_close handles it.\n"
177
183
  "- **Cortex:** `nexo_cortex_check` before budget/campaign/architecture changes\n"
178
- "- **Skills:** before multi-step tasks, `nexo_skill_match(task)` to find reusable procedures. "
179
- "If match found, read it and follow the steps. After completion, `nexo_skill_result(id, success, context)` to record outcome.\n"
180
184
  "- **Dissonance:** user contradicts memory→`nexo_cognitive_dissonance`. Frustrated→force=True\n"
181
185
  "- **Trust:** <40=paranoid verify twice, >80=fluid. Check: `nexo_cognitive_trust`"
182
186
  ),
@@ -0,0 +1,12 @@
1
+ # Run Runtime Doctor
2
+
3
+ Use this skill when you want a fast health snapshot of the running NEXO system.
4
+
5
+ ## Steps
6
+ 1. Run the runtime doctor for the requested tier.
7
+ 2. Review the degraded or critical checks first.
8
+ 3. If the report recommends deterministic fixes, decide whether to run them explicitly.
9
+
10
+ ## Gotchas
11
+ - A critical watchdog result reflects a real system issue, not just a stale skill.
12
+ - `all` is broader and slower than `runtime`.
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import subprocess
4
+ import sys
5
+
6
+
7
+ def main() -> int:
8
+ tier = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1] else "runtime"
9
+ nexo_code = os.environ.get("NEXO_CODE", "")
10
+ if not nexo_code:
11
+ print("NEXO_CODE not set", file=sys.stderr)
12
+ return 1
13
+
14
+ cli_py = os.path.join(nexo_code, "cli.py")
15
+ cmd = [sys.executable, cli_py, "doctor", "--tier", tier, "--json"]
16
+ result = subprocess.run(cmd, text=True)
17
+ return result.returncode
18
+
19
+
20
+ if __name__ == "__main__":
21
+ raise SystemExit(main())
@@ -0,0 +1,25 @@
1
+ {
2
+ "id": "SK-RUN-RUNTIME-DOCTOR",
3
+ "name": "Run Runtime Doctor",
4
+ "description": "Runs the NEXO runtime doctor and returns the current health report.",
5
+ "level": "published",
6
+ "mode": "execute",
7
+ "source_kind": "core",
8
+ "execution_level": "read-only",
9
+ "approval_required": false,
10
+ "tags": ["doctor", "diagnostics", "runtime"],
11
+ "trigger_patterns": ["run doctor", "check runtime health", "diagnose nexo"],
12
+ "params_schema": {
13
+ "tier": {
14
+ "type": "string",
15
+ "required": false,
16
+ "default": "runtime",
17
+ "enum": ["boot", "runtime", "deep", "all"]
18
+ }
19
+ },
20
+ "command_template": {
21
+ "argv": ["{{file_path}}", "{{tier}}"]
22
+ },
23
+ "executable_entry": "script.py",
24
+ "stable_after_uses": 10
25
+ }
@@ -0,0 +1,347 @@
1
+ from __future__ import annotations
2
+ """Runtime helpers for Skills v2.
3
+
4
+ This module is the single execution gate for skills. It decides:
5
+ - guide vs execute vs hybrid mode
6
+ - whether a skill is allowed to run
7
+ - how parameters are validated and rendered
8
+ - how execution is routed through the stable `nexo scripts run` CLI
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from db import (
18
+ approve_skill,
19
+ collect_skill_improvement_candidates,
20
+ collect_scriptable_skill_candidates,
21
+ get_featured_skills,
22
+ get_skill,
23
+ get_skill_execution_spec,
24
+ init_db,
25
+ materialize_personal_skill_definition,
26
+ record_skill_usage,
27
+ render_command_template,
28
+ sync_skill_directories,
29
+ update_skill,
30
+ )
31
+ from script_registry import doctor_script
32
+
33
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
34
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
35
+
36
+
37
+ def _parse_params(params) -> dict:
38
+ if isinstance(params, dict):
39
+ return params
40
+ if isinstance(params, str):
41
+ text = params.strip()
42
+ if not text:
43
+ return {}
44
+ return json.loads(text)
45
+ return {}
46
+
47
+
48
+ def _ensure_ready():
49
+ init_db()
50
+
51
+
52
+ def _resolve_mode(requested: str, skill: dict) -> str:
53
+ mode = (requested or "auto").strip().lower()
54
+ if mode in {"guide", "execute", "hybrid"}:
55
+ return mode
56
+ effective = str(skill.get("mode", "") or "").strip().lower()
57
+ if effective in {"guide", "execute", "hybrid"}:
58
+ return effective
59
+ if skill.get("file_path") and skill.get("content"):
60
+ return "hybrid"
61
+ if skill.get("file_path"):
62
+ return "execute"
63
+ return "guide"
64
+
65
+
66
+ def _summarize_skill(skill: dict) -> str:
67
+ steps = []
68
+ gotchas = []
69
+ try:
70
+ steps = json.loads(skill.get("steps", "[]"))
71
+ except json.JSONDecodeError:
72
+ pass
73
+ try:
74
+ gotchas = json.loads(skill.get("gotchas", "[]"))
75
+ except json.JSONDecodeError:
76
+ pass
77
+
78
+ lines = [
79
+ f"[{skill['id']}] {skill['name']}",
80
+ skill.get("description", "") or "(no description)",
81
+ ]
82
+ if steps:
83
+ lines.append("Steps:")
84
+ for index, step in enumerate(steps[:6], 1):
85
+ lines.append(f"{index}. {step}")
86
+ elif skill.get("content"):
87
+ lines.append(skill["content"][:800])
88
+ if gotchas:
89
+ lines.append("Gotchas:")
90
+ for gotcha in gotchas[:4]:
91
+ lines.append(f"- {gotcha}")
92
+ return "\n".join(lines).strip()
93
+
94
+
95
+ def _resolve_cli_command() -> list[str]:
96
+ installed = NEXO_HOME / "bin" / "nexo"
97
+ if installed.is_file():
98
+ return [str(installed)]
99
+ return [sys.executable, str(NEXO_CODE / "cli.py")]
100
+
101
+
102
+ def _run_skill_script(skill: dict, argv: list[str], timeout: int = 300) -> dict:
103
+ if not argv:
104
+ return {"returncode": 1, "stdout": "", "stderr": "No command to execute"}
105
+
106
+ env = {
107
+ **os.environ,
108
+ "NEXO_HOME": str(NEXO_HOME),
109
+ "NEXO_CODE": str(NEXO_CODE),
110
+ "NEXO_SKILL_ID": skill["id"],
111
+ "NEXO_SKILL_NAME": skill["name"],
112
+ }
113
+
114
+ cli_cmd = _resolve_cli_command()
115
+ cmd = [*cli_cmd, "scripts", "run", argv[0], *argv[1:]]
116
+ try:
117
+ result = subprocess.run(
118
+ cmd,
119
+ capture_output=True,
120
+ text=True,
121
+ timeout=timeout,
122
+ env=env,
123
+ )
124
+ return {
125
+ "returncode": result.returncode,
126
+ "stdout": result.stdout,
127
+ "stderr": result.stderr,
128
+ "command": cmd,
129
+ }
130
+ except subprocess.TimeoutExpired:
131
+ return {
132
+ "returncode": 124,
133
+ "stdout": "",
134
+ "stderr": f"Skill execution timed out after {timeout}s",
135
+ "command": cmd,
136
+ }
137
+
138
+
139
+ def get_featured_skill_summaries(limit: int = 5) -> list[dict]:
140
+ _ensure_ready()
141
+ sync_skill_directories()
142
+ featured = []
143
+ for skill in get_featured_skills(limit=limit):
144
+ triggers = []
145
+ try:
146
+ triggers = json.loads(skill.get("trigger_patterns", "[]"))
147
+ except json.JSONDecodeError:
148
+ pass
149
+ featured.append(
150
+ {
151
+ "id": skill["id"],
152
+ "name": skill["name"],
153
+ "mode": skill.get("mode", "guide"),
154
+ "execution_level": skill.get("execution_level", "none"),
155
+ "source_kind": skill.get("source_kind", "personal"),
156
+ "trust_score": skill.get("trust_score", 0),
157
+ "trigger_patterns": triggers[:3],
158
+ }
159
+ )
160
+ return featured
161
+
162
+
163
+ def apply_skill(skill_id: str, params=None, mode: str = "auto", dry_run: bool = False, context: str = "") -> dict:
164
+ _ensure_ready()
165
+ sync_skill_directories()
166
+ skill = get_skill(skill_id)
167
+ if not skill:
168
+ return {"ok": False, "error": f"Skill {skill_id} not found"}
169
+
170
+ effective_mode = _resolve_mode(mode, skill)
171
+ response = {
172
+ "ok": True,
173
+ "skill_id": skill["id"],
174
+ "skill_name": skill["name"],
175
+ "requested_mode": mode,
176
+ "resolved_mode": effective_mode,
177
+ "approval_state": {
178
+ "approval_required": bool(skill.get("approval_required", 0)),
179
+ "approved_at": skill.get("approved_at", ""),
180
+ "execution_level": skill.get("execution_level", "none"),
181
+ },
182
+ }
183
+
184
+ if effective_mode in {"guide", "hybrid"}:
185
+ response["guide_summary"] = _summarize_skill(skill)
186
+
187
+ if effective_mode in {"execute", "hybrid"}:
188
+ exec_spec = get_skill_execution_spec(skill_id)
189
+ if "error" in exec_spec:
190
+ response["ok"] = False
191
+ response["error"] = exec_spec["error"]
192
+ return response
193
+
194
+ if not skill.get("file_path"):
195
+ response["ok"] = False
196
+ response["error"] = f"Skill {skill_id} has no executable script"
197
+ return response
198
+
199
+ if exec_spec["execution_level"] in {"read-only", "local", "remote"} and not skill.get("approved_at"):
200
+ skill = approve_skill(skill_id, execution_level=exec_spec["execution_level"], approved_by="system:auto")
201
+ response["approval_state"] = {
202
+ "approval_required": bool(skill.get("approval_required", 0)),
203
+ "approved_at": skill.get("approved_at", ""),
204
+ "execution_level": skill.get("execution_level", exec_spec["execution_level"]),
205
+ }
206
+
207
+ doctor = doctor_script(skill["file_path"])
208
+ response["script_doctor"] = doctor
209
+ if doctor["status"] == "fail":
210
+ response["ok"] = False
211
+ response["error"] = "Skill script failed validation"
212
+ return response
213
+
214
+ rendered = render_command_template(skill, _parse_params(params))
215
+ if not rendered.get("ok"):
216
+ response["ok"] = False
217
+ response["error"] = "Invalid skill parameters"
218
+ response["param_errors"] = rendered.get("errors", [])
219
+ return response
220
+
221
+ argv = rendered["argv"] or [skill["file_path"]]
222
+ response["resolved_params"] = rendered["params"]
223
+ response["script_command"] = argv
224
+ if dry_run:
225
+ response["dry_run"] = True
226
+ return response
227
+
228
+ execution = _run_skill_script(skill, argv)
229
+ response["execution_result"] = execution
230
+ success = execution["returncode"] == 0
231
+ record = record_skill_usage(
232
+ skill_id=skill_id,
233
+ success=success,
234
+ context=context or skill["name"],
235
+ notes=(execution["stderr"] or execution["stdout"])[:500],
236
+ )
237
+ response["usage_recorded"] = {
238
+ "success": success,
239
+ "trust_score": record.get("trust_score"),
240
+ "level": record.get("level"),
241
+ "promotion": record.get("_promotion"),
242
+ }
243
+ if not success:
244
+ response["ok"] = False
245
+ response["error"] = f"Skill execution failed with exit {execution['returncode']}"
246
+
247
+ return response
248
+
249
+
250
+ def sync_skills() -> dict:
251
+ _ensure_ready()
252
+ return sync_skill_directories()
253
+
254
+
255
+ def approve_skill_execution(skill_id: str, execution_level: str = "", approved_by: str = "") -> dict:
256
+ _ensure_ready()
257
+ return approve_skill(skill_id, execution_level=execution_level, approved_by=approved_by)
258
+
259
+
260
+ def list_evolution_candidates() -> dict:
261
+ _ensure_ready()
262
+ sync_skill_directories()
263
+ return {
264
+ "scriptable": collect_scriptable_skill_candidates(),
265
+ "improvements": collect_skill_improvement_candidates(),
266
+ }
267
+
268
+
269
+ def auto_promote_skill_evolution(approved_by: str = "system:auto") -> dict:
270
+ """Convert mature guide skills into executable drafts without manual approval."""
271
+ _ensure_ready()
272
+ sync_skill_directories()
273
+ promoted = []
274
+ skipped = []
275
+ for candidate in collect_scriptable_skill_candidates():
276
+ skill = get_skill(candidate["id"])
277
+ if not skill or skill.get("file_path"):
278
+ continue
279
+
280
+ steps = candidate.get("steps") or []
281
+ gotchas = candidate.get("gotchas") or []
282
+ description = candidate.get("description", "") or "Automated skill generated from repeated successful usage."
283
+ lines = [
284
+ "#!/usr/bin/env python3",
285
+ '"""Auto-generated executable skill draft."""',
286
+ "import json",
287
+ "import sys",
288
+ "",
289
+ "def main() -> int:",
290
+ " payload = {",
291
+ f" 'skill_id': {json.dumps(candidate['id'])},",
292
+ f" 'skill_name': {json.dumps(candidate['name'])},",
293
+ f" 'description': {json.dumps(description)},",
294
+ f" 'steps': {json.dumps(steps, ensure_ascii=False)},",
295
+ f" 'gotchas': {json.dumps(gotchas, ensure_ascii=False)},",
296
+ " 'argv': sys.argv[1:],",
297
+ " }",
298
+ " print(json.dumps(payload, ensure_ascii=False))",
299
+ " return 0",
300
+ "",
301
+ 'if __name__ == "__main__":',
302
+ " raise SystemExit(main())",
303
+ "",
304
+ ]
305
+ update = update_skill(
306
+ candidate["id"],
307
+ mode=candidate.get("suggested_mode", "hybrid"),
308
+ execution_level=candidate.get("suggested_execution_level", "read-only"),
309
+ approval_required=0,
310
+ approved_by=approved_by,
311
+ )
312
+ if "error" in update:
313
+ skipped.append({"id": candidate["id"], "reason": update["error"]})
314
+ continue
315
+
316
+ materialized = materialize_personal_skill_definition(
317
+ {
318
+ "id": candidate["id"],
319
+ "name": candidate["name"],
320
+ "description": description,
321
+ "level": skill.get("level", "published"),
322
+ "mode": candidate.get("suggested_mode", "hybrid"),
323
+ "execution_level": candidate.get("suggested_execution_level", "read-only"),
324
+ "approved_by": approved_by,
325
+ "tags": json.loads(skill.get("tags", "[]")) if skill.get("tags") else [],
326
+ "trigger_patterns": candidate.get("trigger_patterns", []),
327
+ "source_sessions": candidate.get("source_sessions", []),
328
+ "steps": steps,
329
+ "gotchas": gotchas,
330
+ "content": skill.get("content", ""),
331
+ "command_template": {"argv": ["{{file_path}}"]},
332
+ "executable_entry": "script.py",
333
+ "script_body": "\n".join(lines),
334
+ }
335
+ )
336
+ if "error" in materialized:
337
+ skipped.append({"id": candidate["id"], "reason": materialized["error"]})
338
+ continue
339
+
340
+ promoted.append(
341
+ {
342
+ "id": candidate["id"],
343
+ "mode": candidate.get("suggested_mode", "hybrid"),
344
+ "execution_level": candidate.get("suggested_execution_level", "read-only"),
345
+ }
346
+ )
347
+ return {"promoted": promoted, "skipped": skipped}
package/src/tools_menu.py CHANGED
@@ -1,8 +1,9 @@
1
- """Menu generator — NEXO operations center."""
1
+ """Menu generator — operations center."""
2
2
 
3
3
  from datetime import datetime, timedelta
4
4
  import json
5
5
  import subprocess
6
+ from user_context import get_context as _get_ctx
6
7
  import sys
7
8
  from pathlib import Path
8
9
  from tools_sessions import handle_status
@@ -109,7 +110,7 @@ def handle_menu() -> str:
109
110
 
110
111
  lines = []
111
112
  lines.append("╔" + "═" * W + "╗")
112
- lines.append("║" + "NEXO — OPERATIONS CENTER".center(W) + "║")
113
+ lines.append("║" + f"{_get_ctx().assistant_name} — OPERATIONS CENTER".center(W) + "║")
113
114
  lines.append("║" + date_str.center(W) + "║")
114
115
  lines.append("╠" + "═" * W + "╣")
115
116