nexo-brain 5.3.13 → 5.3.15
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/bin/nexo-brain.js +52 -1
- package/package.json +1 -1
- package/src/crons/sync.py +18 -4
- package/src/dashboard/static/favicon 2.svg +32 -0
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +40 -0
- package/src/dashboard/static/style 2.css +2458 -0
- package/src/dashboard/templates/adaptive 2.html +118 -0
- package/src/dashboard/templates/artifacts 2.html +133 -0
- package/src/dashboard/templates/backups 2.html +136 -0
- package/src/dashboard/templates/base 2.html +417 -0
- package/src/dashboard/templates/calendar 2.html +591 -0
- package/src/dashboard/templates/chat 2.html +356 -0
- package/src/dashboard/templates/claims 2.html +259 -0
- package/src/dashboard/templates/cortex 2.html +321 -0
- package/src/dashboard/templates/credentials 2.html +128 -0
- package/src/dashboard/templates/crons 2.html +370 -0
- package/src/dashboard/templates/dashboard 2.html +494 -0
- package/src/dashboard/templates/dreams 2.html +252 -0
- package/src/dashboard/templates/email 2.html +160 -0
- package/src/dashboard/templates/evolution 2.html +189 -0
- package/src/dashboard/templates/feed 2.html +249 -0
- package/src/dashboard/templates/followup_health 2.html +170 -0
- package/src/dashboard/templates/graph 2.html +201 -0
- package/src/dashboard/templates/guard 2.html +259 -0
- package/src/dashboard/templates/inbox 2.html +251 -0
- package/src/dashboard/templates/memory 2.html +420 -0
- package/src/dashboard/templates/operations 2.html +608 -0
- package/src/dashboard/templates/plugins 2.html +185 -0
- package/src/dashboard/templates/protocol 2.html +199 -0
- package/src/dashboard/templates/rules 2.html +246 -0
- package/src/dashboard/templates/sentiment 2.html +247 -0
- package/src/dashboard/templates/sessions 2.html +218 -0
- package/src/dashboard/templates/skills 2.html +329 -0
- package/src/dashboard/templates/somatic 2.html +73 -0
- package/src/dashboard/templates/triggers 2.html +133 -0
- package/src/dashboard/templates/trust 2.html +360 -0
- package/src/db/__init__ 2.py +259 -0
- package/src/db/_core 2.py +437 -0
- package/src/db/_credentials 2.py +124 -0
- package/src/db/_entities.py +1 -1
- package/src/db/_episodic 2.py +762 -0
- package/src/db/_evolution 2.py +54 -0
- package/src/db/_fts 2.py +406 -0
- package/src/db/_goal_profiles 2.py +376 -0
- package/src/db/_hot_context 2.py +660 -0
- package/src/db/_outcomes 2.py +800 -0
- package/src/db/_personal_scripts 2.py +582 -0
- package/src/db/_sessions 2.py +330 -0
- package/src/db/_tasks 2.py +91 -0
- package/src/db/_watchers 2.py +173 -0
- package/src/doctor/formatters 2.py +52 -0
- package/src/doctor/models 2.py +69 -0
- package/src/doctor/planes 2.py +87 -0
- package/src/doctor/providers/__init__ 2.py +1 -0
- package/src/doctor/providers/deep 2.py +367 -0
- package/src/evolution_cycle 2.py +519 -0
- package/src/hooks/auto_capture 2.py +208 -0
- package/src/hooks/caffeinate-guard 2.sh +8 -0
- package/src/hooks/capture-session 2.sh +21 -0
- package/src/hooks/capture-tool-logs 2.sh +158 -0
- package/src/hooks/daily-briefing-check 2.sh +33 -0
- package/src/hooks/heartbeat-enforcement 2.py +90 -0
- package/src/hooks/heartbeat-posttool 2.sh +18 -0
- package/src/hooks/inbox-hook 2.sh +76 -0
- package/src/hooks/post-compact 2.sh +152 -0
- package/src/hooks/pre-compact 2.sh +169 -0
- package/src/hooks/protocol-guardrail 2.sh +10 -0
- package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
- package/src/hooks/session-stop 2.sh +52 -0
- package/src/kg_populate 2.py +292 -0
- package/src/maintenance 2.py +53 -0
- package/src/memory_backends 2.py +71 -0
- package/src/migrate_embeddings 2.py +124 -0
- package/src/nexo_sdk 2.py +103 -0
- package/src/observability 2.py +199 -0
- package/src/plugin_loader 2.py +217 -0
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/agents.py +10 -3
- package/src/plugins/artifact_registry 2.py +450 -0
- package/src/plugins/backup 2.py +127 -0
- package/src/plugins/claims_tools 2.py +119 -0
- package/src/plugins/cognitive_memory 2.py +609 -0
- package/src/plugins/core_rules 2.py +252 -0
- package/src/plugins/cortex 2.py +1155 -0
- package/src/plugins/entities 2.py +67 -0
- package/src/plugins/episodic_memory 2.py +560 -0
- package/src/plugins/evolution 2.py +167 -0
- package/src/plugins/goal_engine 2.py +142 -0
- package/src/plugins/guard 2.py +862 -0
- package/src/plugins/impact 2.py +29 -0
- package/src/plugins/knowledge_graph_tools 2.py +137 -0
- package/src/plugins/media_memory_tools 2.py +98 -0
- package/src/plugins/memory_export 2.py +196 -0
- package/src/plugins/outcomes 2.py +130 -0
- package/src/plugins/personal_scripts 2.py +117 -0
- package/src/plugins/preferences 2.py +47 -0
- package/src/plugins/protocol 2.py +1449 -0
- package/src/plugins/schedule.py +2 -1
- package/src/plugins/simple_api 2.py +106 -0
- package/src/plugins/skills 2.py +341 -0
- package/src/plugins/state_watchers 2.py +79 -0
- package/src/plugins/update 2.py +986 -0
- package/src/plugins/user_state_tools 2.py +43 -0
- package/src/plugins/workflow 2.py +588 -0
- package/src/protocol_settings 2.py +59 -0
- package/src/public_contribution 2.py +466 -0
- package/src/public_evolution_queue 2.py +241 -0
- package/src/requirements 2.txt +14 -0
- package/src/requirements.txt +1 -1
- package/src/retroactive_learnings 2.py +373 -0
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +331 -0
- package/src/rules/migrate 2.py +207 -0
- package/src/runtime_power 2.py +874 -0
- package/src/runtime_power.py +18 -1
- package/src/script_registry 2.py +1559 -0
- package/src/scripts/check-context 2.py +272 -0
- package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
- package/src/scripts/deep-sleep/collect 2.py +928 -0
- package/src/scripts/deep-sleep/extract 2.py +330 -0
- package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
- package/src/scripts/deep-sleep/synthesize 2.py +312 -0
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
- package/src/scripts/nexo-agent-run 2.py +75 -0
- package/src/scripts/nexo-auto-update 2.py +6 -0
- package/src/scripts/nexo-backup 2.sh +25 -0
- package/src/scripts/nexo-brain-activation 2.sh +140 -0
- package/src/scripts/nexo-catchup 2.py +300 -0
- package/src/scripts/nexo-cognitive-decay 2.py +257 -0
- package/src/scripts/nexo-cortex-cycle 2.py +293 -0
- package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
- package/src/scripts/nexo-cron-wrapper.sh +7 -0
- package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
- package/src/scripts/nexo-dashboard 2.sh +29 -0
- package/src/scripts/nexo-deep-sleep 2.sh +86 -0
- package/src/scripts/nexo-evolution-run 2.py +1664 -0
- package/src/scripts/nexo-followup-hygiene 2.py +139 -0
- package/src/scripts/nexo-hook-record 2.py +42 -0
- package/src/scripts/nexo-immune 2.py +936 -0
- package/src/scripts/nexo-impact-scorer 2.py +117 -0
- package/src/scripts/nexo-inbox-hook 2.sh +74 -0
- package/src/scripts/nexo-install 2.py +6 -0
- package/src/scripts/nexo-learning-housekeep 2.py +401 -0
- package/src/scripts/nexo-learning-validator 2.py +266 -0
- package/src/scripts/nexo-migrate 2.py +260 -0
- package/src/scripts/nexo-outcome-checker 2.py +127 -0
- package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
- package/src/scripts/nexo-pre-commit 2.py +120 -0
- package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
- package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
- package/src/scripts/nexo-reflection 2.py +256 -0
- package/src/scripts/nexo-runtime-preflight 2.py +274 -0
- package/src/scripts/nexo-sleep 2.py +631 -0
- package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
- package/src/scripts/nexo-sync-clients 2.py +16 -0
- package/src/scripts/nexo-synthesis 2.py +475 -0
- package/src/scripts/nexo-tcc-approve 2.sh +79 -0
- package/src/scripts/nexo-update 2.sh +306 -0
- package/src/scripts/nexo-watchdog 2.sh +1207 -0
- package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
- package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
- package/src/server 2.py +1296 -0
- package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
- package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
- package/src/skills/run-release-final-audit/guide 2.md +16 -0
- package/src/skills/run-release-final-audit/script 2.py +259 -0
- package/src/skills/run-release-final-audit/skill 2.json +77 -0
- package/src/skills/run-runtime-doctor/guide 2.md +12 -0
- package/src/skills/run-runtime-doctor/script 2.py +21 -0
- package/src/skills/run-runtime-doctor/skill 2.json +25 -0
- package/src/skills_runtime 2.py +932 -0
- package/src/state_watchers_runtime 2.py +475 -0
- package/src/storage_router 2.py +32 -0
- package/src/system_catalog 2.py +786 -0
- package/src/tools_coordination 2.py +103 -0
- package/src/tools_credentials 2.py +68 -0
- package/src/tools_drive 2.py +487 -0
- package/src/tools_hot_context 2.py +163 -0
- package/src/tools_learnings 2.py +612 -0
- package/src/tools_menu 2.py +229 -0
- package/src/tools_reminders 2.py +88 -0
- package/src/tools_reminders_crud 2.py +363 -0
- package/src/tools_sessions 2.py +1054 -0
- package/src/tools_system_catalog 2.py +19 -0
- package/src/tools_task_history 2.py +57 -0
- package/src/tools_transcripts 2.py +98 -0
- package/src/transcript_utils 2.py +412 -0
- package/src/user_context 2.py +46 -0
- package/src/user_data_portability 2.py +328 -0
- package/src/user_state_model 2.py +170 -0
- package/templates/CLAUDE.md 2.template +108 -0
- package/templates/CODEX.AGENTS.md 2.template +66 -0
- package/templates/launchagents/README 2.md +132 -0
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
- package/templates/launchagents/com.nexo.auto-close-sessions.plist +1 -1
- package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
- package/templates/launchagents/com.nexo.catchup.plist +1 -1
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
- package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
- package/templates/launchagents/com.nexo.dashboard.plist +1 -1
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
- package/templates/launchagents/com.nexo.deep-sleep.plist +1 -1
- package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
- package/templates/launchagents/com.nexo.evolution.plist +1 -1
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
- package/templates/launchagents/com.nexo.followup-hygiene.plist +1 -1
- package/templates/launchagents/com.nexo.immune 2.plist +41 -0
- package/templates/launchagents/com.nexo.immune.plist +1 -1
- package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
- package/templates/launchagents/com.nexo.postmortem.plist +1 -1
- package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
- package/templates/launchagents/com.nexo.self-audit.plist +1 -1
- package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
- package/templates/launchagents/com.nexo.synthesis.plist +1 -1
- package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
- package/templates/launchagents/com.nexo.watchdog.plist +1 -1
- package/templates/nexo_helper 2.py +301 -0
- package/templates/openclaw 2.json +13 -0
- package/templates/plugin-template 2.py +40 -0
- package/templates/script-template 2.py +59 -0
- package/templates/script-template 2.sh +13 -0
- package/templates/script-template.py +5 -4
- package/templates/skill-script-template 2.py +48 -0
- package/templates/skill-script-template.py +2 -1
- package/templates/skill-template 2.md +33 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
NEXO Learning Validator — Cross-check findings against existing learnings.
|
|
4
|
+
|
|
5
|
+
The wrapper collects the finding + current learnings from SQLite, then asks the
|
|
6
|
+
configured automation backend whether the finding is already known, related, or
|
|
7
|
+
genuinely new. If the backend is unavailable, it falls back to mechanical
|
|
8
|
+
similarity matching.
|
|
9
|
+
|
|
10
|
+
Usage as CLI:
|
|
11
|
+
python3 nexo-learning-validator.py "finding text to validate"
|
|
12
|
+
python3 nexo-learning-validator.py --category project "finding text"
|
|
13
|
+
|
|
14
|
+
Usage as library:
|
|
15
|
+
from nexo_learning_validator import validate_finding
|
|
16
|
+
result = validate_finding("CRITICAL: message_id column is NULL")
|
|
17
|
+
if result["known"]:
|
|
18
|
+
print(f"Already known: {result['matching_learnings']}")
|
|
19
|
+
|
|
20
|
+
Exit codes:
|
|
21
|
+
0 = Finding is NEW (not known)
|
|
22
|
+
1 = Finding is KNOWN (matches existing learning)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import sqlite3
|
|
30
|
+
import sys
|
|
31
|
+
from pathlib import Path
|
|
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().parents[1])))
|
|
35
|
+
if str(NEXO_CODE) not in sys.path:
|
|
36
|
+
sys.path.insert(0, str(NEXO_CODE))
|
|
37
|
+
|
|
38
|
+
from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
from client_preferences import resolve_user_model as _resolve_user_model
|
|
42
|
+
_USER_MODEL = _resolve_user_model()
|
|
43
|
+
except Exception:
|
|
44
|
+
_USER_MODEL = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
49
|
+
JSON_ONLY_SYSTEM_PROMPT = (
|
|
50
|
+
"Return exactly one valid JSON object. No markdown fences. No prose outside JSON."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_all_learnings(category: str | None = None) -> list[dict]:
|
|
55
|
+
"""Fetch all learnings from nexo.db."""
|
|
56
|
+
conn = sqlite3.connect(str(NEXO_DB), timeout=10)
|
|
57
|
+
conn.row_factory = sqlite3.Row
|
|
58
|
+
if category:
|
|
59
|
+
rows = conn.execute(
|
|
60
|
+
"SELECT id, category, title, content FROM learnings WHERE category = ?",
|
|
61
|
+
(category,),
|
|
62
|
+
).fetchall()
|
|
63
|
+
else:
|
|
64
|
+
rows = conn.execute(
|
|
65
|
+
"SELECT id, category, title, content FROM learnings"
|
|
66
|
+
).fetchall()
|
|
67
|
+
conn.close()
|
|
68
|
+
return [dict(r) for r in rows]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _extract_json(text: str) -> dict | None:
|
|
72
|
+
text = (text or "").strip()
|
|
73
|
+
if not text:
|
|
74
|
+
return None
|
|
75
|
+
if text.startswith("```"):
|
|
76
|
+
lines = text.splitlines()
|
|
77
|
+
end = len(lines)
|
|
78
|
+
for idx in range(len(lines) - 1, 0, -1):
|
|
79
|
+
if lines[idx].strip() == "```":
|
|
80
|
+
end = idx
|
|
81
|
+
break
|
|
82
|
+
text = "\n".join(lines[1:end]).strip()
|
|
83
|
+
brace_start = text.find("{")
|
|
84
|
+
if brace_start < 0:
|
|
85
|
+
return None
|
|
86
|
+
depth = 0
|
|
87
|
+
for idx in range(brace_start, len(text)):
|
|
88
|
+
if text[idx] == "{":
|
|
89
|
+
depth += 1
|
|
90
|
+
elif text[idx] == "}":
|
|
91
|
+
depth -= 1
|
|
92
|
+
if depth == 0:
|
|
93
|
+
try:
|
|
94
|
+
return json.loads(text[brace_start:idx + 1])
|
|
95
|
+
except json.JSONDecodeError:
|
|
96
|
+
return None
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def validate_finding(finding: str, category: str | None = None) -> dict:
|
|
101
|
+
"""
|
|
102
|
+
Validate a finding against existing learnings.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
{
|
|
106
|
+
"known": bool,
|
|
107
|
+
"confidence": float (0-1),
|
|
108
|
+
"matching_learnings": [{"id": int, "title": str, "similarity": float}],
|
|
109
|
+
"recommendation": str
|
|
110
|
+
}
|
|
111
|
+
"""
|
|
112
|
+
learnings = get_all_learnings(category)
|
|
113
|
+
|
|
114
|
+
if not learnings:
|
|
115
|
+
return {
|
|
116
|
+
"known": False,
|
|
117
|
+
"confidence": 0,
|
|
118
|
+
"matching_learnings": [],
|
|
119
|
+
"recommendation": "No learnings in DB — finding is new by default",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
learnings_ref = [
|
|
123
|
+
{
|
|
124
|
+
"id": l["id"],
|
|
125
|
+
"cat": l["category"],
|
|
126
|
+
"title": l["title"],
|
|
127
|
+
"content": (l["content"] or "")[:300],
|
|
128
|
+
}
|
|
129
|
+
for l in learnings
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
prompt = f"""You are a finding deduplication engine. Compare a new finding against existing learnings and determine if it's already known.
|
|
133
|
+
|
|
134
|
+
NEW FINDING:
|
|
135
|
+
{finding}
|
|
136
|
+
|
|
137
|
+
EXISTING LEARNINGS ({len(learnings_ref)} total):
|
|
138
|
+
{json.dumps(learnings_ref, indent=1, ensure_ascii=False)}
|
|
139
|
+
|
|
140
|
+
Respond with ONLY valid JSON:
|
|
141
|
+
{{
|
|
142
|
+
"known": true/false,
|
|
143
|
+
"confidence": 0.0-1.0,
|
|
144
|
+
"matching_learnings": [
|
|
145
|
+
{{"id": <learning_id>, "title": "<title>", "similarity": 0.0-1.0}}
|
|
146
|
+
],
|
|
147
|
+
"recommendation": "<one line: KNOWN/LIKELY KNOWN/POSSIBLY RELATED/NEW>"
|
|
148
|
+
}}
|
|
149
|
+
|
|
150
|
+
Rules:
|
|
151
|
+
- confidence >= 0.7 and same root cause = known: true
|
|
152
|
+
- confidence 0.55-0.7 and related topic = known: true, say LIKELY KNOWN
|
|
153
|
+
- confidence < 0.55 = known: false
|
|
154
|
+
- Max 5 matching_learnings, sorted by similarity descending
|
|
155
|
+
- If the finding describes the SAME bug/issue/pattern as a learning, it's known even if worded differently
|
|
156
|
+
- Be strict: different symptoms of different bugs are NOT the same even if they mention the same file"""
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
result = run_automation_prompt(
|
|
160
|
+
prompt,
|
|
161
|
+
model=_USER_MODEL or "sonnet",
|
|
162
|
+
timeout=60,
|
|
163
|
+
output_format="text",
|
|
164
|
+
append_system_prompt=JSON_ONLY_SYSTEM_PROMPT,
|
|
165
|
+
)
|
|
166
|
+
parsed = _extract_json(result.stdout)
|
|
167
|
+
if result.returncode == 0 and parsed:
|
|
168
|
+
return parsed
|
|
169
|
+
except AutomationBackendUnavailableError:
|
|
170
|
+
pass
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
return _mechanical_validate(finding, learnings)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _mechanical_validate(finding: str, learnings: list[dict]) -> dict:
|
|
178
|
+
"""Fallback validation using SequenceMatcher when backend is unavailable."""
|
|
179
|
+
from difflib import SequenceMatcher
|
|
180
|
+
|
|
181
|
+
threshold = 0.45
|
|
182
|
+
finding_kw = _extract_keywords(finding)
|
|
183
|
+
matches = []
|
|
184
|
+
|
|
185
|
+
for learning in learnings:
|
|
186
|
+
title_sim = SequenceMatcher(None, finding.lower(), learning["title"].lower()).ratio()
|
|
187
|
+
content_sim = SequenceMatcher(None, finding.lower(), (learning["content"] or "").lower()).ratio()
|
|
188
|
+
|
|
189
|
+
learning_text = f"{learning['title']} {learning['content'] or ''}"
|
|
190
|
+
learning_kw = _extract_keywords(learning_text)
|
|
191
|
+
kw_overlap = len(finding_kw & learning_kw) / len(finding_kw) if finding_kw and learning_kw else 0
|
|
192
|
+
|
|
193
|
+
combined = max(title_sim, content_sim) * 0.6 + kw_overlap * 0.4
|
|
194
|
+
|
|
195
|
+
if combined >= threshold:
|
|
196
|
+
matches.append({
|
|
197
|
+
"id": learning["id"],
|
|
198
|
+
"category": learning["category"],
|
|
199
|
+
"title": learning["title"],
|
|
200
|
+
"similarity": round(combined, 3),
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
matches.sort(key=lambda x: x["similarity"], reverse=True)
|
|
204
|
+
top = matches[:5]
|
|
205
|
+
|
|
206
|
+
if not top:
|
|
207
|
+
return {"known": False, "confidence": 0, "matching_learnings": [], "recommendation": "NEW finding"}
|
|
208
|
+
|
|
209
|
+
best = top[0]["similarity"]
|
|
210
|
+
if best >= 0.7:
|
|
211
|
+
return {"known": True, "confidence": best, "matching_learnings": top,
|
|
212
|
+
"recommendation": f"KNOWN issue (learning #{top[0]['id']})"}
|
|
213
|
+
if best >= 0.55:
|
|
214
|
+
return {"known": True, "confidence": best, "matching_learnings": top,
|
|
215
|
+
"recommendation": f"LIKELY KNOWN (learning #{top[0]['id']})"}
|
|
216
|
+
return {"known": False, "confidence": best, "matching_learnings": top,
|
|
217
|
+
"recommendation": "POSSIBLY RELATED but different enough to report"}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _extract_keywords(text: str) -> set:
|
|
221
|
+
"""Extract meaningful keywords from text."""
|
|
222
|
+
stop_words = {
|
|
223
|
+
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
|
|
224
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
225
|
+
"should", "may", "might", "must", "shall", "can", "need", "dare",
|
|
226
|
+
"to", "of", "in", "for", "on", "with", "at", "by", "from", "as",
|
|
227
|
+
"and", "but", "or", "nor", "not", "so", "yet", "both", "either",
|
|
228
|
+
"error", "critical", "warning", "bug", "issue", "problem", "fix",
|
|
229
|
+
"el", "la", "los", "las", "un", "una", "de", "en", "que", "por",
|
|
230
|
+
}
|
|
231
|
+
words = set()
|
|
232
|
+
for word in text.lower().split():
|
|
233
|
+
clean = "".join(c for c in word if c.isalnum() or c == "_")
|
|
234
|
+
if clean and len(clean) > 2 and clean not in stop_words:
|
|
235
|
+
words.add(clean)
|
|
236
|
+
return words
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main():
|
|
240
|
+
import argparse
|
|
241
|
+
|
|
242
|
+
parser = argparse.ArgumentParser(description="Validate findings against existing NEXO learnings")
|
|
243
|
+
parser.add_argument("finding", help="The finding text to validate")
|
|
244
|
+
parser.add_argument("--category", "-c", help="Filter learnings by category")
|
|
245
|
+
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
|
246
|
+
args = parser.parse_args()
|
|
247
|
+
|
|
248
|
+
result = validate_finding(args.finding, args.category)
|
|
249
|
+
|
|
250
|
+
if args.json:
|
|
251
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
252
|
+
else:
|
|
253
|
+
status = "KNOWN" if result["known"] else "NEW"
|
|
254
|
+
print(f"Status: {status} (confidence: {result['confidence']:.0%})")
|
|
255
|
+
print(f"Recommendation: {result['recommendation']}")
|
|
256
|
+
if result["matching_learnings"]:
|
|
257
|
+
print("Related learnings:")
|
|
258
|
+
for match in result["matching_learnings"]:
|
|
259
|
+
cat = match.get("category", "?")
|
|
260
|
+
print(f" #{match['id']} [{cat}] {match['title']} ({match['similarity']:.0%})")
|
|
261
|
+
|
|
262
|
+
sys.exit(1 if result["known"] else 0)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
main()
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""NEXO Migration Tool — automatic, idempotent upgrades between versions.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 nexo-migrate.py # auto-detect current → target
|
|
6
|
+
python3 nexo-migrate.py --dry-run # show what would happen
|
|
7
|
+
python3 nexo-migrate.py --from 1.6.0 # override detected current version
|
|
8
|
+
|
|
9
|
+
Reads current version from $NEXO_HOME/version.json.
|
|
10
|
+
Reads target version from the installed runtime package.json.
|
|
11
|
+
Backs up NEXO_HOME/db/ before any migration.
|
|
12
|
+
Runs DB schema migrations via the existing _schema.py system.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import sys
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))).expanduser()
|
|
24
|
+
SCRIPT_ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _resolve_runtime_root() -> Path:
|
|
28
|
+
candidates: list[Path] = []
|
|
29
|
+
|
|
30
|
+
raw_code = os.environ.get("NEXO_CODE", "").strip()
|
|
31
|
+
if raw_code:
|
|
32
|
+
env_code = Path(raw_code).expanduser()
|
|
33
|
+
candidates.append(env_code)
|
|
34
|
+
if env_code.name == "src":
|
|
35
|
+
candidates.append(env_code.parent)
|
|
36
|
+
|
|
37
|
+
candidates.extend([SCRIPT_ROOT, NEXO_HOME])
|
|
38
|
+
|
|
39
|
+
seen: set[str] = set()
|
|
40
|
+
for candidate in candidates:
|
|
41
|
+
candidate = candidate if candidate.is_dir() else candidate.parent
|
|
42
|
+
key = str(candidate)
|
|
43
|
+
if key in seen:
|
|
44
|
+
continue
|
|
45
|
+
seen.add(key)
|
|
46
|
+
if (candidate / "package.json").is_file():
|
|
47
|
+
return candidate
|
|
48
|
+
if (candidate / "src" / "server.py").is_file() and (candidate / "package.json").is_file():
|
|
49
|
+
return candidate
|
|
50
|
+
|
|
51
|
+
return SCRIPT_ROOT
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
RUNTIME_ROOT = _resolve_runtime_root()
|
|
55
|
+
SOURCE_ROOT = RUNTIME_ROOT / "src" if (RUNTIME_ROOT / "src" / "db").is_dir() else RUNTIME_ROOT
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── Version helpers ──────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def parse_version(v: str) -> tuple:
|
|
61
|
+
"""Parse '1.7.0-beta.1' → (1, 7, 0, 'beta.1'). Pre-release is optional."""
|
|
62
|
+
parts = v.strip().lstrip("v").split("-", 1)
|
|
63
|
+
nums = tuple(int(x) for x in parts[0].split("."))
|
|
64
|
+
pre = parts[1] if len(parts) > 1 else ""
|
|
65
|
+
return (*nums, pre)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def version_key(v: str) -> tuple:
|
|
69
|
+
"""Sortable key: releases sort after pre-releases of same version."""
|
|
70
|
+
nums = parse_version(v)
|
|
71
|
+
# Empty pre-release string sorts AFTER any pre-release tag
|
|
72
|
+
pre = nums[3] if len(nums) > 3 else ""
|
|
73
|
+
return (nums[0], nums[1], nums[2], 0 if pre else 1, pre)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_current_version() -> str:
|
|
77
|
+
"""Read installed version from NEXO_HOME/version.json."""
|
|
78
|
+
vfile = NEXO_HOME / "version.json"
|
|
79
|
+
if not vfile.exists():
|
|
80
|
+
return "0.0.0"
|
|
81
|
+
try:
|
|
82
|
+
data = json.loads(vfile.read_text())
|
|
83
|
+
return data.get("version", "0.0.0")
|
|
84
|
+
except Exception:
|
|
85
|
+
return "0.0.0"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_target_version() -> str:
|
|
89
|
+
"""Read target version from installed runtime package.json."""
|
|
90
|
+
pkg = RUNTIME_ROOT / "package.json"
|
|
91
|
+
if not pkg.exists():
|
|
92
|
+
print(f"ERROR: package.json not found at {pkg}", file=sys.stderr)
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
data = json.loads(pkg.read_text())
|
|
95
|
+
return data["version"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ── Backup ───────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
def backup_databases() -> str:
|
|
101
|
+
"""Backup all .db files before migration. Returns backup dir path."""
|
|
102
|
+
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
103
|
+
backup_dir = NEXO_HOME / "backups" / f"pre-migrate-{ts}"
|
|
104
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
|
|
106
|
+
data_dir = NEXO_HOME / "data"
|
|
107
|
+
if data_dir.exists():
|
|
108
|
+
for db_file in data_dir.glob("*.db*"):
|
|
109
|
+
shutil.copy2(db_file, backup_dir / db_file.name)
|
|
110
|
+
# Also check legacy db/ location
|
|
111
|
+
legacy_db_dir = NEXO_HOME / "db"
|
|
112
|
+
if legacy_db_dir.exists():
|
|
113
|
+
for db_file in legacy_db_dir.glob("*.db*"):
|
|
114
|
+
if not (backup_dir / db_file.name).exists():
|
|
115
|
+
shutil.copy2(db_file, backup_dir / db_file.name)
|
|
116
|
+
|
|
117
|
+
# Also backup version.json
|
|
118
|
+
vfile = NEXO_HOME / "version.json"
|
|
119
|
+
if vfile.exists():
|
|
120
|
+
shutil.copy2(vfile, backup_dir / "version.json")
|
|
121
|
+
|
|
122
|
+
return str(backup_dir)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ── Migration steps ──────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
def ensure_nexo_home_dirs():
|
|
128
|
+
"""Create all required NEXO_HOME subdirectories."""
|
|
129
|
+
dirs = [
|
|
130
|
+
"db", "brain", "logs", "operations", "coordination",
|
|
131
|
+
"scripts", "hooks", "plugins", "backups", "memory",
|
|
132
|
+
"docs", "projects", "learnings", "agents", "skills",
|
|
133
|
+
]
|
|
134
|
+
for d in dirs:
|
|
135
|
+
(NEXO_HOME / d).mkdir(parents=True, exist_ok=True)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run_db_schema_migrations():
|
|
139
|
+
"""Run the formal DB schema migration system from _schema.py."""
|
|
140
|
+
if str(SOURCE_ROOT) not in sys.path:
|
|
141
|
+
sys.path.insert(0, str(SOURCE_ROOT))
|
|
142
|
+
|
|
143
|
+
# Set NEXO_HOME env for the db module
|
|
144
|
+
os.environ["NEXO_HOME"] = str(NEXO_HOME)
|
|
145
|
+
os.environ["NEXO_SKIP_FS_INDEX"] = "1" # Don't rebuild FTS during migration
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
from db import init_db
|
|
149
|
+
init_db()
|
|
150
|
+
print(" DB schema migrations applied.")
|
|
151
|
+
except Exception as e:
|
|
152
|
+
print(f" WARNING: DB schema migration error: {e}", file=sys.stderr)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def write_version_json(version: str):
|
|
156
|
+
"""Write version.json with the installed version."""
|
|
157
|
+
vfile = NEXO_HOME / "version.json"
|
|
158
|
+
data = {
|
|
159
|
+
"version": version,
|
|
160
|
+
"installed_at": datetime.now().isoformat(timespec="seconds"),
|
|
161
|
+
"nexo_home": str(NEXO_HOME),
|
|
162
|
+
}
|
|
163
|
+
vfile.write_text(json.dumps(data, indent=2) + "\n")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ── Migration registry ───────────────────────────────────────────
|
|
167
|
+
# Each entry: version → list of (description, callable)
|
|
168
|
+
# Migrations run for all versions > current AND <= target.
|
|
169
|
+
|
|
170
|
+
def _migrate_1_7_0():
|
|
171
|
+
"""1.7.0: Ensure NEXO_HOME paths, create directories, update version."""
|
|
172
|
+
ensure_nexo_home_dirs()
|
|
173
|
+
run_db_schema_migrations()
|
|
174
|
+
print(" Created/verified all NEXO_HOME directories.")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
MIGRATION_REGISTRY: dict[str, list[tuple[str, callable]]] = {
|
|
178
|
+
"1.7.0": [
|
|
179
|
+
("Ensure NEXO_HOME dirs + DB schema", _migrate_1_7_0),
|
|
180
|
+
],
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ── Main ─────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
def get_applicable_migrations(current: str, target: str) -> list[tuple[str, str, callable]]:
|
|
187
|
+
"""Return list of (version, description, fn) for migrations between current and target."""
|
|
188
|
+
current_key = version_key(current)
|
|
189
|
+
target_key = version_key(target)
|
|
190
|
+
|
|
191
|
+
applicable = []
|
|
192
|
+
for ver, steps in sorted(MIGRATION_REGISTRY.items(), key=lambda x: version_key(x[0])):
|
|
193
|
+
ver_key = version_key(ver)
|
|
194
|
+
# Run if version > current and <= target (base version comparison)
|
|
195
|
+
base_ver = ver.split("-")[0] # strip pre-release for comparison
|
|
196
|
+
base_ver_key = version_key(base_ver)
|
|
197
|
+
if base_ver_key > (current_key[0], current_key[1], current_key[2], current_key[3], current_key[4] if len(current_key) > 4 else ""):
|
|
198
|
+
if base_ver_key <= (target_key[0], target_key[1], target_key[2], 1, ""):
|
|
199
|
+
for desc, fn in steps:
|
|
200
|
+
applicable.append((ver, desc, fn))
|
|
201
|
+
|
|
202
|
+
return applicable
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main():
|
|
206
|
+
parser = argparse.ArgumentParser(description="NEXO Migration Tool")
|
|
207
|
+
parser.add_argument("--dry-run", action="store_true", help="Show what would happen without executing")
|
|
208
|
+
parser.add_argument("--from", dest="from_ver", help="Override detected current version")
|
|
209
|
+
args = parser.parse_args()
|
|
210
|
+
|
|
211
|
+
current = args.from_ver or get_current_version()
|
|
212
|
+
target = get_target_version()
|
|
213
|
+
|
|
214
|
+
print(f"NEXO Migration: {current} → {target}")
|
|
215
|
+
print(f"NEXO_HOME: {NEXO_HOME}")
|
|
216
|
+
print()
|
|
217
|
+
|
|
218
|
+
if version_key(current) >= version_key(target):
|
|
219
|
+
print("Already up to date. Nothing to migrate.")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
migrations = get_applicable_migrations(current, target)
|
|
223
|
+
if not migrations:
|
|
224
|
+
print("No migration steps needed (only version bump).")
|
|
225
|
+
else:
|
|
226
|
+
print(f"Migrations to run ({len(migrations)}):")
|
|
227
|
+
for ver, desc, _ in migrations:
|
|
228
|
+
print(f" [{ver}] {desc}")
|
|
229
|
+
print()
|
|
230
|
+
|
|
231
|
+
if args.dry_run:
|
|
232
|
+
print("DRY RUN — no changes made.")
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
# Backup before anything
|
|
236
|
+
backup_path = backup_databases()
|
|
237
|
+
print(f"Backup created: {backup_path}")
|
|
238
|
+
print()
|
|
239
|
+
|
|
240
|
+
# Ensure base directories exist
|
|
241
|
+
ensure_nexo_home_dirs()
|
|
242
|
+
|
|
243
|
+
# Run migrations
|
|
244
|
+
for ver, desc, fn in migrations:
|
|
245
|
+
print(f"Running [{ver}] {desc}...")
|
|
246
|
+
try:
|
|
247
|
+
fn()
|
|
248
|
+
print(f" Done.")
|
|
249
|
+
except Exception as e:
|
|
250
|
+
print(f" ERROR: {e}", file=sys.stderr)
|
|
251
|
+
print(f" Backup at: {backup_path}", file=sys.stderr)
|
|
252
|
+
sys.exit(1)
|
|
253
|
+
|
|
254
|
+
# Write final version
|
|
255
|
+
write_version_json(target)
|
|
256
|
+
print(f"\nMigration complete: {current} → {target}")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if __name__ == "__main__":
|
|
260
|
+
main()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
"""NEXO Outcome Checker — daily verification of pending tracked outcomes."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
12
|
+
_script_dir = Path(__file__).resolve().parent
|
|
13
|
+
_repo_src = _script_dir.parent
|
|
14
|
+
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
|
|
15
|
+
if str(NEXO_CODE) not in sys.path:
|
|
16
|
+
sys.path.insert(0, str(NEXO_CODE))
|
|
17
|
+
|
|
18
|
+
import db as nexo_db
|
|
19
|
+
|
|
20
|
+
LOG_FILE = NEXO_HOME / "logs" / "outcome-checker.log"
|
|
21
|
+
SUMMARY_FILE = NEXO_HOME / "coordination" / "outcome-checker-summary.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def log(message: str):
|
|
25
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
26
|
+
line = f"[{ts}] {message}"
|
|
27
|
+
print(line, flush=True)
|
|
28
|
+
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
with open(LOG_FILE, "a") as handle:
|
|
30
|
+
handle.write(line + "\n")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main() -> int:
|
|
34
|
+
log("=== Outcome Checker starting ===")
|
|
35
|
+
nexo_db.init_db()
|
|
36
|
+
|
|
37
|
+
due_rows = nexo_db.pending_outcomes_due(limit=500)
|
|
38
|
+
if not due_rows:
|
|
39
|
+
summary = {
|
|
40
|
+
"checked_at": datetime.now().isoformat(timespec="seconds"),
|
|
41
|
+
"checked": 0,
|
|
42
|
+
"met": 0,
|
|
43
|
+
"missed": 0,
|
|
44
|
+
"pending": 0,
|
|
45
|
+
"errors": 0,
|
|
46
|
+
"ids": [],
|
|
47
|
+
}
|
|
48
|
+
SUMMARY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
SUMMARY_FILE.write_text(json.dumps(summary, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
50
|
+
log("No pending due outcomes.")
|
|
51
|
+
log("=== Outcome Checker complete ===")
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
checked = met = missed = pending = errors = 0
|
|
55
|
+
checked_ids: list[int] = []
|
|
56
|
+
for row in due_rows:
|
|
57
|
+
checked += 1
|
|
58
|
+
checked_ids.append(int(row["id"]))
|
|
59
|
+
result = nexo_db.evaluate_outcome(int(row["id"]), create_learning_on_miss=True)
|
|
60
|
+
if "error" in result:
|
|
61
|
+
errors += 1
|
|
62
|
+
log(f"ERROR outcome #{row['id']}: {result['error']}")
|
|
63
|
+
continue
|
|
64
|
+
status = result.get("status", "pending")
|
|
65
|
+
if status == "met":
|
|
66
|
+
met += 1
|
|
67
|
+
elif status == "missed":
|
|
68
|
+
missed += 1
|
|
69
|
+
else:
|
|
70
|
+
pending += 1
|
|
71
|
+
log(
|
|
72
|
+
f"Outcome #{row['id']} -> {status} "
|
|
73
|
+
f"(action={result.get('action_type')}:{result.get('action_id') or '—'}, "
|
|
74
|
+
f"deadline={result.get('deadline')})"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Phase 2 item 2: after closing outcomes, attempt to promote any
|
|
78
|
+
# outcome pattern that just crossed the suggested-skill threshold to a
|
|
79
|
+
# real draft skill. The helper is idempotent and capped at
|
|
80
|
+
# max_promotions per run, so this is safe to call on every cycle.
|
|
81
|
+
promotion_summary: dict = {"promoted": [], "skipped": [], "errors": [], "scanned": 0}
|
|
82
|
+
try:
|
|
83
|
+
from skills_runtime import auto_promote_outcome_patterns_to_skills
|
|
84
|
+
promotion_summary = auto_promote_outcome_patterns_to_skills(
|
|
85
|
+
min_success_rate=0.8,
|
|
86
|
+
max_promotions=3,
|
|
87
|
+
)
|
|
88
|
+
if promotion_summary.get("promoted"):
|
|
89
|
+
log(
|
|
90
|
+
f"Auto-promoted {len(promotion_summary['promoted'])} outcome pattern(s) "
|
|
91
|
+
f"to skill draft(s) (scanned={promotion_summary.get('scanned', 0)})"
|
|
92
|
+
)
|
|
93
|
+
for entry in promotion_summary["promoted"]:
|
|
94
|
+
log(
|
|
95
|
+
f" -> {entry.get('pattern_key')} -> skill {entry.get('skill_id')} "
|
|
96
|
+
f"(success_rate={entry.get('success_rate')}, created={entry.get('created')})"
|
|
97
|
+
)
|
|
98
|
+
elif promotion_summary.get("scanned"):
|
|
99
|
+
log(
|
|
100
|
+
f"Outcome pattern auto-promote: scanned {promotion_summary['scanned']}, "
|
|
101
|
+
f"none qualified (skipped={len(promotion_summary['skipped'])})"
|
|
102
|
+
)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
log(f"WARN: outcome pattern auto-promote raised: {e}")
|
|
105
|
+
|
|
106
|
+
summary = {
|
|
107
|
+
"checked_at": datetime.now().isoformat(timespec="seconds"),
|
|
108
|
+
"checked": checked,
|
|
109
|
+
"met": met,
|
|
110
|
+
"missed": missed,
|
|
111
|
+
"pending": pending,
|
|
112
|
+
"errors": errors,
|
|
113
|
+
"ids": checked_ids,
|
|
114
|
+
"auto_promoted_patterns": promotion_summary,
|
|
115
|
+
}
|
|
116
|
+
SUMMARY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
SUMMARY_FILE.write_text(json.dumps(summary, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
118
|
+
log(
|
|
119
|
+
f"Summary: checked={checked} met={met} missed={missed} "
|
|
120
|
+
f"pending={pending} errors={errors}"
|
|
121
|
+
)
|
|
122
|
+
log("=== Outcome Checker complete ===")
|
|
123
|
+
return 1 if errors else 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
raise SystemExit(main())
|