nexo-brain 5.3.26 → 5.3.28
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/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/hook_guardrails.py +44 -0
- package/src/server.py +3 -0
- package/src/tools_sessions.py +6 -1
- package/src/dashboard/static/favicon 2.svg +0 -32
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +0 -40
- package/src/dashboard/static/style 2.css +0 -2458
- package/src/dashboard/templates/adaptive 2.html +0 -118
- package/src/dashboard/templates/artifacts 2.html +0 -133
- package/src/dashboard/templates/backups 2.html +0 -136
- package/src/dashboard/templates/base 2.html +0 -417
- package/src/dashboard/templates/calendar 2.html +0 -591
- package/src/dashboard/templates/chat 2.html +0 -356
- package/src/dashboard/templates/claims 2.html +0 -259
- package/src/dashboard/templates/cortex 2.html +0 -321
- package/src/dashboard/templates/credentials 2.html +0 -128
- package/src/dashboard/templates/crons 2.html +0 -370
- package/src/dashboard/templates/dashboard 2.html +0 -494
- package/src/dashboard/templates/dreams 2.html +0 -252
- package/src/dashboard/templates/email 2.html +0 -160
- package/src/dashboard/templates/evolution 2.html +0 -189
- package/src/dashboard/templates/feed 2.html +0 -249
- package/src/dashboard/templates/followup_health 2.html +0 -170
- package/src/dashboard/templates/graph 2.html +0 -201
- package/src/dashboard/templates/guard 2.html +0 -259
- package/src/dashboard/templates/inbox 2.html +0 -251
- package/src/dashboard/templates/memory 2.html +0 -420
- package/src/dashboard/templates/operations 2.html +0 -608
- package/src/dashboard/templates/plugins 2.html +0 -185
- package/src/dashboard/templates/protocol 2.html +0 -199
- package/src/dashboard/templates/rules 2.html +0 -246
- package/src/dashboard/templates/sentiment 2.html +0 -247
- package/src/dashboard/templates/sessions 2.html +0 -218
- package/src/dashboard/templates/skills 2.html +0 -329
- package/src/dashboard/templates/somatic 2.html +0 -73
- package/src/dashboard/templates/triggers 2.html +0 -133
- package/src/dashboard/templates/trust 2.html +0 -360
- package/src/db/__init__ 2.py +0 -259
- package/src/db/_core 2.py +0 -437
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_episodic 2.py +0 -762
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_goal_profiles 2.py +0 -376
- package/src/db/_hot_context 2.py +0 -660
- package/src/db/_outcomes 2.py +0 -800
- package/src/db/_personal_scripts 2.py +0 -582
- package/src/db/_sessions 2.py +0 -330
- package/src/db/_tasks 2.py +0 -91
- package/src/db/_watchers 2.py +0 -173
- package/src/doctor/formatters 2.py +0 -52
- package/src/doctor/models 2.py +0 -69
- package/src/doctor/planes 2.py +0 -87
- package/src/doctor/providers/__init__ 2.py +0 -1
- package/src/doctor/providers/deep 2.py +0 -367
- package/src/evolution_cycle 2.py +0 -519
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -158
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/heartbeat-enforcement 2.py +0 -90
- package/src/hooks/heartbeat-posttool 2.sh +0 -18
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -152
- package/src/hooks/pre-compact 2.sh +0 -169
- package/src/hooks/protocol-guardrail 2.sh +0 -10
- package/src/hooks/protocol-pretool-guardrail 2.sh +0 -9
- package/src/hooks/session-stop 2.sh +0 -52
- package/src/kg_populate 2.py +0 -292
- package/src/maintenance 2.py +0 -53
- package/src/memory_backends 2.py +0 -71
- package/src/migrate_embeddings 2.py +0 -124
- package/src/nexo_sdk 2.py +0 -103
- package/src/observability 2.py +0 -199
- package/src/plugin_loader 2.py +0 -217
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -127
- package/src/plugins/claims_tools 2.py +0 -119
- package/src/plugins/cognitive_memory 2.py +0 -609
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -1155
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -560
- package/src/plugins/evolution 2.py +0 -167
- package/src/plugins/goal_engine 2.py +0 -142
- package/src/plugins/guard 2.py +0 -862
- package/src/plugins/impact 2.py +0 -29
- package/src/plugins/knowledge_graph_tools 2.py +0 -137
- package/src/plugins/media_memory_tools 2.py +0 -98
- package/src/plugins/memory_export 2.py +0 -196
- package/src/plugins/outcomes 2.py +0 -130
- package/src/plugins/personal_scripts 2.py +0 -117
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/protocol 2.py +0 -1449
- package/src/plugins/simple_api 2.py +0 -106
- package/src/plugins/skills 2.py +0 -341
- package/src/plugins/state_watchers 2.py +0 -79
- package/src/plugins/update 2.py +0 -986
- package/src/plugins/user_state_tools 2.py +0 -43
- package/src/plugins/workflow 2.py +0 -588
- package/src/protocol_settings 2.py +0 -59
- package/src/public_contribution 2.py +0 -466
- package/src/public_evolution_queue 2.py +0 -241
- package/src/requirements 2.txt +0 -14
- package/src/retroactive_learnings 2.py +0 -373
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/runtime_power 2.py +0 -874
- package/src/script_registry 2.py +0 -1559
- package/src/scripts/check-context 2.py +0 -272
- package/src/scripts/deep-sleep/apply_findings 2.py +0 -2327
- package/src/scripts/deep-sleep/collect 2.py +0 -928
- package/src/scripts/deep-sleep/extract 2.py +0 -330
- package/src/scripts/deep-sleep/extract-prompt 2.md +0 -285
- package/src/scripts/deep-sleep/synthesize 2.py +0 -312
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +0 -336
- package/src/scripts/nexo-agent-run 2.py +0 -75
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -300
- package/src/scripts/nexo-cognitive-decay 2.py +0 -257
- package/src/scripts/nexo-cortex-cycle 2.py +0 -293
- package/src/scripts/nexo-cron-wrapper 2.sh +0 -53
- package/src/scripts/nexo-daily-self-audit 2.py +0 -2161
- package/src/scripts/nexo-dashboard 2.sh +0 -29
- package/src/scripts/nexo-deep-sleep 2.sh +0 -86
- package/src/scripts/nexo-evolution-run 2.py +0 -1664
- package/src/scripts/nexo-followup-hygiene 2.py +0 -139
- package/src/scripts/nexo-hook-record 2.py +0 -42
- package/src/scripts/nexo-immune 2.py +0 -936
- package/src/scripts/nexo-impact-scorer 2.py +0 -117
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -401
- package/src/scripts/nexo-learning-validator 2.py +0 -266
- package/src/scripts/nexo-migrate 2.py +0 -260
- package/src/scripts/nexo-outcome-checker 2.py +0 -127
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -456
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -35
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -354
- package/src/scripts/nexo-reflection 2.py +0 -256
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-sleep 2.py +0 -631
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-sync-clients 2.py +0 -16
- package/src/scripts/nexo-synthesis 2.py +0 -475
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -306
- package/src/scripts/nexo-watchdog 2.sh +0 -1207
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/scripts/rehydrate_learnings_from_archive 2.py +0 -245
- package/src/server 2.py +0 -1296
- package/src/skills/run-nexo-audit-phase/guide 2.md +0 -43
- package/src/skills/run-nexo-audit-phase/skill 2.json +0 -59
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +0 -17
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +0 -276
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +0 -58
- package/src/skills/run-release-final-audit/guide 2.md +0 -16
- package/src/skills/run-release-final-audit/script 2.py +0 -259
- package/src/skills/run-release-final-audit/skill 2.json +0 -77
- package/src/skills/run-runtime-doctor/guide 2.md +0 -12
- package/src/skills/run-runtime-doctor/script 2.py +0 -21
- package/src/skills/run-runtime-doctor/skill 2.json +0 -25
- package/src/skills_runtime 2.py +0 -932
- package/src/state_watchers_runtime 2.py +0 -475
- package/src/storage_router 2.py +0 -32
- package/src/system_catalog 2.py +0 -786
- package/src/tools_coordination 2.py +0 -103
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_drive 2.py +0 -487
- package/src/tools_hot_context 2.py +0 -163
- package/src/tools_learnings 2.py +0 -612
- package/src/tools_menu 2.py +0 -229
- package/src/tools_reminders 2.py +0 -88
- package/src/tools_reminders_crud 2.py +0 -363
- package/src/tools_sessions 2.py +0 -1054
- package/src/tools_system_catalog 2.py +0 -19
- package/src/tools_task_history 2.py +0 -57
- package/src/tools_transcripts 2.py +0 -98
- package/src/transcript_utils 2.py +0 -412
- package/src/user_context 2.py +0 -46
- package/src/user_data_portability 2.py +0 -328
- package/src/user_state_model 2.py +0 -170
- package/templates/CLAUDE.md 2.template +0 -108
- package/templates/CODEX.AGENTS.md 2.template +0 -66
- package/templates/launchagents/README 2.md +0 -132
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +0 -39
- package/templates/launchagents/com.nexo.catchup 2.plist +0 -39
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +0 -40
- package/templates/launchagents/com.nexo.dashboard 2.plist +0 -43
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +0 -43
- package/templates/launchagents/com.nexo.evolution 2.plist +0 -44
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +0 -45
- package/templates/launchagents/com.nexo.immune 2.plist +0 -41
- package/templates/launchagents/com.nexo.postmortem 2.plist +0 -45
- package/templates/launchagents/com.nexo.self-audit 2.plist +0 -47
- package/templates/launchagents/com.nexo.synthesis 2.plist +0 -45
- package/templates/launchagents/com.nexo.watchdog 2.plist +0 -37
- package/templates/nexo_helper 2.py +0 -301
- package/templates/openclaw 2.json +0 -13
- package/templates/plugin-template 2.py +0 -40
- package/templates/script-template 2.py +0 -59
- package/templates/script-template 2.sh +0 -13
- package/templates/skill-script-template 2.py +0 -48
- package/templates/skill-template 2.md +0 -33
package/src/skills_runtime 2.py
DELETED
|
@@ -1,932 +0,0 @@
|
|
|
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 re
|
|
14
|
-
import subprocess
|
|
15
|
-
import sys
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
from db import (
|
|
19
|
-
approve_skill,
|
|
20
|
-
capture_outcome_pattern,
|
|
21
|
-
collect_skill_improvement_candidates,
|
|
22
|
-
collect_scriptable_skill_candidates,
|
|
23
|
-
create_skill,
|
|
24
|
-
get_featured_skills,
|
|
25
|
-
get_skill,
|
|
26
|
-
get_skill_outcome_evidence,
|
|
27
|
-
get_skill_execution_spec,
|
|
28
|
-
init_db,
|
|
29
|
-
list_outcome_pattern_candidates,
|
|
30
|
-
list_skill_outcome_reviews,
|
|
31
|
-
materialize_personal_skill_definition,
|
|
32
|
-
record_skill_usage,
|
|
33
|
-
render_command_template,
|
|
34
|
-
sync_skill_directories,
|
|
35
|
-
update_skill,
|
|
36
|
-
)
|
|
37
|
-
from script_registry import doctor_script
|
|
38
|
-
|
|
39
|
-
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
40
|
-
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
|
|
41
|
-
VALID_LEVELS = {"trace", "draft", "published", "stable", "archived"}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _parse_params(params) -> dict:
|
|
45
|
-
if isinstance(params, dict):
|
|
46
|
-
return params
|
|
47
|
-
if isinstance(params, str):
|
|
48
|
-
text = params.strip()
|
|
49
|
-
if not text:
|
|
50
|
-
return {}
|
|
51
|
-
return json.loads(text)
|
|
52
|
-
return {}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _ensure_ready():
|
|
56
|
-
init_db()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _resolve_mode(requested: str, skill: dict) -> str:
|
|
60
|
-
mode = (requested or "auto").strip().lower()
|
|
61
|
-
if mode in {"guide", "execute", "hybrid"}:
|
|
62
|
-
return mode
|
|
63
|
-
effective = str(skill.get("mode", "") or "").strip().lower()
|
|
64
|
-
if effective in {"guide", "execute", "hybrid"}:
|
|
65
|
-
return effective
|
|
66
|
-
if skill.get("file_path") and skill.get("content"):
|
|
67
|
-
return "hybrid"
|
|
68
|
-
if skill.get("file_path"):
|
|
69
|
-
return "execute"
|
|
70
|
-
return "guide"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _summarize_skill(skill: dict) -> str:
|
|
74
|
-
steps = []
|
|
75
|
-
gotchas = []
|
|
76
|
-
try:
|
|
77
|
-
steps = json.loads(skill.get("steps", "[]"))
|
|
78
|
-
except json.JSONDecodeError:
|
|
79
|
-
pass
|
|
80
|
-
try:
|
|
81
|
-
gotchas = json.loads(skill.get("gotchas", "[]"))
|
|
82
|
-
except json.JSONDecodeError:
|
|
83
|
-
pass
|
|
84
|
-
|
|
85
|
-
lines = [
|
|
86
|
-
f"[{skill['id']}] {skill['name']}",
|
|
87
|
-
skill.get("description", "") or "(no description)",
|
|
88
|
-
]
|
|
89
|
-
if steps:
|
|
90
|
-
lines.append("Steps:")
|
|
91
|
-
for index, step in enumerate(steps[:6], 1):
|
|
92
|
-
lines.append(f"{index}. {step}")
|
|
93
|
-
elif skill.get("content"):
|
|
94
|
-
lines.append(skill["content"][:800])
|
|
95
|
-
if gotchas:
|
|
96
|
-
lines.append("Gotchas:")
|
|
97
|
-
for gotcha in gotchas[:4]:
|
|
98
|
-
lines.append(f"- {gotcha}")
|
|
99
|
-
return "\n".join(lines).strip()
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _resolve_cli_command() -> list[str]:
|
|
103
|
-
installed = NEXO_HOME / "bin" / "nexo"
|
|
104
|
-
if installed.is_file():
|
|
105
|
-
return [str(installed)]
|
|
106
|
-
return [sys.executable, str(NEXO_CODE / "cli.py")]
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _run_skill_script(skill: dict, argv: list[str], timeout: int = 300) -> dict:
|
|
110
|
-
if not argv:
|
|
111
|
-
return {"returncode": 1, "stdout": "", "stderr": "No command to execute"}
|
|
112
|
-
|
|
113
|
-
env = {
|
|
114
|
-
**os.environ,
|
|
115
|
-
"NEXO_HOME": str(NEXO_HOME),
|
|
116
|
-
"NEXO_CODE": str(NEXO_CODE),
|
|
117
|
-
"NEXO_SKILL_ID": skill["id"],
|
|
118
|
-
"NEXO_SKILL_NAME": skill["name"],
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
cli_cmd = _resolve_cli_command()
|
|
122
|
-
cmd = [*cli_cmd, "scripts", "run", argv[0], *argv[1:]]
|
|
123
|
-
try:
|
|
124
|
-
result = subprocess.run(
|
|
125
|
-
cmd,
|
|
126
|
-
capture_output=True,
|
|
127
|
-
text=True,
|
|
128
|
-
timeout=timeout,
|
|
129
|
-
env=env,
|
|
130
|
-
)
|
|
131
|
-
return {
|
|
132
|
-
"returncode": result.returncode,
|
|
133
|
-
"stdout": result.stdout,
|
|
134
|
-
"stderr": result.stderr,
|
|
135
|
-
"command": cmd,
|
|
136
|
-
}
|
|
137
|
-
except subprocess.TimeoutExpired:
|
|
138
|
-
return {
|
|
139
|
-
"returncode": 124,
|
|
140
|
-
"stdout": "",
|
|
141
|
-
"stderr": f"Skill execution timed out after {timeout}s",
|
|
142
|
-
"command": cmd,
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def get_featured_skill_summaries(limit: int = 5) -> list[dict]:
|
|
147
|
-
_ensure_ready()
|
|
148
|
-
sync_skill_directories()
|
|
149
|
-
featured = []
|
|
150
|
-
for skill in get_featured_skills(limit=limit):
|
|
151
|
-
triggers = []
|
|
152
|
-
try:
|
|
153
|
-
triggers = json.loads(skill.get("trigger_patterns", "[]"))
|
|
154
|
-
except json.JSONDecodeError:
|
|
155
|
-
pass
|
|
156
|
-
featured.append(
|
|
157
|
-
{
|
|
158
|
-
"id": skill["id"],
|
|
159
|
-
"name": skill["name"],
|
|
160
|
-
"mode": skill.get("mode", "guide"),
|
|
161
|
-
"execution_level": skill.get("execution_level", "none"),
|
|
162
|
-
"source_kind": skill.get("source_kind", "personal"),
|
|
163
|
-
"trust_score": skill.get("trust_score", 0),
|
|
164
|
-
"trigger_patterns": triggers[:3],
|
|
165
|
-
"outcome_review": {
|
|
166
|
-
"has_evidence": bool((skill.get("_outcome_review") or {}).get("has_evidence")),
|
|
167
|
-
"recommended_action": (skill.get("_outcome_review") or {}).get("recommended_action", "observe"),
|
|
168
|
-
"success_rate": (skill.get("_outcome_review") or {}).get("success_rate"),
|
|
169
|
-
"resolved_outcomes": (skill.get("_outcome_review") or {}).get("resolved_outcomes", 0),
|
|
170
|
-
"ranking_weight": float(skill.get("_outcome_rank", 0.0)),
|
|
171
|
-
},
|
|
172
|
-
}
|
|
173
|
-
)
|
|
174
|
-
return featured
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _skill_json_list(skill: dict, key: str) -> list:
|
|
178
|
-
try:
|
|
179
|
-
value = json.loads(skill.get(key, "[]"))
|
|
180
|
-
except json.JSONDecodeError:
|
|
181
|
-
value = []
|
|
182
|
-
return value if isinstance(value, list) else []
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _outcome_skill_id(candidate: dict) -> str:
|
|
186
|
-
raw_parts = [
|
|
187
|
-
candidate.get("area", ""),
|
|
188
|
-
candidate.get("task_type", ""),
|
|
189
|
-
candidate.get("goal_profile_id", ""),
|
|
190
|
-
candidate.get("selected_choice", ""),
|
|
191
|
-
]
|
|
192
|
-
chunks = []
|
|
193
|
-
for part in raw_parts:
|
|
194
|
-
cleaned = re.sub(r"[^A-Z0-9]+", "-", str(part or "").upper()).strip("-")
|
|
195
|
-
if cleaned:
|
|
196
|
-
chunks.append(cleaned[:12])
|
|
197
|
-
suffix = "-".join(chunks[:4]) or "GENERAL"
|
|
198
|
-
return f"SK-OUTCOME-{suffix}"
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def _outcome_pattern_to_skill_payload(candidate: dict, learning_id: int) -> dict:
|
|
202
|
-
selected_choice = candidate.get("selected_choice", "")
|
|
203
|
-
context_label = candidate.get("context_label", "contexto general")
|
|
204
|
-
evidence = candidate.get("evidence") or []
|
|
205
|
-
evidence_summary = ", ".join(
|
|
206
|
-
f"eval#{item.get('evaluation_id')}/outcome#{item.get('outcome_id')}:{item.get('status')}"
|
|
207
|
-
for item in evidence[:5]
|
|
208
|
-
)
|
|
209
|
-
description = (
|
|
210
|
-
f"Draft skill candidate derived from {candidate.get('resolved_outcomes', 0)} resolved outcomes "
|
|
211
|
-
f"for '{selected_choice}' in {context_label}."
|
|
212
|
-
)
|
|
213
|
-
steps = [
|
|
214
|
-
f"Confirm that the current case matches {context_label}.",
|
|
215
|
-
f"Use '{selected_choice}' as the default starting strategy.",
|
|
216
|
-
"Check fresh constraints or evidence that could invalidate the historical pattern.",
|
|
217
|
-
"Link and evaluate the resulting outcome so the evidence base keeps improving.",
|
|
218
|
-
]
|
|
219
|
-
gotchas = [
|
|
220
|
-
"Do not apply this skill if current evidence contradicts the pattern.",
|
|
221
|
-
"Keep outcomes linked; a pattern without fresh feedback should not gain trust forever.",
|
|
222
|
-
]
|
|
223
|
-
content = "\n".join(
|
|
224
|
-
[
|
|
225
|
-
f"# Outcome pattern skill candidate — {selected_choice}",
|
|
226
|
-
"",
|
|
227
|
-
description,
|
|
228
|
-
"",
|
|
229
|
-
"## Evidence",
|
|
230
|
-
f"- Success rate: {candidate.get('success_rate', 0.0):.3f}",
|
|
231
|
-
f"- Resolved outcomes: {candidate.get('resolved_outcomes', 0)}",
|
|
232
|
-
f"- Context: {context_label}",
|
|
233
|
-
f"- Evidence refs: {evidence_summary or 'none'}",
|
|
234
|
-
"",
|
|
235
|
-
"## Steps",
|
|
236
|
-
*(f"{index}. {step}" for index, step in enumerate(steps, 1)),
|
|
237
|
-
"",
|
|
238
|
-
"## Gotchas",
|
|
239
|
-
*(f"- {gotcha}" for gotcha in gotchas),
|
|
240
|
-
"",
|
|
241
|
-
]
|
|
242
|
-
)
|
|
243
|
-
tags = [
|
|
244
|
-
"outcomes-derived",
|
|
245
|
-
str(candidate.get("area") or "").strip(),
|
|
246
|
-
str(candidate.get("task_type") or "").strip(),
|
|
247
|
-
str(candidate.get("goal_profile_id") or "").strip(),
|
|
248
|
-
]
|
|
249
|
-
trigger_patterns = [
|
|
250
|
-
str(candidate.get("selected_choice") or "").strip(),
|
|
251
|
-
str(candidate.get("area") or "").strip(),
|
|
252
|
-
str(candidate.get("task_type") or "").strip(),
|
|
253
|
-
]
|
|
254
|
-
return {
|
|
255
|
-
"id": _outcome_skill_id(candidate),
|
|
256
|
-
"name": f"Outcome Pattern: {selected_choice}",
|
|
257
|
-
"description": description,
|
|
258
|
-
"level": "draft",
|
|
259
|
-
"mode": "guide",
|
|
260
|
-
"tags": [item for item in tags if item],
|
|
261
|
-
"trigger_patterns": [item for item in trigger_patterns if item],
|
|
262
|
-
"linked_learnings": [int(learning_id)],
|
|
263
|
-
"steps": steps,
|
|
264
|
-
"gotchas": gotchas,
|
|
265
|
-
"content": content,
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def test_skill(skill_id: str, params=None, mode: str = "auto", context: str = "") -> dict:
|
|
270
|
-
result = apply_skill(skill_id, params=params, mode=mode, dry_run=True, context=context or "skill_test")
|
|
271
|
-
result["tested"] = True
|
|
272
|
-
result["test_kind"] = "dry_run"
|
|
273
|
-
return result
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def promote_skill(skill_id: str, target_level: str = "published", reason: str = "") -> dict:
|
|
277
|
-
_ensure_ready()
|
|
278
|
-
sync_skill_directories()
|
|
279
|
-
skill = get_skill(skill_id)
|
|
280
|
-
if not skill:
|
|
281
|
-
return {"ok": False, "error": f"Skill {skill_id} not found"}
|
|
282
|
-
clean_target = str(target_level or "published").strip().lower()
|
|
283
|
-
if clean_target not in VALID_LEVELS:
|
|
284
|
-
return {"ok": False, "error": f"Unsupported target_level: {target_level}"}
|
|
285
|
-
if clean_target == "archived":
|
|
286
|
-
return {"ok": False, "error": "Use retire_skill to archive skills explicitly"}
|
|
287
|
-
outcome_review = get_skill_outcome_evidence(skill_id)
|
|
288
|
-
if outcome_review.get("has_evidence"):
|
|
289
|
-
recommended = outcome_review.get("recommended_action")
|
|
290
|
-
if clean_target == "published" and recommended not in {"promote_published", "promote_stable"}:
|
|
291
|
-
return {
|
|
292
|
-
"ok": False,
|
|
293
|
-
"error": "Outcome evidence does not yet support promotion to published",
|
|
294
|
-
"outcome_review": outcome_review,
|
|
295
|
-
}
|
|
296
|
-
if clean_target == "stable" and recommended != "promote_stable":
|
|
297
|
-
return {
|
|
298
|
-
"ok": False,
|
|
299
|
-
"error": "Outcome evidence does not yet support promotion to stable",
|
|
300
|
-
"outcome_review": outcome_review,
|
|
301
|
-
}
|
|
302
|
-
updated = update_skill(skill_id, level=clean_target)
|
|
303
|
-
if "error" in updated:
|
|
304
|
-
return {"ok": False, "error": updated["error"]}
|
|
305
|
-
return {
|
|
306
|
-
"ok": True,
|
|
307
|
-
"skill_id": skill_id,
|
|
308
|
-
"previous_level": skill.get("level", ""),
|
|
309
|
-
"level": updated.get("level", clean_target),
|
|
310
|
-
"reason": str(reason or "").strip(),
|
|
311
|
-
"outcome_review": outcome_review,
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
def retire_skill(skill_id: str, replacement_id: str = "", reason: str = "") -> dict:
|
|
316
|
-
_ensure_ready()
|
|
317
|
-
sync_skill_directories()
|
|
318
|
-
skill = get_skill(skill_id)
|
|
319
|
-
if not skill:
|
|
320
|
-
return {"ok": False, "error": f"Skill {skill_id} not found"}
|
|
321
|
-
replacement = None
|
|
322
|
-
clean_replacement = str(replacement_id or "").strip()
|
|
323
|
-
if clean_replacement:
|
|
324
|
-
replacement = get_skill(clean_replacement)
|
|
325
|
-
if not replacement:
|
|
326
|
-
return {"ok": False, "error": f"Replacement skill {clean_replacement} not found"}
|
|
327
|
-
updated = update_skill(skill_id, level="archived")
|
|
328
|
-
if "error" in updated:
|
|
329
|
-
return {"ok": False, "error": updated["error"]}
|
|
330
|
-
return {
|
|
331
|
-
"ok": True,
|
|
332
|
-
"skill_id": skill_id,
|
|
333
|
-
"level": updated.get("level", "archived"),
|
|
334
|
-
"replacement_id": clean_replacement,
|
|
335
|
-
"reason": str(reason or "").strip(),
|
|
336
|
-
"outcome_review": get_skill_outcome_evidence(skill_id),
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def review_skill_outcomes(skill_id: str, auto_apply: bool = False) -> dict:
|
|
341
|
-
_ensure_ready()
|
|
342
|
-
sync_skill_directories()
|
|
343
|
-
skill = get_skill(skill_id)
|
|
344
|
-
if not skill:
|
|
345
|
-
return {"ok": False, "error": f"Skill {skill_id} not found"}
|
|
346
|
-
|
|
347
|
-
review = get_skill_outcome_evidence(skill_id)
|
|
348
|
-
if "error" in review:
|
|
349
|
-
return {"ok": False, "error": review["error"]}
|
|
350
|
-
|
|
351
|
-
result = {
|
|
352
|
-
"ok": True,
|
|
353
|
-
"skill_id": skill_id,
|
|
354
|
-
"level": skill.get("level", ""),
|
|
355
|
-
"review": review,
|
|
356
|
-
"auto_applied": False,
|
|
357
|
-
"applied_action": "",
|
|
358
|
-
}
|
|
359
|
-
if not auto_apply:
|
|
360
|
-
return result
|
|
361
|
-
|
|
362
|
-
action = review.get("recommended_action")
|
|
363
|
-
target_level = ""
|
|
364
|
-
if action == "promote_published":
|
|
365
|
-
target_level = "published"
|
|
366
|
-
elif action == "promote_stable":
|
|
367
|
-
target_level = "stable"
|
|
368
|
-
elif action == "retire":
|
|
369
|
-
target_level = "archived"
|
|
370
|
-
if not target_level:
|
|
371
|
-
return result
|
|
372
|
-
|
|
373
|
-
updated = update_skill(skill_id, level=target_level)
|
|
374
|
-
if "error" in updated:
|
|
375
|
-
return {"ok": False, "error": updated["error"], "review": review}
|
|
376
|
-
result["auto_applied"] = True
|
|
377
|
-
result["applied_action"] = action
|
|
378
|
-
result["level"] = updated.get("level", target_level)
|
|
379
|
-
result["skill"] = updated
|
|
380
|
-
return result
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
def compose_skills(
|
|
384
|
-
*,
|
|
385
|
-
new_skill_id: str,
|
|
386
|
-
name: str,
|
|
387
|
-
component_ids: list[str],
|
|
388
|
-
description: str = "",
|
|
389
|
-
level: str = "draft",
|
|
390
|
-
mode: str = "guide",
|
|
391
|
-
tags: list[str] | None = None,
|
|
392
|
-
trigger_patterns: list[str] | None = None,
|
|
393
|
-
) -> dict:
|
|
394
|
-
_ensure_ready()
|
|
395
|
-
sync_skill_directories()
|
|
396
|
-
if get_skill(new_skill_id):
|
|
397
|
-
return {"ok": False, "error": f"Skill {new_skill_id} already exists"}
|
|
398
|
-
components = []
|
|
399
|
-
for skill_id in component_ids:
|
|
400
|
-
skill = get_skill(skill_id)
|
|
401
|
-
if not skill:
|
|
402
|
-
return {"ok": False, "error": f"Component skill {skill_id} not found"}
|
|
403
|
-
components.append(skill)
|
|
404
|
-
if not components:
|
|
405
|
-
return {"ok": False, "error": "At least one component skill is required"}
|
|
406
|
-
|
|
407
|
-
merged_steps: list[str] = []
|
|
408
|
-
merged_gotchas: list[str] = []
|
|
409
|
-
merged_tags = set(tags or [])
|
|
410
|
-
merged_triggers = set(trigger_patterns or [])
|
|
411
|
-
linked_learnings = set()
|
|
412
|
-
source_sessions = set()
|
|
413
|
-
content_lines = [f"# {name}", "", description or "Composite skill built from existing NEXO skills.", "", "## Components"]
|
|
414
|
-
for skill in components:
|
|
415
|
-
content_lines.append(f"- {skill['id']}: {skill['name']}")
|
|
416
|
-
for step in _skill_json_list(skill, "steps"):
|
|
417
|
-
if step and step not in merged_steps:
|
|
418
|
-
merged_steps.append(step)
|
|
419
|
-
for gotcha in _skill_json_list(skill, "gotchas"):
|
|
420
|
-
if gotcha and gotcha not in merged_gotchas:
|
|
421
|
-
merged_gotchas.append(gotcha)
|
|
422
|
-
for trigger in _skill_json_list(skill, "trigger_patterns"):
|
|
423
|
-
if trigger:
|
|
424
|
-
merged_triggers.add(trigger)
|
|
425
|
-
for tag in _skill_json_list(skill, "tags"):
|
|
426
|
-
if tag:
|
|
427
|
-
merged_tags.add(tag)
|
|
428
|
-
for item in _skill_json_list(skill, "linked_learnings"):
|
|
429
|
-
if item:
|
|
430
|
-
linked_learnings.add(item)
|
|
431
|
-
for item in _skill_json_list(skill, "source_sessions"):
|
|
432
|
-
if item:
|
|
433
|
-
source_sessions.add(item)
|
|
434
|
-
|
|
435
|
-
if merged_steps:
|
|
436
|
-
content_lines.extend(["", "## Steps"])
|
|
437
|
-
for index, step in enumerate(merged_steps, 1):
|
|
438
|
-
content_lines.append(f"{index}. {step}")
|
|
439
|
-
if merged_gotchas:
|
|
440
|
-
content_lines.extend(["", "## Gotchas"])
|
|
441
|
-
for gotcha in merged_gotchas:
|
|
442
|
-
content_lines.append(f"- {gotcha}")
|
|
443
|
-
|
|
444
|
-
created = create_skill(
|
|
445
|
-
skill_id=new_skill_id,
|
|
446
|
-
name=name,
|
|
447
|
-
description=description or f"Composite skill built from {', '.join(component_ids)}",
|
|
448
|
-
level=level,
|
|
449
|
-
tags=sorted(merged_tags),
|
|
450
|
-
trigger_patterns=sorted(merged_triggers),
|
|
451
|
-
source_sessions=sorted(source_sessions),
|
|
452
|
-
linked_learnings=sorted(linked_learnings),
|
|
453
|
-
steps=merged_steps,
|
|
454
|
-
gotchas=merged_gotchas,
|
|
455
|
-
content="\n".join(content_lines).strip() + "\n",
|
|
456
|
-
mode=mode,
|
|
457
|
-
source_kind="personal",
|
|
458
|
-
)
|
|
459
|
-
if "error" in created:
|
|
460
|
-
return {"ok": False, "error": created["error"]}
|
|
461
|
-
return {
|
|
462
|
-
"ok": True,
|
|
463
|
-
"skill_id": new_skill_id,
|
|
464
|
-
"component_ids": component_ids,
|
|
465
|
-
"level": created.get("level", level),
|
|
466
|
-
"mode": created.get("mode", mode),
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
def apply_skill(skill_id: str, params=None, mode: str = "auto", dry_run: bool = False, context: str = "") -> dict:
|
|
471
|
-
_ensure_ready()
|
|
472
|
-
sync_skill_directories()
|
|
473
|
-
skill = get_skill(skill_id)
|
|
474
|
-
if not skill:
|
|
475
|
-
return {"ok": False, "error": f"Skill {skill_id} not found"}
|
|
476
|
-
|
|
477
|
-
effective_mode = _resolve_mode(mode, skill)
|
|
478
|
-
response = {
|
|
479
|
-
"ok": True,
|
|
480
|
-
"skill_id": skill["id"],
|
|
481
|
-
"skill_name": skill["name"],
|
|
482
|
-
"requested_mode": mode,
|
|
483
|
-
"resolved_mode": effective_mode,
|
|
484
|
-
"approval_state": {
|
|
485
|
-
"approval_required": bool(skill.get("approval_required", 0)),
|
|
486
|
-
"approved_at": skill.get("approved_at", ""),
|
|
487
|
-
"execution_level": skill.get("execution_level", "none"),
|
|
488
|
-
},
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if effective_mode in {"guide", "hybrid"}:
|
|
492
|
-
response["guide_summary"] = _summarize_skill(skill)
|
|
493
|
-
|
|
494
|
-
if effective_mode in {"execute", "hybrid"}:
|
|
495
|
-
exec_spec = get_skill_execution_spec(skill_id)
|
|
496
|
-
if "error" in exec_spec:
|
|
497
|
-
response["ok"] = False
|
|
498
|
-
response["error"] = exec_spec["error"]
|
|
499
|
-
return response
|
|
500
|
-
|
|
501
|
-
if not skill.get("file_path"):
|
|
502
|
-
response["ok"] = False
|
|
503
|
-
response["error"] = f"Skill {skill_id} has no executable script"
|
|
504
|
-
return response
|
|
505
|
-
|
|
506
|
-
if exec_spec["execution_level"] in {"read-only", "local", "remote"} and not skill.get("approved_at"):
|
|
507
|
-
skill = approve_skill(skill_id, execution_level=exec_spec["execution_level"], approved_by="system:auto")
|
|
508
|
-
response["approval_state"] = {
|
|
509
|
-
"approval_required": bool(skill.get("approval_required", 0)),
|
|
510
|
-
"approved_at": skill.get("approved_at", ""),
|
|
511
|
-
"execution_level": skill.get("execution_level", exec_spec["execution_level"]),
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
doctor = doctor_script(skill["file_path"])
|
|
515
|
-
response["script_doctor"] = doctor
|
|
516
|
-
if doctor["status"] == "fail":
|
|
517
|
-
response["ok"] = False
|
|
518
|
-
response["error"] = "Skill script failed validation"
|
|
519
|
-
return response
|
|
520
|
-
|
|
521
|
-
rendered = render_command_template(skill, _parse_params(params))
|
|
522
|
-
if not rendered.get("ok"):
|
|
523
|
-
response["ok"] = False
|
|
524
|
-
response["error"] = "Invalid skill parameters"
|
|
525
|
-
response["param_errors"] = rendered.get("errors", [])
|
|
526
|
-
return response
|
|
527
|
-
|
|
528
|
-
argv = rendered["argv"] or [skill["file_path"]]
|
|
529
|
-
response["resolved_params"] = rendered["params"]
|
|
530
|
-
response["script_command"] = argv
|
|
531
|
-
if dry_run:
|
|
532
|
-
response["dry_run"] = True
|
|
533
|
-
return response
|
|
534
|
-
|
|
535
|
-
execution = _run_skill_script(skill, argv)
|
|
536
|
-
response["execution_result"] = execution
|
|
537
|
-
success = execution["returncode"] == 0
|
|
538
|
-
record = record_skill_usage(
|
|
539
|
-
skill_id=skill_id,
|
|
540
|
-
success=success,
|
|
541
|
-
context=context or skill["name"],
|
|
542
|
-
notes=(execution["stderr"] or execution["stdout"])[:500],
|
|
543
|
-
)
|
|
544
|
-
response["usage_recorded"] = {
|
|
545
|
-
"success": success,
|
|
546
|
-
"trust_score": record.get("trust_score"),
|
|
547
|
-
"level": record.get("level"),
|
|
548
|
-
"promotion": record.get("_promotion"),
|
|
549
|
-
}
|
|
550
|
-
if not success:
|
|
551
|
-
response["ok"] = False
|
|
552
|
-
response["error"] = f"Skill execution failed with exit {execution['returncode']}"
|
|
553
|
-
|
|
554
|
-
return response
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
def sync_skills() -> dict:
|
|
558
|
-
_ensure_ready()
|
|
559
|
-
return sync_skill_directories()
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
def approve_skill_execution(skill_id: str, execution_level: str = "", approved_by: str = "") -> dict:
|
|
563
|
-
_ensure_ready()
|
|
564
|
-
return approve_skill(skill_id, execution_level=execution_level, approved_by=approved_by)
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
def list_evolution_candidates() -> dict:
|
|
568
|
-
_ensure_ready()
|
|
569
|
-
sync_skill_directories()
|
|
570
|
-
outcome_patterns = [
|
|
571
|
-
candidate
|
|
572
|
-
for candidate in list_outcome_pattern_candidates(limit=20)
|
|
573
|
-
if candidate.get("candidate_type") == "reinforce_strategy" and candidate.get("suggested_skill_candidate")
|
|
574
|
-
]
|
|
575
|
-
return {
|
|
576
|
-
"scriptable": collect_scriptable_skill_candidates(),
|
|
577
|
-
"improvements": collect_skill_improvement_candidates(),
|
|
578
|
-
"outcome_patterns": outcome_patterns,
|
|
579
|
-
"outcome_lifecycle": list_skill_outcome_reviews(limit=20, actionable_only=True),
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
def materialize_outcome_pattern_skill(pattern_key: str) -> dict:
|
|
584
|
-
_ensure_ready()
|
|
585
|
-
sync_skill_directories()
|
|
586
|
-
candidates = list_outcome_pattern_candidates(limit=200)
|
|
587
|
-
candidate = next((item for item in candidates if item.get("pattern_key") == pattern_key), None)
|
|
588
|
-
if not candidate:
|
|
589
|
-
return {"ok": False, "error": "Outcome pattern candidate not found"}
|
|
590
|
-
if candidate.get("candidate_type") != "reinforce_strategy":
|
|
591
|
-
return {"ok": False, "error": "Only reinforce_strategy patterns can seed a skill"}
|
|
592
|
-
if not candidate.get("suggested_skill_candidate"):
|
|
593
|
-
return {"ok": False, "error": "Pattern is not strong enough yet to seed a skill draft"}
|
|
594
|
-
|
|
595
|
-
learning_result = capture_outcome_pattern(pattern_key, target="learning", category="outcomes")
|
|
596
|
-
if "error" in learning_result:
|
|
597
|
-
return {"ok": False, "error": learning_result["error"]}
|
|
598
|
-
learning = learning_result.get("learning") or {}
|
|
599
|
-
skill_id = _outcome_skill_id(candidate)
|
|
600
|
-
existing = get_skill(skill_id)
|
|
601
|
-
if existing:
|
|
602
|
-
return {
|
|
603
|
-
"ok": True,
|
|
604
|
-
"created": False,
|
|
605
|
-
"skill": existing,
|
|
606
|
-
"candidate": candidate,
|
|
607
|
-
"learning": learning,
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
payload = _outcome_pattern_to_skill_payload(candidate, int(learning.get("id", 0) or 0))
|
|
611
|
-
created = materialize_personal_skill_definition(payload)
|
|
612
|
-
if "error" in created:
|
|
613
|
-
return {"ok": False, "error": created["error"]}
|
|
614
|
-
return {
|
|
615
|
-
"ok": True,
|
|
616
|
-
"created": True,
|
|
617
|
-
"skill": created,
|
|
618
|
-
"candidate": candidate,
|
|
619
|
-
"learning": learning,
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
def auto_promote_outcome_patterns_to_skills(
|
|
624
|
-
*,
|
|
625
|
-
min_success_rate: float = 0.8,
|
|
626
|
-
max_promotions: int = 3,
|
|
627
|
-
) -> dict:
|
|
628
|
-
"""Promote mature outcome patterns to draft skills without manual approval.
|
|
629
|
-
|
|
630
|
-
Closes Fase 2 item 2 of NEXO-AUDIT-2026-04-11. Until this function ran
|
|
631
|
-
automatically, every outcome pattern that crossed the suggested-skill
|
|
632
|
-
threshold sat in the candidates table waiting for an explicit
|
|
633
|
-
nexo_skill_seed_from_outcome_pattern call. The materialization helper
|
|
634
|
-
already existed (see materialize_outcome_pattern_skill above), but no
|
|
635
|
-
process invoked it on a schedule.
|
|
636
|
-
|
|
637
|
-
This wrapper:
|
|
638
|
-
- Lists current outcome pattern candidates (top 20).
|
|
639
|
-
- Filters to reinforce_strategy candidates that the analyzer already
|
|
640
|
-
flagged as suggested_skill_candidate=True (resolved >= 4) AND whose
|
|
641
|
-
success_rate is at or above min_success_rate (default 0.8).
|
|
642
|
-
- Calls materialize_outcome_pattern_skill() per qualifying candidate,
|
|
643
|
-
capped at max_promotions per invocation so a sudden flood does not
|
|
644
|
-
materialize dozens of skills in one cycle.
|
|
645
|
-
- materialize_outcome_pattern_skill() is itself idempotent: if the
|
|
646
|
-
target skill id already exists it returns created=False without
|
|
647
|
-
re-creating, so this function is safe to run repeatedly.
|
|
648
|
-
|
|
649
|
-
Returns a stats dict:
|
|
650
|
-
{
|
|
651
|
-
"promoted": [list of {pattern_key, skill_id, created}],
|
|
652
|
-
"skipped": [list of {pattern_key, reason}],
|
|
653
|
-
"errors": [list of {pattern_key, error}],
|
|
654
|
-
"scanned": int, # number of candidates inspected
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
Best-effort: never raises. A single failing pattern logs an error entry
|
|
658
|
-
but lets the loop continue, so one bad row never blocks the queue.
|
|
659
|
-
"""
|
|
660
|
-
_ensure_ready()
|
|
661
|
-
sync_skill_directories()
|
|
662
|
-
|
|
663
|
-
promoted: list[dict] = []
|
|
664
|
-
skipped: list[dict] = []
|
|
665
|
-
errors: list[dict] = []
|
|
666
|
-
|
|
667
|
-
try:
|
|
668
|
-
candidates = list_outcome_pattern_candidates(limit=20)
|
|
669
|
-
except Exception as e:
|
|
670
|
-
return {
|
|
671
|
-
"promoted": [],
|
|
672
|
-
"skipped": [],
|
|
673
|
-
"errors": [{"pattern_key": "*", "error": f"list_outcome_pattern_candidates raised: {e}"}],
|
|
674
|
-
"scanned": 0,
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
scanned = 0
|
|
678
|
-
promote_budget = max(0, int(max_promotions))
|
|
679
|
-
for candidate in candidates:
|
|
680
|
-
scanned += 1
|
|
681
|
-
pattern_key = (candidate.get("pattern_key") or "").strip()
|
|
682
|
-
if not pattern_key:
|
|
683
|
-
skipped.append({"pattern_key": "", "reason": "missing pattern_key"})
|
|
684
|
-
continue
|
|
685
|
-
if candidate.get("candidate_type") != "reinforce_strategy":
|
|
686
|
-
skipped.append({"pattern_key": pattern_key, "reason": "not reinforce_strategy"})
|
|
687
|
-
continue
|
|
688
|
-
if not candidate.get("suggested_skill_candidate"):
|
|
689
|
-
skipped.append({"pattern_key": pattern_key, "reason": "below suggested_skill_candidate threshold"})
|
|
690
|
-
continue
|
|
691
|
-
success_rate = float(candidate.get("success_rate") or 0.0)
|
|
692
|
-
if success_rate < float(min_success_rate):
|
|
693
|
-
skipped.append({
|
|
694
|
-
"pattern_key": pattern_key,
|
|
695
|
-
"reason": f"success_rate {success_rate:.3f} < {min_success_rate:.3f}",
|
|
696
|
-
})
|
|
697
|
-
continue
|
|
698
|
-
if promote_budget <= 0:
|
|
699
|
-
skipped.append({"pattern_key": pattern_key, "reason": "promotion budget exhausted"})
|
|
700
|
-
continue
|
|
701
|
-
|
|
702
|
-
try:
|
|
703
|
-
result = materialize_outcome_pattern_skill(pattern_key)
|
|
704
|
-
except Exception as e:
|
|
705
|
-
errors.append({"pattern_key": pattern_key, "error": str(e)})
|
|
706
|
-
continue
|
|
707
|
-
|
|
708
|
-
if not result.get("ok"):
|
|
709
|
-
errors.append({
|
|
710
|
-
"pattern_key": pattern_key,
|
|
711
|
-
"error": result.get("error", "materialize_outcome_pattern_skill failed"),
|
|
712
|
-
})
|
|
713
|
-
continue
|
|
714
|
-
|
|
715
|
-
skill = result.get("skill") or {}
|
|
716
|
-
promoted.append({
|
|
717
|
-
"pattern_key": pattern_key,
|
|
718
|
-
"skill_id": skill.get("id") or skill.get("skill_id"),
|
|
719
|
-
"created": bool(result.get("created")),
|
|
720
|
-
"success_rate": success_rate,
|
|
721
|
-
"resolved_outcomes": int(candidate.get("resolved_outcomes") or 0),
|
|
722
|
-
})
|
|
723
|
-
# Only newly-created skills consume the budget. Idempotent re-use of
|
|
724
|
-
# an existing skill costs nothing because it didn't materialize a new
|
|
725
|
-
# one — letting other candidates through.
|
|
726
|
-
if result.get("created"):
|
|
727
|
-
promote_budget -= 1
|
|
728
|
-
|
|
729
|
-
return {
|
|
730
|
-
"promoted": promoted,
|
|
731
|
-
"skipped": skipped,
|
|
732
|
-
"errors": errors,
|
|
733
|
-
"scanned": scanned,
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
def detect_skill_coactivation_candidates(
|
|
738
|
-
*,
|
|
739
|
-
min_co_occurrence: int = 3,
|
|
740
|
-
min_success_rate: float = 0.6,
|
|
741
|
-
limit: int = 20,
|
|
742
|
-
) -> list[dict]:
|
|
743
|
-
"""Detect pairs of skills that fire together and could be composed.
|
|
744
|
-
|
|
745
|
-
Closes Fase 5 item 5 of NEXO-AUDIT-2026-04-11. NEXO already has
|
|
746
|
-
compose_skills (manual composer), auto_promote_outcome_patterns_to_skills
|
|
747
|
-
(Fase 2 item 2), and auto_promote_skill_evolution (guide → executable
|
|
748
|
-
draft). What it lacked is the Voyager-style observation pass: when
|
|
749
|
-
the same two skills are used inside the same session multiple times,
|
|
750
|
-
a composite skill probably wants to exist.
|
|
751
|
-
|
|
752
|
-
This function reads skill_usage rows, groups by session_id to get
|
|
753
|
-
the set of skills per session, builds a co-occurrence count over
|
|
754
|
-
all unordered pairs, and returns candidates whose count is at or
|
|
755
|
-
above min_co_occurrence and whose joint success rate is at or
|
|
756
|
-
above min_success_rate.
|
|
757
|
-
|
|
758
|
-
Args:
|
|
759
|
-
min_co_occurrence: minimum number of sessions both skills must
|
|
760
|
-
have co-occurred in. Default 3 — same threshold the
|
|
761
|
-
outcome_pattern detector uses.
|
|
762
|
-
min_success_rate: minimum joint success rate (success rows /
|
|
763
|
-
total rows for the pair). Default 0.6.
|
|
764
|
-
limit: max candidates to return. Default 20.
|
|
765
|
-
|
|
766
|
-
Returns a list (newest co-occurrence first):
|
|
767
|
-
[
|
|
768
|
-
{
|
|
769
|
-
"skill_a": str, "skill_b": str,
|
|
770
|
-
"co_occurrence": int,
|
|
771
|
-
"joint_success_rate": float,
|
|
772
|
-
"sessions": [session_id, ...],
|
|
773
|
-
"suggested_skill_id": str, # canonical id for the composite
|
|
774
|
-
},
|
|
775
|
-
...
|
|
776
|
-
]
|
|
777
|
-
|
|
778
|
-
Pure DB read, never raises. Empty list when skill_usage table is
|
|
779
|
-
missing or no candidates qualify.
|
|
780
|
-
"""
|
|
781
|
-
_ensure_ready()
|
|
782
|
-
try:
|
|
783
|
-
from db import get_db
|
|
784
|
-
conn = get_db()
|
|
785
|
-
# Sanity check the table exists.
|
|
786
|
-
conn.execute("SELECT 1 FROM skill_usage LIMIT 1").fetchone()
|
|
787
|
-
except Exception:
|
|
788
|
-
return []
|
|
789
|
-
|
|
790
|
-
try:
|
|
791
|
-
rows = conn.execute(
|
|
792
|
-
"SELECT skill_id, session_id, success FROM skill_usage "
|
|
793
|
-
"WHERE session_id IS NOT NULL AND session_id != '' "
|
|
794
|
-
"ORDER BY created_at DESC LIMIT 5000"
|
|
795
|
-
).fetchall()
|
|
796
|
-
except Exception:
|
|
797
|
-
return []
|
|
798
|
-
|
|
799
|
-
by_session: dict[str, list[tuple[str, int]]] = {}
|
|
800
|
-
for row in rows:
|
|
801
|
-
sid = (row["session_id"] or "").strip() if hasattr(row, "keys") else (row[1] or "").strip()
|
|
802
|
-
if not sid:
|
|
803
|
-
continue
|
|
804
|
-
skill_id = row["skill_id"] if hasattr(row, "keys") else row[0]
|
|
805
|
-
success = row["success"] if hasattr(row, "keys") else row[2]
|
|
806
|
-
by_session.setdefault(sid, []).append((skill_id, int(success or 0)))
|
|
807
|
-
|
|
808
|
-
pair_stats: dict[tuple[str, str], dict] = {}
|
|
809
|
-
for sid, usages in by_session.items():
|
|
810
|
-
# Distinct skills with their best (any) success in this session.
|
|
811
|
-
seen_in_session: dict[str, int] = {}
|
|
812
|
-
for skill_id, success in usages:
|
|
813
|
-
seen_in_session[skill_id] = max(seen_in_session.get(skill_id, 0), success)
|
|
814
|
-
skills_in_session = sorted(seen_in_session.keys())
|
|
815
|
-
if len(skills_in_session) < 2:
|
|
816
|
-
continue
|
|
817
|
-
for i in range(len(skills_in_session)):
|
|
818
|
-
for j in range(i + 1, len(skills_in_session)):
|
|
819
|
-
pair = (skills_in_session[i], skills_in_session[j])
|
|
820
|
-
stats = pair_stats.setdefault(
|
|
821
|
-
pair,
|
|
822
|
-
{"co_occurrence": 0, "joint_success": 0, "sessions": []},
|
|
823
|
-
)
|
|
824
|
-
stats["co_occurrence"] += 1
|
|
825
|
-
# Joint success: both skills succeeded in this session.
|
|
826
|
-
if seen_in_session[pair[0]] and seen_in_session[pair[1]]:
|
|
827
|
-
stats["joint_success"] += 1
|
|
828
|
-
stats["sessions"].append(sid)
|
|
829
|
-
|
|
830
|
-
candidates: list[dict] = []
|
|
831
|
-
for (skill_a, skill_b), stats in pair_stats.items():
|
|
832
|
-
co = stats["co_occurrence"]
|
|
833
|
-
if co < int(min_co_occurrence):
|
|
834
|
-
continue
|
|
835
|
-
rate = stats["joint_success"] / co if co > 0 else 0.0
|
|
836
|
-
if rate < float(min_success_rate):
|
|
837
|
-
continue
|
|
838
|
-
# Deterministic suggested id derived from the pair so re-running
|
|
839
|
-
# the detector points at the same composite skill.
|
|
840
|
-
suggested_id = "SK-COMPOSE-" + "+".join(sorted([skill_a, skill_b]))
|
|
841
|
-
candidates.append({
|
|
842
|
-
"skill_a": skill_a,
|
|
843
|
-
"skill_b": skill_b,
|
|
844
|
-
"co_occurrence": co,
|
|
845
|
-
"joint_success_rate": round(rate, 3),
|
|
846
|
-
"sessions": stats["sessions"][:5],
|
|
847
|
-
"suggested_skill_id": suggested_id,
|
|
848
|
-
})
|
|
849
|
-
|
|
850
|
-
candidates.sort(key=lambda c: (-c["co_occurrence"], -c["joint_success_rate"]))
|
|
851
|
-
return candidates[: max(1, int(limit))]
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
def auto_promote_skill_evolution(approved_by: str = "system:auto") -> dict:
|
|
855
|
-
"""Convert mature guide skills into executable drafts without manual approval."""
|
|
856
|
-
_ensure_ready()
|
|
857
|
-
sync_skill_directories()
|
|
858
|
-
promoted = []
|
|
859
|
-
skipped = []
|
|
860
|
-
for candidate in collect_scriptable_skill_candidates():
|
|
861
|
-
skill = get_skill(candidate["id"])
|
|
862
|
-
if not skill or skill.get("file_path"):
|
|
863
|
-
continue
|
|
864
|
-
|
|
865
|
-
steps = candidate.get("steps") or []
|
|
866
|
-
gotchas = candidate.get("gotchas") or []
|
|
867
|
-
description = candidate.get("description", "") or "Automated skill generated from repeated successful usage."
|
|
868
|
-
lines = [
|
|
869
|
-
"#!/usr/bin/env python3",
|
|
870
|
-
'"""Auto-generated executable skill draft."""',
|
|
871
|
-
"import json",
|
|
872
|
-
"import sys",
|
|
873
|
-
"",
|
|
874
|
-
"def main() -> int:",
|
|
875
|
-
" payload = {",
|
|
876
|
-
f" 'skill_id': {json.dumps(candidate['id'])},",
|
|
877
|
-
f" 'skill_name': {json.dumps(candidate['name'])},",
|
|
878
|
-
f" 'description': {json.dumps(description)},",
|
|
879
|
-
f" 'steps': {json.dumps(steps, ensure_ascii=False)},",
|
|
880
|
-
f" 'gotchas': {json.dumps(gotchas, ensure_ascii=False)},",
|
|
881
|
-
" 'argv': sys.argv[1:],",
|
|
882
|
-
" }",
|
|
883
|
-
" print(json.dumps(payload, ensure_ascii=False))",
|
|
884
|
-
" return 0",
|
|
885
|
-
"",
|
|
886
|
-
'if __name__ == "__main__":',
|
|
887
|
-
" raise SystemExit(main())",
|
|
888
|
-
"",
|
|
889
|
-
]
|
|
890
|
-
update = update_skill(
|
|
891
|
-
candidate["id"],
|
|
892
|
-
mode=candidate.get("suggested_mode", "hybrid"),
|
|
893
|
-
execution_level=candidate.get("suggested_execution_level", "read-only"),
|
|
894
|
-
approval_required=0,
|
|
895
|
-
approved_by=approved_by,
|
|
896
|
-
)
|
|
897
|
-
if "error" in update:
|
|
898
|
-
skipped.append({"id": candidate["id"], "reason": update["error"]})
|
|
899
|
-
continue
|
|
900
|
-
|
|
901
|
-
materialized = materialize_personal_skill_definition(
|
|
902
|
-
{
|
|
903
|
-
"id": candidate["id"],
|
|
904
|
-
"name": candidate["name"],
|
|
905
|
-
"description": description,
|
|
906
|
-
"level": skill.get("level", "published"),
|
|
907
|
-
"mode": candidate.get("suggested_mode", "hybrid"),
|
|
908
|
-
"execution_level": candidate.get("suggested_execution_level", "read-only"),
|
|
909
|
-
"approved_by": approved_by,
|
|
910
|
-
"tags": json.loads(skill.get("tags", "[]")) if skill.get("tags") else [],
|
|
911
|
-
"trigger_patterns": candidate.get("trigger_patterns", []),
|
|
912
|
-
"source_sessions": candidate.get("source_sessions", []),
|
|
913
|
-
"steps": steps,
|
|
914
|
-
"gotchas": gotchas,
|
|
915
|
-
"content": skill.get("content", ""),
|
|
916
|
-
"command_template": {"argv": ["{{file_path}}"]},
|
|
917
|
-
"executable_entry": "script.py",
|
|
918
|
-
"script_body": "\n".join(lines),
|
|
919
|
-
}
|
|
920
|
-
)
|
|
921
|
-
if "error" in materialized:
|
|
922
|
-
skipped.append({"id": candidate["id"], "reason": materialized["error"]})
|
|
923
|
-
continue
|
|
924
|
-
|
|
925
|
-
promoted.append(
|
|
926
|
-
{
|
|
927
|
-
"id": candidate["id"],
|
|
928
|
-
"mode": candidate.get("suggested_mode", "hybrid"),
|
|
929
|
-
"execution_level": candidate.get("suggested_execution_level", "read-only"),
|
|
930
|
-
}
|
|
931
|
-
)
|
|
932
|
-
return {"promoted": promoted, "skipped": skipped}
|