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.
- package/README.md +65 -2
- package/bin/nexo-brain.js +208 -11
- package/bin/nexo.js +55 -0
- package/community/skills/.gitkeep +1 -0
- package/package.json +5 -2
- package/src/auto_update.py +158 -8
- package/src/cli.py +605 -0
- package/src/cognitive/_ingest.py +1 -1
- package/src/cognitive/_memory.py +4 -4
- package/src/crons/manifest.json +8 -0
- package/src/dashboard/app.py +700 -35
- package/src/dashboard/templates/adaptive.html +112 -218
- package/src/dashboard/templates/artifacts.html +133 -0
- package/src/dashboard/templates/backups.html +136 -0
- package/src/dashboard/templates/base.html +413 -0
- package/src/dashboard/templates/calendar.html +523 -654
- package/src/dashboard/templates/chat.html +356 -0
- package/src/dashboard/templates/claims.html +259 -0
- package/src/dashboard/templates/cortex.html +262 -0
- package/src/dashboard/templates/credentials.html +128 -0
- package/src/dashboard/templates/crons.html +370 -0
- package/src/dashboard/templates/dashboard.html +383 -578
- package/src/dashboard/templates/dreams.html +252 -0
- package/src/dashboard/templates/email.html +160 -0
- package/src/dashboard/templates/evolution.html +189 -0
- package/src/dashboard/templates/feed.html +249 -0
- package/src/dashboard/templates/followup_health.html +170 -0
- package/src/dashboard/templates/graph.html +191 -269
- package/src/dashboard/templates/guard.html +259 -0
- package/src/dashboard/templates/inbox.html +220 -346
- package/src/dashboard/templates/memory.html +317 -197
- package/src/dashboard/templates/operations.html +521 -698
- package/src/dashboard/templates/plugins.html +185 -0
- package/src/dashboard/templates/rules.html +246 -0
- package/src/dashboard/templates/sentiment.html +247 -0
- package/src/dashboard/templates/sessions.html +215 -182
- package/src/dashboard/templates/skills.html +329 -0
- package/src/dashboard/templates/somatic.html +68 -172
- package/src/dashboard/templates/triggers.html +133 -0
- package/src/dashboard/templates/trust.html +360 -0
- package/src/db/__init__.py +5 -0
- package/src/db/_schema.py +16 -1
- package/src/db/_sessions.py +22 -0
- package/src/db/_skills.py +980 -274
- package/src/doctor/__init__.py +1 -0
- package/src/doctor/formatters.py +52 -0
- package/src/doctor/models.py +44 -0
- package/src/doctor/orchestrator.py +42 -0
- package/src/doctor/providers/__init__.py +1 -0
- package/src/doctor/providers/boot.py +206 -0
- package/src/doctor/providers/deep.py +292 -0
- package/src/doctor/providers/runtime.py +686 -0
- package/src/hooks/post-compact.sh +5 -1
- package/src/hooks/pre-compact.sh +1 -1
- package/src/plugins/doctor.py +36 -0
- package/src/plugins/evolution.py +2 -1
- package/src/plugins/skills.py +135 -175
- package/src/requirements.txt +1 -0
- package/src/script_registry.py +322 -0
- package/src/scripts/deep-sleep/apply_findings.py +63 -48
- package/src/scripts/deep-sleep/extract-prompt.md +14 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
- package/src/scripts/deep-sleep/synthesize.py +37 -1
- package/src/scripts/nexo-dashboard.sh +29 -0
- package/src/scripts/nexo-day-orchestrator.sh +139 -0
- package/src/scripts/nexo-evolution-run.py +2 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -1
- package/src/scripts/nexo-watchdog.sh +1 -1
- package/src/server.py +9 -5
- package/src/skills/run-runtime-doctor/guide.md +12 -0
- package/src/skills/run-runtime-doctor/script.py +21 -0
- package/src/skills/run-runtime-doctor/skill.json +25 -0
- package/src/skills_runtime.py +347 -0
- package/src/tools_menu.py +3 -2
- package/src/tools_sessions.py +126 -0
- package/src/user_context.py +46 -0
- package/templates/nexo_helper.py +45 -0
- package/templates/script-template.py +44 -0
- package/templates/skill-script-template.py +39 -0
- 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:
|
|
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}
|
|
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
|
-
#
|
|
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
|
-
"
|
|
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 —
|
|
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("║" + "
|
|
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
|
|