nexo-brain 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  4. package/src/__pycache__/cognitive.cpython-314.pyc +0 -0
  5. package/src/__pycache__/db.cpython-314.pyc +0 -0
  6. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  7. package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
  8. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  9. package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
  10. package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
  11. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  12. package/src/__pycache__/server.cpython-314.pyc +0 -0
  13. package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
  14. package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
  15. package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
  16. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  17. package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
  18. package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
  19. package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
  20. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  21. package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
  22. package/src/cognitive.py +63 -14
  23. package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
  24. package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
  25. package/src/dashboard/app.py +1 -1
  26. package/src/db.py +29 -13
  27. package/src/evolution_cycle.py +72 -94
  28. package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
  29. package/src/hooks/session-start.sh +5 -2
  30. package/src/hooks/session-stop.sh +79 -133
  31. package/src/knowledge_graph.py +3 -3
  32. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  33. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  34. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  35. package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
  36. package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
  37. package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
  38. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  39. package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
  40. package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
  41. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  42. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  43. package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
  44. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
  45. package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
  46. package/src/plugins/artifact_registry.py +450 -0
  47. package/src/plugins/cognitive_memory.py +9 -9
  48. package/src/plugins/episodic_memory.py +8 -8
  49. package/src/plugins/evolution.py +4 -4
  50. package/src/plugins/guard.py +26 -2
  51. package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
  52. package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
  53. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  54. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  55. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  56. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  57. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  58. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  59. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  60. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  61. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  62. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  63. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  64. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  65. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  66. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  67. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  68. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  69. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  70. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  71. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  72. package/src/scripts/nexo-brain-activation.sh +140 -0
  73. package/src/scripts/nexo-evolution-run.py +1 -1
  74. package/src/scripts/nexo-followup-hygiene.py +107 -0
  75. package/src/scripts/nexo-postmortem-consolidator.py +25 -25
  76. package/src/scripts/nexo-pre-commit.py +118 -0
  77. package/src/scripts/nexo-proactive-dashboard.py +342 -0
  78. package/src/scripts/nexo-runtime-preflight.py +270 -0
  79. package/src/scripts/nexo-send-email.py +25 -0
  80. package/src/scripts/nexo-send-reply.py +176 -0
  81. package/src/scripts/nexo-snapshot-restore.sh +34 -0
  82. package/src/scripts/nexo-watchdog-smoke.py +114 -0
  83. package/src/server.py +5 -5
  84. package/src/tools_coordination.py +3 -3
  85. package/src/tools_learnings.py +1 -1
  86. package/src/tools_menu.py +31 -49
  87. package/src/tools_reminders_crud.py +1 -1
  88. package/src/tools_sessions.py +12 -12
  89. package/src/rules/__init__ 2.py +0 -0
  90. package/src/rules/migrate 2.py +0 -207
@@ -0,0 +1,450 @@
1
+ """Artifact Registry plugin — structured index of things NEXO creates/deploys.
2
+
3
+ Solves 'recent work amnesia': NEXO builds services, dashboards, scripts, APIs
4
+ but can't find them hours later because semantic search ('backend') doesn't
5
+ match operational terms ('FastAPI localhost:6174').
6
+
7
+ Architecture (from 3-way AI debate — GPT-5.4 + Gemini 3.1 Pro + Claude Opus 4.6):
8
+ 1. Structured SQLite table with aliases, ports, paths, run commands
9
+ 2. Retrieval ladder: exact alias → port/path match → fuzzy token → semantic fallback
10
+ 3. User-language alias learning: when the user says 'backend' and it resolves
11
+ to dashboard:6174, store that mapping for O(1) next time
12
+ 4. Temporal filtering: 'last night' → hard SQL constraint before any search
13
+ """
14
+
15
+ import json
16
+ import datetime
17
+ from db import get_db
18
+
19
+
20
+ # Valid artifact kinds
21
+ VALID_KINDS = {
22
+ 'service', 'dashboard', 'script', 'api', 'cron', 'website',
23
+ 'database', 'repo', 'config', 'tool', 'plugin', 'other',
24
+ }
25
+
26
+ VALID_STATES = {'active', 'inactive', 'broken', 'archived'}
27
+
28
+
29
+ def _cognitive_ingest_safe(content, source_type, source_id="", source_title="", domain=""):
30
+ """Ingest to cognitive STM. Silently fails if cognitive engine unavailable."""
31
+ try:
32
+ import cognitive
33
+ cognitive.ingest(content, source_type, source_id, source_title, domain)
34
+ except Exception:
35
+ pass
36
+
37
+
38
+ def handle_artifact_create(
39
+ kind: str,
40
+ canonical_name: str,
41
+ aliases: str = '[]',
42
+ description: str = '',
43
+ uri: str = '',
44
+ ports: str = '[]',
45
+ paths: str = '[]',
46
+ run_cmd: str = '',
47
+ repo: str = '',
48
+ domain: str = '',
49
+ session_id: str = '',
50
+ metadata: str = '{}',
51
+ ) -> str:
52
+ """Register a new artifact (service, dashboard, script, API, etc.).
53
+
54
+ Call this whenever NEXO creates, deploys, or discovers a runnable/accessible artifact.
55
+
56
+ Args:
57
+ kind: Type — service, dashboard, script, api, cron, website, database, repo, config, tool, plugin, other
58
+ canonical_name: Primary name (e.g., 'NEXO Brain Dashboard')
59
+ aliases: JSON array of alternative names users might use (e.g., '["backend", "dashboard", "nexo web"]')
60
+ description: What it does (1-2 sentences)
61
+ uri: Access URL or address (e.g., 'localhost:6174', 'nexo-brain.com')
62
+ ports: JSON array of ports (e.g., '[6174]')
63
+ paths: JSON array of file paths (e.g., '["/Users/x/nexo/src/dashboard/app.py"]')
64
+ run_cmd: Command to start/open it (e.g., 'python3 -m dashboard.app --port 6174')
65
+ repo: Repository path or URL
66
+ domain: Project domain (nexo, my-project, project-a, project-b, etc.)
67
+ session_id: Current session ID
68
+ metadata: JSON object with extra key-value pairs
69
+ """
70
+ if kind not in VALID_KINDS:
71
+ return f"ERROR: kind must be one of: {', '.join(sorted(VALID_KINDS))}"
72
+
73
+ # Parse aliases
74
+ try:
75
+ alias_list = json.loads(aliases) if aliases and aliases != '[]' else []
76
+ except (json.JSONDecodeError, TypeError):
77
+ alias_list = [a.strip() for a in aliases.split(',') if a.strip()]
78
+
79
+ conn = get_db()
80
+ cur = conn.execute(
81
+ """INSERT INTO artifact_registry
82
+ (kind, canonical_name, aliases, description, uri, ports, paths,
83
+ run_cmd, repo, domain, state, session_id, metadata)
84
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)""",
85
+ (kind, canonical_name, json.dumps(alias_list), description, uri, ports,
86
+ paths, run_cmd, repo, domain, session_id, metadata),
87
+ )
88
+ artifact_id = cur.lastrowid
89
+ conn.commit()
90
+
91
+ # Insert aliases into lookup table
92
+ for alias in alias_list + [canonical_name.lower()]:
93
+ alias_clean = alias.strip().lower()
94
+ if alias_clean:
95
+ try:
96
+ conn.execute(
97
+ "INSERT OR IGNORE INTO artifact_aliases (artifact_id, phrase, source) VALUES (?, ?, 'create')",
98
+ (artifact_id, alias_clean),
99
+ )
100
+ except Exception:
101
+ pass
102
+ conn.commit()
103
+
104
+ # Ingest to cognitive memory
105
+ content = f"Artifact: {canonical_name} ({kind}). {description}. URI: {uri}. Aliases: {', '.join(alias_list)}"
106
+ _cognitive_ingest_safe(content, "artifact", f"A{artifact_id}", canonical_name[:80], domain)
107
+
108
+ return f"Artifact #{artifact_id} created: {canonical_name} ({kind}) — {uri or 'no URI'}"
109
+
110
+
111
+ def handle_artifact_find(query: str, kind: str = '', state: str = 'active') -> str:
112
+ """Find artifacts using the retrieval ladder: exact alias → port/path → fuzzy token → all recent.
113
+
114
+ This is the PRIMARY retrieval tool. Use it when the user references something
115
+ they or NEXO built/deployed/created. Designed for natural language like
116
+ 'the backend', 'that script from yesterday', 'localhost something'.
117
+
118
+ Args:
119
+ query: What to search for — name, alias, port, path, or description fragment
120
+ kind: Filter by kind (optional)
121
+ state: Filter by state — default 'active'. Use 'all' for everything.
122
+ """
123
+ conn = get_db()
124
+ results = []
125
+ query_lower = query.strip().lower()
126
+
127
+ state_filter = "AND state = ?" if state != 'all' else ""
128
+ state_params = (state,) if state != 'all' else ()
129
+
130
+ kind_filter = "AND kind = ?" if kind else ""
131
+ kind_params = (kind,) if kind else ()
132
+
133
+ extra_filters = state_filter + " " + kind_filter
134
+ extra_params = state_params + kind_params
135
+
136
+ # --- STAGE 1: Exact alias match (fastest, O(1)) ---
137
+ rows = conn.execute(
138
+ f"""SELECT DISTINCT r.* FROM artifact_registry r
139
+ JOIN artifact_aliases a ON a.artifact_id = r.id
140
+ WHERE a.phrase = ? {extra_filters}
141
+ ORDER BY r.last_touched_at DESC LIMIT 5""",
142
+ (query_lower,) + extra_params,
143
+ ).fetchall()
144
+ if rows:
145
+ results = [dict(r) for r in rows]
146
+ return _format_results(results, "alias match", query)
147
+
148
+ # --- STAGE 2: Port or URI match ---
149
+ rows = conn.execute(
150
+ f"""SELECT * FROM artifact_registry
151
+ WHERE (uri LIKE ? OR ports LIKE ?) {extra_filters}
152
+ ORDER BY last_touched_at DESC LIMIT 5""",
153
+ (f"%{query_lower}%", f"%{query_lower}%") + extra_params,
154
+ ).fetchall()
155
+ if rows:
156
+ results = [dict(r) for r in rows]
157
+ return _format_results(results, "URI/port match", query)
158
+
159
+ # --- STAGE 3: Path match ---
160
+ rows = conn.execute(
161
+ f"""SELECT * FROM artifact_registry
162
+ WHERE paths LIKE ? {extra_filters}
163
+ ORDER BY last_touched_at DESC LIMIT 5""",
164
+ (f"%{query_lower}%",) + extra_params,
165
+ ).fetchall()
166
+ if rows:
167
+ results = [dict(r) for r in rows]
168
+ return _format_results(results, "path match", query)
169
+
170
+ # --- STAGE 4: Fuzzy token match on name, description, aliases ---
171
+ tokens = query_lower.split()
172
+ if tokens:
173
+ conditions = " AND ".join(
174
+ "(LOWER(canonical_name) LIKE ? OR LOWER(description) LIKE ? OR LOWER(aliases) LIKE ?)"
175
+ for _ in tokens
176
+ )
177
+ params = []
178
+ for t in tokens:
179
+ p = f"%{t}%"
180
+ params.extend([p, p, p])
181
+ rows = conn.execute(
182
+ f"""SELECT * FROM artifact_registry
183
+ WHERE {conditions} {extra_filters}
184
+ ORDER BY last_touched_at DESC LIMIT 10""",
185
+ tuple(params) + extra_params,
186
+ ).fetchall()
187
+ if rows:
188
+ results = [dict(r) for r in rows]
189
+ return _format_results(results, "token match", query)
190
+
191
+ # --- STAGE 5: Recent artifacts (last 72h) as fallback ---
192
+ cutoff = (datetime.datetime.now() - datetime.timedelta(hours=72)).isoformat()
193
+ rows = conn.execute(
194
+ f"""SELECT * FROM artifact_registry
195
+ WHERE last_touched_at >= ? {extra_filters}
196
+ ORDER BY last_touched_at DESC LIMIT 10""",
197
+ (cutoff,) + extra_params,
198
+ ).fetchall()
199
+ if rows:
200
+ results = [dict(r) for r in rows]
201
+ return _format_results(results, "recent (72h)", query)
202
+
203
+ return f"No artifacts found for '{query}'. Use artifact_list to see all registered artifacts."
204
+
205
+
206
+ def handle_artifact_update(
207
+ id: int,
208
+ canonical_name: str = '',
209
+ aliases: str = '',
210
+ description: str = '',
211
+ uri: str = '',
212
+ ports: str = '',
213
+ paths: str = '',
214
+ run_cmd: str = '',
215
+ state: str = '',
216
+ domain: str = '',
217
+ metadata: str = '',
218
+ ) -> str:
219
+ """Update an artifact. Only non-empty fields are changed.
220
+
221
+ Args:
222
+ id: Artifact ID to update
223
+ canonical_name: New primary name
224
+ aliases: New JSON array of aliases (replaces existing)
225
+ description: New description
226
+ uri: New URI
227
+ ports: New ports JSON array
228
+ paths: New paths JSON array
229
+ run_cmd: New run command
230
+ state: New state (active, inactive, broken, archived)
231
+ domain: New domain
232
+ metadata: New metadata JSON (merged with existing)
233
+ """
234
+ conn = get_db()
235
+ row = conn.execute("SELECT * FROM artifact_registry WHERE id = ?", (id,)).fetchone()
236
+ if not row:
237
+ return f"ERROR: Artifact #{id} not found."
238
+
239
+ updates = []
240
+ params = []
241
+
242
+ if canonical_name:
243
+ updates.append("canonical_name = ?"); params.append(canonical_name)
244
+ if description:
245
+ updates.append("description = ?"); params.append(description)
246
+ if uri:
247
+ updates.append("uri = ?"); params.append(uri)
248
+ if ports:
249
+ updates.append("ports = ?"); params.append(ports)
250
+ if paths:
251
+ updates.append("paths = ?"); params.append(paths)
252
+ if run_cmd:
253
+ updates.append("run_cmd = ?"); params.append(run_cmd)
254
+ if domain:
255
+ updates.append("domain = ?"); params.append(domain)
256
+ if state:
257
+ if state not in VALID_STATES:
258
+ return f"ERROR: state must be one of: {', '.join(sorted(VALID_STATES))}"
259
+ updates.append("state = ?"); params.append(state)
260
+ if metadata:
261
+ try:
262
+ existing = json.loads(row["metadata"] or '{}')
263
+ new = json.loads(metadata)
264
+ existing.update(new)
265
+ updates.append("metadata = ?"); params.append(json.dumps(existing))
266
+ except (json.JSONDecodeError, TypeError):
267
+ pass
268
+
269
+ if aliases:
270
+ try:
271
+ alias_list = json.loads(aliases) if aliases.startswith('[') else [a.strip() for a in aliases.split(',')]
272
+ except (json.JSONDecodeError, TypeError):
273
+ alias_list = [a.strip() for a in aliases.split(',')]
274
+ updates.append("aliases = ?"); params.append(json.dumps(alias_list))
275
+ # Rebuild alias lookup table
276
+ conn.execute("DELETE FROM artifact_aliases WHERE artifact_id = ?", (id,))
277
+ for alias in alias_list:
278
+ alias_clean = alias.strip().lower()
279
+ if alias_clean:
280
+ conn.execute(
281
+ "INSERT OR IGNORE INTO artifact_aliases (artifact_id, phrase, source) VALUES (?, ?, 'update')",
282
+ (id, alias_clean),
283
+ )
284
+
285
+ if not updates:
286
+ return "Nothing to update."
287
+
288
+ updates.append("last_touched_at = datetime('now')")
289
+ params.append(id)
290
+ conn.execute(f"UPDATE artifact_registry SET {', '.join(updates)} WHERE id = ?", tuple(params))
291
+ conn.commit()
292
+ return f"Artifact #{id} updated."
293
+
294
+
295
+ def handle_artifact_learn_alias(id: int, phrase: str) -> str:
296
+ """Learn a new alias from user language. Call this when the user refers to an
297
+ artifact with a term not yet registered (e.g., the user says 'backend' for dashboard:6174).
298
+
299
+ Args:
300
+ id: Artifact ID
301
+ phrase: The user's term (e.g., 'backend', 'that api thing')
302
+ """
303
+ conn = get_db()
304
+ row = conn.execute("SELECT * FROM artifact_registry WHERE id = ?", (id,)).fetchone()
305
+ if not row:
306
+ return f"ERROR: Artifact #{id} not found."
307
+
308
+ phrase_clean = phrase.strip().lower()
309
+ if not phrase_clean:
310
+ return "ERROR: Empty phrase."
311
+
312
+ # Add to alias lookup table
313
+ conn.execute(
314
+ "INSERT OR IGNORE INTO artifact_aliases (artifact_id, phrase, source) VALUES (?, ?, 'user_language')",
315
+ (id, phrase_clean),
316
+ )
317
+
318
+ # Also add to the artifact's aliases JSON array
319
+ try:
320
+ existing = json.loads(row["aliases"] or '[]')
321
+ except (json.JSONDecodeError, TypeError):
322
+ existing = []
323
+ if phrase_clean not in [a.lower() for a in existing]:
324
+ existing.append(phrase_clean)
325
+ conn.execute(
326
+ "UPDATE artifact_registry SET aliases = ?, last_touched_at = datetime('now') WHERE id = ?",
327
+ (json.dumps(existing), id),
328
+ )
329
+
330
+ conn.commit()
331
+ return f"Alias '{phrase_clean}' learned for artifact #{id} ({row['canonical_name']})."
332
+
333
+
334
+ def handle_artifact_list(kind: str = '', state: str = 'active', recent_hours: int = 0) -> str:
335
+ """List all artifacts, optionally filtered.
336
+
337
+ Args:
338
+ kind: Filter by kind (service, dashboard, script, etc.)
339
+ state: Filter by state — 'active' (default), 'all', 'inactive', 'broken', 'archived'
340
+ recent_hours: If >0, only show artifacts touched in the last N hours
341
+ """
342
+ conn = get_db()
343
+ conditions = []
344
+ params = []
345
+
346
+ if state != 'all':
347
+ conditions.append("state = ?"); params.append(state)
348
+ if kind:
349
+ conditions.append("kind = ?"); params.append(kind)
350
+ if recent_hours > 0:
351
+ cutoff = (datetime.datetime.now() - datetime.timedelta(hours=recent_hours)).isoformat()
352
+ conditions.append("last_touched_at >= ?"); params.append(cutoff)
353
+
354
+ where = "WHERE " + " AND ".join(conditions) if conditions else ""
355
+ rows = conn.execute(
356
+ f"SELECT * FROM artifact_registry {where} ORDER BY last_touched_at DESC",
357
+ tuple(params),
358
+ ).fetchall()
359
+
360
+ if not rows:
361
+ filters = []
362
+ if kind: filters.append(f"kind={kind}")
363
+ if state != 'all': filters.append(f"state={state}")
364
+ if recent_hours: filters.append(f"last {recent_hours}h")
365
+ return f"No artifacts found{' (' + ', '.join(filters) + ')' if filters else ''}."
366
+
367
+ lines = [f"ARTIFACT REGISTRY ({len(rows)}):"]
368
+ for r in rows:
369
+ r = dict(r)
370
+ aliases_str = ""
371
+ try:
372
+ aliases = json.loads(r.get("aliases", "[]"))
373
+ if aliases:
374
+ aliases_str = f" aka [{', '.join(aliases[:3])}]"
375
+ except (json.JSONDecodeError, TypeError):
376
+ pass
377
+ uri_str = f" → {r['uri']}" if r.get("uri") else ""
378
+ cmd_str = f" | cmd: {r['run_cmd'][:60]}" if r.get("run_cmd") else ""
379
+ touched = r.get("last_touched_at", "")[:16]
380
+ lines.append(
381
+ f" #{r['id']} [{r['kind']}] {r['canonical_name']}{aliases_str}{uri_str}{cmd_str} "
382
+ f"({r['state']}, {touched})"
383
+ )
384
+ return "\n".join(lines)
385
+
386
+
387
+ def handle_artifact_delete(id: int) -> str:
388
+ """Delete an artifact from the registry.
389
+
390
+ Args:
391
+ id: Artifact ID to delete
392
+ """
393
+ conn = get_db()
394
+ row = conn.execute("SELECT canonical_name FROM artifact_registry WHERE id = ?", (id,)).fetchone()
395
+ if not row:
396
+ return f"ERROR: Artifact #{id} not found."
397
+ name = row["canonical_name"]
398
+ conn.execute("DELETE FROM artifact_aliases WHERE artifact_id = ?", (id,))
399
+ conn.execute("DELETE FROM artifact_registry WHERE id = ?", (id,))
400
+ conn.commit()
401
+ return f"Artifact #{id} ({name}) deleted."
402
+
403
+
404
+ def _format_results(results, method, query):
405
+ """Format search results for display."""
406
+ lines = [f"ARTIFACTS FOUND ({len(results)}, via {method} for '{query}'):"]
407
+ for r in results:
408
+ aliases_str = ""
409
+ try:
410
+ aliases = json.loads(r.get("aliases", "[]"))
411
+ if aliases:
412
+ aliases_str = f" aka [{', '.join(aliases[:4])}]"
413
+ except (json.JSONDecodeError, TypeError):
414
+ pass
415
+ uri_str = f" → {r['uri']}" if r.get("uri") else ""
416
+ cmd_str = f"\n Run: {r['run_cmd']}" if r.get("run_cmd") else ""
417
+ paths_str = ""
418
+ try:
419
+ paths = json.loads(r.get("paths", "[]"))
420
+ if paths:
421
+ paths_str = f"\n Paths: {', '.join(paths[:3])}"
422
+ except (json.JSONDecodeError, TypeError):
423
+ pass
424
+ touched = r.get("last_touched_at", "")[:16]
425
+ lines.append(
426
+ f" #{r['id']} [{r['kind']}] {r['canonical_name']}{aliases_str}{uri_str} "
427
+ f"({r['state']}, {touched}){cmd_str}{paths_str}"
428
+ )
429
+ return "\n".join(lines)
430
+
431
+
432
+ # Plugin registration — TOOLS array consumed by plugin_loader.py
433
+ TOOLS = [
434
+ (handle_artifact_create, "nexo_artifact_create",
435
+ "Register a new artifact (service, dashboard, script, API, etc.) in the Artifact Registry. "
436
+ "Call this whenever NEXO creates, deploys, or discovers a runnable/accessible artifact."),
437
+ (handle_artifact_find, "nexo_artifact_find",
438
+ "Find artifacts using the retrieval ladder: exact alias → port/path → fuzzy token → recent. "
439
+ "PRIMARY retrieval tool for when users reference something built/deployed. Handles natural "
440
+ "language like 'the backend', 'that script', 'localhost something'."),
441
+ (handle_artifact_update, "nexo_artifact_update",
442
+ "Update an existing artifact. Only non-empty fields are changed."),
443
+ (handle_artifact_learn_alias, "nexo_artifact_learn_alias",
444
+ "Learn a new alias from user language. Call when the user refers to an artifact with "
445
+ "an unregistered term (e.g., 'backend' for the NEXO Brain Dashboard)."),
446
+ (handle_artifact_list, "nexo_artifact_list",
447
+ "List all registered artifacts, optionally filtered by kind, state, or recency."),
448
+ (handle_artifact_delete, "nexo_artifact_delete",
449
+ "Delete an artifact from the registry."),
450
+ ]
@@ -30,7 +30,7 @@ def handle_cognitive_retrieve(
30
30
  min_score: Minimum cosine similarity score (default 0.5)
31
31
  stores: Which store to search — "both", "stm", or "ltm" (default "both")
32
32
  source_type: Filter by source type e.g. "change", "learning", "diary" (default: all)
33
- domain: Filter by domain e.g. "wazion", "shopify" (default: all)
33
+ domain: Filter by domain e.g. "my-project", "shopify" (default: all)
34
34
  include_archived: If True, also search archived memories (default False)
35
35
  use_hyde: If True, use HyDE query expansion — embeds 3-5 query variants and searches with centroid. Better recall for conceptual queries. (default False)
36
36
  spreading_depth: If >0, boost co-activated neighbors (memories frequently retrieved together). 1=direct neighbors only. (default 0)
@@ -222,13 +222,13 @@ def handle_cognitive_metrics(days: int = 7) -> str:
222
222
 
223
223
 
224
224
  def handle_cognitive_sentiment(text: str) -> str:
225
- """Detect Francisco's sentiment from his text. Returns mood, intensity, and guidance.
225
+ """Detect the user's sentiment from his text. Returns mood, intensity, and guidance.
226
226
 
227
- Call this with Francisco's recent message to adapt NEXO's tone and behavior.
227
+ Call this with the user's recent message to adapt NEXO's tone and behavior.
228
228
  Also logs the sentiment for historical tracking.
229
229
 
230
230
  Args:
231
- text: Francisco's recent message or instruction
231
+ text: The user's recent message or instruction
232
232
  """
233
233
  result = cognitive.log_sentiment(text)
234
234
  trust = cognitive.get_trust_score()
@@ -294,8 +294,8 @@ def handle_cognitive_trust(event: str = '', context: str = '', delta: float = No
294
294
  def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
295
295
  """Detect cognitive dissonance: find established memories that conflict with a new instruction.
296
296
 
297
- Use BEFORE applying a new preference or rule from Francisco that might contradict
298
- existing knowledge. If conflicts found, verbalize them and ask Francisco to resolve.
297
+ Use BEFORE applying a new preference or rule from the user that might contradict
298
+ existing knowledge. If conflicts found, verbalize them and ask the user to resolve.
299
299
 
300
300
  Args:
301
301
  instruction: The new instruction or preference to check against LTM
@@ -328,7 +328,7 @@ def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
328
328
  lines.append("")
329
329
 
330
330
  lines.append("RESOLVE with nexo_cognitive_resolve, or use force=True to skip:")
331
- lines.append(" - 'paradigm_shift': Francisco changed his mind permanently.")
331
+ lines.append(" - 'paradigm_shift': The user changed their mind permanently.")
332
332
  lines.append(" - 'exception': One-time override. Old memory stays.")
333
333
  lines.append(" - 'override': Old memory was wrong.")
334
334
 
@@ -336,7 +336,7 @@ def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
336
336
 
337
337
 
338
338
  def handle_cognitive_resolve(memory_id: int, resolution: str, context: str = '') -> str:
339
- """Resolve a cognitive dissonance by applying Francisco's decision.
339
+ """Resolve a cognitive dissonance by applying the user's decision.
340
340
 
341
341
  Args:
342
342
  memory_id: The LTM memory ID from the dissonance detection
@@ -546,7 +546,7 @@ TOOLS = [
546
546
  (handle_cognitive_metrics, "nexo_cognitive_metrics", "Performance metrics: retrieval relevance %, repeat error rate, multilingual recommendation (spec section 9)"),
547
547
  (handle_cognitive_dissonance, "nexo_cognitive_dissonance", "Detect conflicts between a new instruction and established LTM memories. force=True to skip discussion."),
548
548
  (handle_cognitive_resolve, "nexo_cognitive_resolve", "Resolve a cognitive dissonance: paradigm_shift, exception, or override."),
549
- (handle_cognitive_sentiment, "nexo_cognitive_sentiment", "Detect Francisco's sentiment and get tone guidance. Also logs for tracking."),
549
+ (handle_cognitive_sentiment, "nexo_cognitive_sentiment", "Detect the user's sentiment and get tone guidance. Also logs for tracking."),
550
550
  (handle_cognitive_trust, "nexo_cognitive_trust", "View or adjust trust score (0-100). Without args: view. With event: adjust."),
551
551
  (handle_cognitive_pin, "nexo_cognitive_pin", "Pin a memory — never decays, boosted +0.2 in search results."),
552
552
  (handle_cognitive_snooze, "nexo_cognitive_snooze", "Snooze a memory — hidden from searches until a date, then auto-restores."),
@@ -27,7 +27,7 @@ def handle_decision_log(domain: str, decision: str, alternatives: str = '',
27
27
  """Log a non-trivial decision with reasoning context.
28
28
 
29
29
  Args:
30
- domain: Area (ads, shopify, server, wazion, nexo, project, other)
30
+ domain: Area (ads, shopify, server, my-project, nexo, project, other)
31
31
  decision: What was decided
32
32
  alternatives: JSON array or text of options considered and why discarded
33
33
  based_on: Data, metrics, or observations that informed this decision
@@ -35,7 +35,7 @@ def handle_decision_log(domain: str, decision: str, alternatives: str = '',
35
35
  context_ref: Related followup/reminder ID (e.g., NF-ADS1, R71)
36
36
  session_id: Current session ID (auto-filled if empty)
37
37
  """
38
- valid_domains = {'ads', 'shopify', 'server', 'wazion', 'nexo', 'project', 'other'}
38
+ valid_domains = {'ads', 'shopify', 'server', 'my-project', 'nexo', 'project', 'other'}
39
39
  if domain not in valid_domains:
40
40
  return f"ERROR: domain debe ser uno de: {', '.join(sorted(valid_domains))}"
41
41
  if confidence not in ('high', 'medium', 'low'):
@@ -92,10 +92,10 @@ def handle_decision_search(query: str = '', domain: str = '', days: int = 30) ->
92
92
 
93
93
  Args:
94
94
  query: Text to search in decision, alternatives, based_on, outcome
95
- domain: Filter by area (ads, shopify, server, wazion, nexo, project, other)
95
+ domain: Filter by area (ads, shopify, server, my-project, nexo, project, other)
96
96
  days: Look back N days (default 30)
97
97
  """
98
- valid_domains = {'ads', 'shopify', 'server', 'wazion', 'nexo', 'project', 'other'}
98
+ valid_domains = {'ads', 'shopify', 'server', 'my-project', 'nexo', 'project', 'other'}
99
99
  if domain and domain not in valid_domains:
100
100
  return f"ERROR: domain debe ser uno de: {', '.join(sorted(valid_domains))}"
101
101
  results = search_decisions(query, domain, days)
@@ -103,7 +103,7 @@ def handle_decision_search(query: str = '', domain: str = '', days: int = 30) ->
103
103
  scope = f"'{query}'" if query else domain or 'todas'
104
104
  return f"No decisions found for {scope} in {days} days."
105
105
 
106
- lines = [f"DECISIONES ({len(results)}):"]
106
+ lines = [f"DECISIONS ({len(results)}):"]
107
107
  for d in results:
108
108
  conf = d.get('confidence', '?')
109
109
  outcome_str = f" → {d['outcome'][:50]}" if d.get('outcome') else ""
@@ -265,7 +265,7 @@ def handle_change_log(files: str, what_changed: str, why: str,
265
265
  triggered_by: str = '', affects: str = '',
266
266
  risks: str = '', verify: str = '',
267
267
  commit_ref: str = '', session_id: str = '') -> str:
268
- """Log a code/config change with full context. OBLIGATORIO after every edit to production code.
268
+ """Log a code/config change with full context. MANDATORY after every edit to production code.
269
269
 
270
270
  Args:
271
271
  files: File path(s) modified (comma-separated if multiple)
@@ -279,7 +279,7 @@ def handle_change_log(files: str, what_changed: str, why: str,
279
279
  session_id: Current session ID
280
280
  """
281
281
  if not files or not what_changed or not why:
282
- return "ERROR: files, what_changed, y why son obligatorios"
282
+ return "ERROR: files, what_changed, and why are required"
283
283
  sid = session_id or 'unknown'
284
284
  result = log_change(sid, files, what_changed, why, triggered_by, affects, risks, verify, commit_ref)
285
285
  if "error" in result:
@@ -422,7 +422,7 @@ def handle_diary_archive_search(
422
422
 
423
423
  Args:
424
424
  query: Text to search in diary content
425
- domain: Filter by project domain (e.g. 'wazion', 'my-store')
425
+ domain: Filter by project domain (e.g. 'my-project', 'my-store')
426
426
  year: Filter by year (e.g. 2026)
427
427
  month: Filter by month (1-12), requires year
428
428
  limit: Max results (default 20)
@@ -77,10 +77,10 @@ def handle_evolution_approve(log_id: int, notes: str = '') -> str:
77
77
 
78
78
  Args:
79
79
  log_id: Evolution log entry ID to approve
80
- notes: Optional notes from Francisco
80
+ notes: Optional notes from the owner
81
81
  """
82
82
  update_evolution_log_status(log_id, "accepted",
83
- test_result=f"Approved by Francisco. {notes}".strip())
83
+ test_result=f"Approved by the owner. {notes}".strip())
84
84
  return f"Proposal #{log_id} APPROVED. Will be applied in next Evolution cycle."
85
85
 
86
86
 
@@ -92,7 +92,7 @@ def handle_evolution_reject(log_id: int, reason: str = '') -> str:
92
92
  reason: Why this proposal was rejected
93
93
  """
94
94
  update_evolution_log_status(log_id, "rejected",
95
- test_result=f"Rejected: {reason}" if reason else "Rejected by Francisco")
95
+ test_result=f"Rejected: {reason}" if reason else "Rejected by the owner")
96
96
  return f"Proposal #{log_id} REJECTED. Reason: {reason or 'no reason given'}"
97
97
 
98
98
 
@@ -104,7 +104,7 @@ TOOLS = [
104
104
  (handle_evolution_propose, "nexo_evolution_propose",
105
105
  "Manually trigger an evolution analysis outside weekly schedule"),
106
106
  (handle_evolution_approve, "nexo_evolution_approve",
107
- "Approve a pending Evolution proposal (Francisco only)"),
107
+ "Approve a pending Evolution proposal (owner only)"),
108
108
  (handle_evolution_reject, "nexo_evolution_reject",
109
109
  "Reject a pending Evolution proposal with reason"),
110
110
  ]
@@ -59,7 +59,7 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
59
59
 
60
60
  Args:
61
61
  files: Comma-separated file paths about to be edited
62
- area: System area (wazion, shopify, infrastructure, nexo-ops, etc.)
62
+ area: System area (my-project, shopify, infrastructure, nexo-ops, etc.)
63
63
  include_schemas: Include DB table schemas if files touch database code (true/false)
64
64
  """
65
65
  conn = get_db()
@@ -174,6 +174,23 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
174
174
  "reason": "prohibition_keyword"
175
175
  })
176
176
 
177
+ # 5b. Behavioral rules — when called without files (session-level check)
178
+ if not file_list:
179
+ behavioral = conn.execute(
180
+ """SELECT l.id, l.title, l.category, COUNT(e.id) as violations
181
+ FROM learnings l
182
+ LEFT JOIN error_repetitions e ON e.original_learning_id = l.id
183
+ WHERE l.category = 'nexo-ops' AND l.status = 'active'
184
+ GROUP BY l.id
185
+ ORDER BY violations DESC, l.created_at DESC
186
+ LIMIT 5"""
187
+ ).fetchall()
188
+ if behavioral:
189
+ result["behavioral_rules"] = [
190
+ {"id": r["id"], "rule": r["title"], "violations": r["violations"]}
191
+ for r in behavioral
192
+ ]
193
+
177
194
  # 6. Area repetition rate
178
195
  if area:
179
196
  total_area = conn.execute(
@@ -266,6 +283,13 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
266
283
  lines.append(f" #{l['id']} [{l['category']}] {l['rule']}")
267
284
  lines.append("")
268
285
 
286
+ if result.get("behavioral_rules"):
287
+ lines.append("SESSION BEHAVIORAL RULES (top 5 most-violated):")
288
+ for r in result["behavioral_rules"]:
289
+ v = f" ({r['violations']}x violated)" if r["violations"] > 0 else ""
290
+ lines.append(f" #{r['id']} {r['rule']}{v}")
291
+ lines.append("")
292
+
269
293
  if result["universal_rules"]:
270
294
  lines.append(f"UNIVERSAL RULES ({len(result['universal_rules'])}):")
271
295
  for r in result["universal_rules"][:10]:
@@ -456,7 +480,7 @@ def handle_guard_cross_check(findings: list, area: str = "") -> str:
456
480
 
457
481
  Args:
458
482
  findings: List of audit finding strings to cross-check
459
- area: System area to narrow the learning search (wazion, shopify, etc.)
483
+ area: System area to narrow the learning search (my-project, shopify, etc.)
460
484
  """
461
485
  # Common English/Spanish stopwords to skip during keyword extraction
462
486
  STOPWORDS = {