nexo-brain 7.23.13 → 7.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +15 -11
  3. package/bin/nexo-brain.js +42 -235
  4. package/package.json +1 -1
  5. package/src/auto_update.py +30 -0
  6. package/src/automation_supervisor.py +1 -1
  7. package/src/cli.py +255 -9
  8. package/src/cognitive_control_observatory.py +224 -0
  9. package/src/crons/manifest.json +13 -0
  10. package/src/dashboard/app.py +26 -9
  11. package/src/db/__init__.py +2 -0
  12. package/src/db/_fts.py +38 -8
  13. package/src/db/_learnings.py +1 -1
  14. package/src/db/_memory_v2.py +107 -1
  15. package/src/db/_protocol.py +2 -2
  16. package/src/db/_reminders.py +132 -4
  17. package/src/db/_schema.py +48 -2
  18. package/src/doctor/providers/runtime.py +69 -0
  19. package/src/events_bus.py +4 -5
  20. package/src/learning_resolver.py +419 -0
  21. package/src/lifecycle_events.py +9 -9
  22. package/src/local_context/api.py +67 -5
  23. package/src/local_context/usage_events.py +24 -0
  24. package/src/memory_fabric.py +536 -0
  25. package/src/memory_observation_processor.py +28 -0
  26. package/src/memory_retrieval.py +5 -5
  27. package/src/operator_language.py +2 -0
  28. package/src/plugins/backup.py +1 -1
  29. package/src/plugins/cortex.py +21 -21
  30. package/src/plugins/episodic_memory.py +11 -11
  31. package/src/plugins/goal_engine.py +3 -3
  32. package/src/plugins/personal_scripts.py +75 -0
  33. package/src/plugins/protocol.py +10 -1
  34. package/src/pre_answer_router.py +120 -3
  35. package/src/r_catalog.py +4 -5
  36. package/src/saved_not_used_audit.py +31 -31
  37. package/src/script_registry.py +444 -1
  38. package/src/scripts/deep-sleep/apply_findings.py +79 -17
  39. package/src/scripts/nexo-backup.sh +30 -0
  40. package/src/scripts/nexo-daily-self-audit.py +46 -13
  41. package/src/scripts/nexo-email-migrate-config.py +2 -2
  42. package/src/scripts/nexo-email-monitor.py +19 -19
  43. package/src/scripts/nexo-followup-hygiene.py +40 -8
  44. package/src/scripts/nexo-followup-runner.py +31 -31
  45. package/src/scripts/nexo-inbox-hook.sh +1 -1
  46. package/src/scripts/nexo-learning-validator.py +24 -3
  47. package/src/scripts/nexo-memory-fabric.py +45 -0
  48. package/src/server.py +73 -1
  49. package/src/system_catalog.py +31 -31
  50. package/src/tools_learnings.py +96 -65
  51. package/src/tools_memory_v2.py +2 -2
  52. package/src/tools_sessions.py +25 -7
  53. package/src/tools_transcripts.py +50 -8
  54. package/src/transcript_index.py +105 -2
  55. package/src/transcript_utils.py +65 -13
  56. package/templates/core-prompts/postmortem-consolidator.md +3 -3
  57. package/templates/core-prompts/r17-promise-debt-injection.md +1 -1
  58. package/templates/core-prompts/server-mcp-instructions.md +6 -6
  59. package/tool-enforcement-map.json +143 -13
@@ -73,7 +73,7 @@ RESULTS_FILE = data_dir() / "followup-runner-results.json"
73
73
  CLI_TIMEOUT = AUTOMATION_SUBPROCESS_TIMEOUT
74
74
  LOCK_FILE = LOG_DIR / "followup-runner.lock"
75
75
  MAX_FOLLOWUPS_PER_RUN = 5 # Focus: Opus can actually execute 5, not 30
76
- COOLDOWN_DAYS = 3 # Don't retry needs_decision/blocked for 3 days
76
+ COOLDOWN_DAYS = 3 # Don't retry waiting_user/stale_review/blocked for 3 days
77
77
  STALE_FOLLOWUP_TRIAGE_DAYS = 14
78
78
  MAX_STALE_TRIAGE_PER_RUN = 8
79
79
  MAX_NEEDS_OPERATOR_BRIEFING = 12
@@ -134,7 +134,7 @@ def _history_has_recent_movement(history, *, days: int = STALE_FOLLOWUP_TRIAGE_D
134
134
 
135
135
  def _is_stale_followup_for_triage(followup: dict) -> bool:
136
136
  status = str(followup.get("status") or "").strip().lower()
137
- if status in {"needs_decision", "waiting_user", "blocked", "waiting"}:
137
+ if status in {"needs_decision", "waiting_user", "blocked", "waiting", "stale_review"}:
138
138
  return False
139
139
  if _followup_days_overdue(str(followup.get("date") or "")) < STALE_FOLLOWUP_TRIAGE_DAYS:
140
140
  return False
@@ -148,7 +148,7 @@ def _is_in_cooldown(fu_id: str, state: dict) -> bool:
148
148
  if not last:
149
149
  return False
150
150
  last_status = last.get("status", "")
151
- if last_status not in ("needs_decision", "blocked"):
151
+ if last_status not in ("needs_decision", "waiting_user", "stale_review", "blocked"):
152
152
  return False
153
153
  last_date_str = last.get("date", "")
154
154
  if not last_date_str:
@@ -298,17 +298,17 @@ def get_all_active_followups(state: dict) -> dict:
298
298
  conn = sqlite3.connect(str(NEXO_DB))
299
299
  conn.row_factory = sqlite3.Row
300
300
  try:
301
- rows = conn.execute(
302
- "SELECT id, description, date, reasoning, verification, priority, recurrence, status, owner, updated_at "
303
- "FROM followups WHERE status NOT LIKE 'COMPLETED%' "
304
- "AND UPPER(COALESCE(status, '')) NOT IN ('BLOCKED', 'ARCHIVED', 'DELETED', 'WAITING', 'DONE') "
305
- "AND description NOT LIKE '[Abandoned]%' "
306
- "ORDER BY "
307
- " CASE priority "
308
- " WHEN 'critical' THEN 1 WHEN 'high' THEN 2 "
309
- " WHEN 'medium' THEN 3 WHEN 'low' THEN 4 ELSE 5 END, "
310
- " date ASC"
311
- ).fetchall()
301
+ snapshot = nexo_db.followup_lifecycle_snapshot(limit=5000)
302
+ rows = [
303
+ item for item in (snapshot.get("lanes") or {}).get("active", [])
304
+ if not str(item.get("description") or "").startswith("[Abandoned]")
305
+ ]
306
+ rows.sort(
307
+ key=lambda item: (
308
+ {"critical": 1, "high": 2, "medium": 3, "low": 4}.get(str(item.get("priority") or "medium"), 5),
309
+ str(item.get("date") or "9999-12-31"),
310
+ )
311
+ )
312
312
 
313
313
  result = {"actionable": [], "needs_operator": [], "future": [], "backlog": [], "cooled_down": [], "stale_triage": []}
314
314
  undated_triage_budget = 2
@@ -429,7 +429,7 @@ def complete_followup_if_needed(fu_id: str, result_summary: str = ""):
429
429
  return
430
430
  try:
431
431
  nexo_db.complete_followup(fu_id, result_summary)
432
- log(f" {fu_id}: marcado completado por el runner")
432
+ log(f" {fu_id}: marked completed por el runner")
433
433
  except Exception as exc:
434
434
  log(f" {fu_id}: failed to mark followup as completed ({exc})")
435
435
 
@@ -488,7 +488,7 @@ def attention_reminder_id(fu_id: str) -> str:
488
488
 
489
489
 
490
490
  def attention_reminder_category(status: str) -> str:
491
- return "decisions" if status == "needs_decision" else "waiting"
491
+ return "decisions" if status in {"needs_decision", "waiting_user", "stale_review"} else "waiting"
492
492
 
493
493
 
494
494
  def attention_reminder_description(
@@ -502,14 +502,14 @@ def attention_reminder_description(
502
502
  detail = " ".join((summary or "").split())
503
503
  if not detail:
504
504
  detail = (
505
- "El runner no puede cerrar este punto sin intervención del operador."
505
+ "The runner cannot close this item without operator input."
506
506
  if _uses_spanish(operator_language)
507
507
  else "The runner cannot close this item without operator input."
508
508
  )
509
509
  description = f"{fu_id}: {detail}"
510
510
  opts_text = render_options(options)
511
511
  if opts_text:
512
- description += f" {'Opciones' if _uses_spanish(operator_language) else 'Options'}: {opts_text}"
512
+ description += f" {'Options' if _uses_spanish(operator_language) else 'Options'}: {opts_text}"
513
513
  return description[:480]
514
514
 
515
515
 
@@ -548,7 +548,7 @@ def upsert_attention_reminder(
548
548
  log(f" {fu_id}: failed to update reminder {reminder_id} ({result['error']})")
549
549
  return
550
550
  nexo_db.add_reminder_note(reminder_id, description, actor="followup-runner")
551
- log(f" {fu_id}: reminder {reminder_id} actualizado para orchestrator")
551
+ log(f" {fu_id}: reminder {reminder_id} updated for orchestrator")
552
552
  return
553
553
 
554
554
  result = nexo_db.create_reminder(
@@ -565,7 +565,7 @@ def upsert_attention_reminder(
565
565
  f"source_followup={fu_id} status={status}",
566
566
  actor="followup-runner",
567
567
  )
568
- log(f" {fu_id}: reminder {reminder_id} creado para orchestrator")
568
+ log(f" {fu_id}: reminder {reminder_id} created for orchestrator")
569
569
 
570
570
 
571
571
  def resolve_attention_reminder(fu_id: str, *, resolution: str = ""):
@@ -579,14 +579,14 @@ def resolve_attention_reminder(fu_id: str, *, resolution: str = ""):
579
579
  if resolution:
580
580
  nexo_db.add_reminder_note(
581
581
  reminder_id,
582
- f"Resuelto desde {fu_id}: {resolution[:300]}",
582
+ f"Resolved from {fu_id}: {resolution[:300]}",
583
583
  actor="followup-runner",
584
584
  )
585
585
  result = nexo_db.complete_reminder(reminder_id)
586
586
  if result.get("error"):
587
587
  log(f" {fu_id}: failed to complete reminder {reminder_id} ({result['error']})")
588
588
  return
589
- log(f" {fu_id}: reminder {reminder_id} marcado completado")
589
+ log(f" {fu_id}: reminder {reminder_id} marked completed")
590
590
 
591
591
 
592
592
  def defer_followup_after_attention(
@@ -602,7 +602,7 @@ def defer_followup_after_attention(
602
602
  details = summary.strip()
603
603
  opts_text = render_options(options)
604
604
  if opts_text:
605
- details = f"{details}\nOpciones: {opts_text}"
605
+ details = f"{details}\nOptions: {opts_text}"
606
606
  if details:
607
607
  note_result = nexo_db.add_followup_note(
608
608
  fu_id,
@@ -690,10 +690,10 @@ def get_recent_activity(hours: int = 24) -> str:
690
690
 
691
691
  # Recent followup notes from the runner
692
692
  notes = conn.execute(
693
- "SELECT followup_id, note, created_at FROM followup_history "
694
- "WHERE actor='followup-runner' AND created_at >= datetime('now', ?)"
693
+ "SELECT item_id AS followup_id, note, created_at FROM item_history "
694
+ "WHERE item_type='followup' AND actor='followup-runner' AND created_at >= ? "
695
695
  "ORDER BY created_at DESC LIMIT 10",
696
- (f"-{hours} hours",)
696
+ ((datetime.now() - timedelta(hours=hours)).timestamp(),),
697
697
  ).fetchall()
698
698
  if notes:
699
699
  lines.append("\nFOLLOWUP NOTES WRITTEN (last 24h):")
@@ -821,7 +821,7 @@ def main():
821
821
  update_followup_fields(
822
822
  fid,
823
823
  date_value=date.today().isoformat(),
824
- status="needs_decision",
824
+ status="stale_review",
825
825
  history_event="stale_triage",
826
826
  history_note=summary,
827
827
  )
@@ -829,10 +829,10 @@ def main():
829
829
  fid,
830
830
  summary=summary,
831
831
  options={"a": "close obsolete", "b": "reschedule", "c": "convert to next action"},
832
- status="needs_decision",
832
+ status="stale_review",
833
833
  operator_language=_operator_language(),
834
834
  )
835
- record_attempt(state, fid, "needs_decision")
835
+ record_attempt(state, fid, "stale_review")
836
836
 
837
837
  results = []
838
838
 
@@ -914,7 +914,7 @@ def main():
914
914
  advance_recurrent(fid, recurrence, summary)
915
915
  resolve_attention_reminder(fid, resolution=summary)
916
916
  record_attempt(state, fid, "checked")
917
- elif r["status"] in ("needs_decision", "blocked"):
917
+ elif r["status"] in ("needs_decision", "waiting_user", "stale_review", "blocked"):
918
918
  defer_followup_after_attention(
919
919
  fid,
920
920
  summary=summary,
@@ -929,7 +929,7 @@ def main():
929
929
 
930
930
  total = len(all_actionable) + len(groups["needs_operator"]) + len(groups["future"]) + len(groups["backlog"]) + len(stale_triage)
931
931
  attention_handed_off = any(
932
- r.get("needs_attention") or r["status"] in ("needs_decision", "blocked")
932
+ r.get("needs_attention") or r["status"] in ("needs_decision", "waiting_user", "stale_review", "blocked")
933
933
  for r in results
934
934
  )
935
935
  if total > 0 or results:
@@ -65,7 +65,7 @@ if [ -n "$MESSAGES" ]; then
65
65
  fi
66
66
 
67
67
  if [ -n "$QUESTIONS" ]; then
68
- echo " ⚠ PREGUNTAS de otra terminal — responder con nexo_answer:"
68
+ echo " ⚠ QUESTIONS from another terminal — answer with nexo_answer:"
69
69
  echo "$QUESTIONS" | while IFS='|' read -r qid from question; do
70
70
  echo " Q[$qid] de [$from]: $question"
71
71
  done
@@ -62,6 +62,7 @@ DB_PATH = data_dir() / "nexo.db"
62
62
 
63
63
  from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
64
64
  from core_prompts import render_core_prompt
65
+ from learning_resolver import resolve_learning_candidate
65
66
 
66
67
  try:
67
68
  from client_preferences import resolve_user_model as _resolve_user_model
@@ -79,12 +80,12 @@ def get_all_learnings(category: str | None = None) -> list[dict]:
79
80
  conn.row_factory = sqlite3.Row
80
81
  if category:
81
82
  rows = conn.execute(
82
- "SELECT id, category, title, content FROM learnings WHERE category = ?",
83
+ "SELECT id, category, title, content FROM learnings WHERE category = ? AND COALESCE(status, 'active') = 'active'",
83
84
  (category,),
84
85
  ).fetchall()
85
86
  else:
86
87
  rows = conn.execute(
87
- "SELECT id, category, title, content FROM learnings"
88
+ "SELECT id, category, title, content FROM learnings WHERE COALESCE(status, 'active') = 'active'"
88
89
  ).fetchall()
89
90
  conn.close()
90
91
  return [dict(r) for r in rows]
@@ -131,6 +132,12 @@ def validate_finding(finding: str, category: str | None = None) -> dict:
131
132
  "recommendation": str
132
133
  }
133
134
  """
135
+ resolver = resolve_learning_candidate(
136
+ category=category or "process",
137
+ title=(finding or "Finding").strip()[:120] or "Finding",
138
+ content=finding or "",
139
+ source_authority="code_test_evidence" if any(token in (finding or "").lower() for token in ("test", "traceback", "stack", "verified", "evidence")) else "inference",
140
+ )
134
141
  learnings = get_all_learnings(category)
135
142
 
136
143
  if not learnings:
@@ -139,6 +146,7 @@ def validate_finding(finding: str, category: str | None = None) -> dict:
139
146
  "confidence": 0,
140
147
  "matching_learnings": [],
141
148
  "recommendation": "No learnings in DB — finding is new by default",
149
+ "resolver": resolver,
142
150
  }
143
151
 
144
152
  learnings_ref = [
@@ -168,13 +176,26 @@ def validate_finding(finding: str, category: str | None = None) -> dict:
168
176
  )
169
177
  parsed = _extract_json(result.stdout)
170
178
  if result.returncode == 0 and parsed:
179
+ parsed["resolver"] = resolver
171
180
  return parsed
172
181
  except AutomationBackendUnavailableError:
173
182
  pass
174
183
  except Exception:
175
184
  pass
176
185
 
177
- return _mechanical_validate(finding, learnings)
186
+ result = _mechanical_validate(finding, learnings)
187
+ result["resolver"] = resolver
188
+ if resolver.get("action") in {"merge", "supersede", "conflict_review"}:
189
+ result["known"] = True
190
+ result["confidence"] = max(float(result.get("confidence") or 0), float(resolver.get("similarity") or 0.7))
191
+ result["matching_learnings"] = result.get("matching_learnings") or [{
192
+ "id": resolver.get("target_id"),
193
+ "category": category or "process",
194
+ "title": resolver.get("target_title"),
195
+ "similarity": resolver.get("similarity"),
196
+ }]
197
+ result["recommendation"] = f"Resolver action: {resolver.get('action')} ({resolver.get('reason')})"
198
+ return result
178
199
 
179
200
 
180
201
  def _mechanical_validate(finding: str, learnings: list[dict]) -> dict:
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env python3
2
+ # nexo: name=memory-fabric
3
+ # nexo: description=Refresh transcript search, historical backup diaries, and graph links.
4
+ # nexo: runtime=python
5
+ # nexo: cron_id=memory-fabric
6
+ # nexo: schedule=02:35
7
+ # nexo: recovery_policy=catchup
8
+ # nexo: run_on_boot=true
9
+ # nexo: run_on_wake=true
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+
17
+
18
+ RUNTIME_ROOT = Path(__file__).resolve().parents[1]
19
+ if str(RUNTIME_ROOT) not in sys.path:
20
+ sys.path.insert(0, str(RUNTIME_ROOT))
21
+
22
+
23
+ def _int_env(name: str, default: int) -> int:
24
+ raw = os.environ.get(name, "").strip()
25
+ if not raw:
26
+ return default
27
+ try:
28
+ return max(1, int(raw))
29
+ except ValueError:
30
+ return default
31
+
32
+
33
+ def main() -> int:
34
+ import memory_fabric
35
+
36
+ result = memory_fabric.repair_memory_fabric(
37
+ transcript_limit=_int_env("NEXO_MEMORY_FABRIC_TRANSCRIPT_LIMIT", 1000),
38
+ backup_limit=_int_env("NEXO_MEMORY_FABRIC_BACKUP_LIMIT", 10000),
39
+ )
40
+ print(json.dumps(result, ensure_ascii=False, sort_keys=True))
41
+ return 0
42
+
43
+
44
+ if __name__ == "__main__":
45
+ raise SystemExit(main())
package/src/server.py CHANGED
@@ -79,7 +79,7 @@ from tools_reminders_crud import (
79
79
  from tools_learnings import (
80
80
  handle_learning_add, handle_learning_search,
81
81
  handle_learning_update, handle_learning_delete, handle_learning_list,
82
- handle_learning_quality,
82
+ handle_learning_quality, handle_learning_resolve_candidate,
83
83
  )
84
84
  from tools_credentials import (
85
85
  handle_credential_get, handle_credential_create,
@@ -1089,6 +1089,19 @@ def nexo_context_router(query: str, intent: str = "answer", limit: int = 4, curr
1089
1089
  return json.dumps(result, ensure_ascii=False)
1090
1090
 
1091
1091
 
1092
+ @mcp.tool
1093
+ def nexo_cognitive_control_observatory(window_seconds: int = 86400) -> str:
1094
+ """Read-only metrics for Local Context, learnings, followups and intraday memory."""
1095
+ from cognitive_control_observatory import build_cognitive_control_observatory
1096
+
1097
+ return json.dumps(
1098
+ build_cognitive_control_observatory(window_seconds=window_seconds),
1099
+ ensure_ascii=False,
1100
+ indent=2,
1101
+ sort_keys=True,
1102
+ )
1103
+
1104
+
1092
1105
  @mcp.tool
1093
1106
  def nexo_local_asset_get(asset_id: str) -> str:
1094
1107
  """Return one indexed local asset by asset id."""
@@ -1386,6 +1399,23 @@ def nexo_memory_observation_process(limit: int = 25, backfill_limit: int = 100,
1386
1399
  )
1387
1400
 
1388
1401
 
1402
+ @mcp.tool
1403
+ def nexo_intraday_memory_cycle(limit: int = 20, backfill_limit: int = 20, pending_sla_seconds: int = 3600) -> str:
1404
+ """Run a low-limit daytime memory observation cycle that only publishes evidence-backed intraday facts."""
1405
+ from memory_observation_processor import process_intraday_cycle
1406
+
1407
+ return json.dumps(
1408
+ process_intraday_cycle(
1409
+ process_limit=limit,
1410
+ backfill_limit=backfill_limit,
1411
+ pending_sla_seconds=pending_sla_seconds,
1412
+ ),
1413
+ ensure_ascii=False,
1414
+ indent=2,
1415
+ sort_keys=True,
1416
+ )
1417
+
1418
+
1389
1419
  @mcp.tool
1390
1420
  def nexo_memory_observation_list(
1391
1421
  query: str = "",
@@ -1968,6 +1998,19 @@ def nexo_followup_get(id: str) -> str:
1968
1998
  return handle_followup_get(id)
1969
1999
 
1970
2000
 
2001
+ @mcp.tool
2002
+ def nexo_followup_lifecycle(limit: int = 500) -> str:
2003
+ """Return followups grouped by lifecycle lane for runner, dashboard and startup parity."""
2004
+ from db import followup_lifecycle_snapshot
2005
+
2006
+ return json.dumps(
2007
+ followup_lifecycle_snapshot(limit=limit),
2008
+ ensure_ascii=False,
2009
+ indent=2,
2010
+ sort_keys=True,
2011
+ )
2012
+
2013
+
1971
2014
  @mcp.tool
1972
2015
  def nexo_followup_update(
1973
2016
  id: str,
@@ -2066,6 +2109,7 @@ def nexo_learning_add(
2066
2109
  review_days: int = 30,
2067
2110
  priority: str = "medium",
2068
2111
  supersedes_id: int = 0,
2112
+ source_authority: str = "explicit_instruction",
2069
2113
  ) -> str:
2070
2114
  """Add a new learning (resolved error, pattern, gotcha).
2071
2115
 
@@ -2079,11 +2123,39 @@ def nexo_learning_add(
2079
2123
  review_days: Days until this learning should be reviewed again (default 30).
2080
2124
  priority: critical, high, medium, low (default: medium). Critical/high never decay below floor.
2081
2125
  supersedes_id: Existing learning ID this new canonical rule replaces (optional).
2126
+ source_authority: Authority tier for conflict resolution: francisco_correction, explicit_instruction, code_test_evidence, deep_sleep, inference.
2082
2127
  """
2083
2128
  return handle_learning_add(
2084
2129
  category, title, content, reasoning,
2085
2130
  prevention=prevention, applies_to=applies_to,
2086
2131
  review_days=review_days, priority=priority, supersedes_id=supersedes_id,
2132
+ source_authority=source_authority,
2133
+ )
2134
+
2135
+
2136
+ @mcp.tool
2137
+ def nexo_learning_resolve_candidate(
2138
+ category: str,
2139
+ title: str,
2140
+ content: str,
2141
+ reasoning: str = "",
2142
+ prevention: str = "",
2143
+ applies_to: str = "",
2144
+ priority: str = "medium",
2145
+ supersedes_id: int = 0,
2146
+ source_authority: str = "inference",
2147
+ ) -> str:
2148
+ """Dry-run the canonical learning resolver without creating or updating learnings."""
2149
+ return handle_learning_resolve_candidate(
2150
+ category=category,
2151
+ title=title,
2152
+ content=content,
2153
+ reasoning=reasoning,
2154
+ prevention=prevention,
2155
+ applies_to=applies_to,
2156
+ priority=priority,
2157
+ supersedes_id=supersedes_id,
2158
+ source_authority=source_authority,
2087
2159
  )
2088
2160
 
2089
2161
 
@@ -262,110 +262,110 @@ def _guide_for_tool(name: str) -> dict[str, list]:
262
262
  if name == "nexo_learning_add":
263
263
  return {
264
264
  "workflow": [
265
- "Usa `applies_to` si quieres que el guard recuerde este learning antes de tocar un archivo, directorio o patrón concreto.",
266
- "Usa `priority` (`critical`, `high`, `medium`, `low`) para marcar severidad operativa.",
265
+ "Use `applies_to` if you want the pre-edit check to surface this learning before touching a concrete file, directory, or pattern.",
266
+ "Use `priority` (`critical`, `high`, `medium`, `low`) to mark operational severity.",
267
267
  ],
268
268
  "examples": [
269
269
  {
270
- "title": "Learning mínimo",
271
- "code": 'nexo_learning_add(category="shopify", title="Hacer pull antes de editar", content="Siempre sincronizar antes de editar el tema live.")',
270
+ "title": "Minimal learning",
271
+ "code": 'nexo_learning_add(category="shopify", title="Pull before editing", content="Always sync before editing the live theme.")',
272
272
  },
273
273
  {
274
- "title": "Learning ligado a archivo o patrón",
275
- "code": 'nexo_learning_add(category="recambios-bmw", title="Pull antes de editar theme", content="El admin puede tocar JSONs live.", applies_to="/abs/path/templates/product.json,templates/*.json,sections/*.liquid", prevention="Ejecutar `shopify theme pull` antes de editar.", priority="high")',
274
+ "title": "Learning linked to a file or pattern",
275
+ "code": 'nexo_learning_add(category="recambios-bmw", title="Pull before editing theme", content="The admin can touch live JSON files.", applies_to="/abs/path/templates/product.json,templates/*.json,sections/*.liquid", prevention="Run `shopify theme pull` before editing.", priority="high")',
276
276
  },
277
277
  ],
278
278
  "common_errors": [
279
- "Usar `severity` en vez de `priority`.",
280
- "Olvidar `title`, que es obligatorio.",
281
- "No poner `applies_to` cuando quieres que el warning salte antes de tocar archivos concretos.",
279
+ "Using `severity` instead of `priority`.",
280
+ "Omitting `title`, which is required.",
281
+ "Omitting `applies_to` when you want the warning to appear before touching concrete files.",
282
282
  ],
283
283
  }
284
284
  if name == "nexo_learning_update":
285
285
  return {
286
286
  "workflow": [
287
- "Úsalo para completar o endurecer un learning existente cuando descubres nuevos archivos afectados, mejor `prevention` o prioridad distinta.",
287
+ "Use it to complete or harden an existing learning when you discover newly affected files, a better `prevention`, or a different priority.",
288
288
  ],
289
289
  "examples": [
290
290
  {
291
- "title": "Añadir alcance a un learning existente",
292
- "code": 'nexo_learning_update(id=57, applies_to="/abs/path/file.py,src/plugins/*.py", prevention="Leer schema antes del primer uso", priority="high")',
291
+ "title": "Add scope to an existing learning",
292
+ "code": 'nexo_learning_update(id=57, applies_to="/abs/path/file.py,src/plugins/*.py", prevention="Read the schema before first use", priority="high")',
293
293
  },
294
294
  ],
295
295
  "common_errors": [
296
- "Intentar recrear el learning desde cero cuando basta con actualizar el existente.",
296
+ "Recreating the learning from scratch when updating the existing one is enough.",
297
297
  ],
298
298
  }
299
299
  if name == "nexo_reminder_get":
300
300
  return {
301
301
  "workflow": [
302
- "Devuelve el `READ_TOKEN` necesario para `update`, `delete`, `restore` y `note` sobre ese reminder.",
302
+ "Returns the `READ_TOKEN` required for `update`, `delete`, `restore`, and `note` on that reminder.",
303
303
  ],
304
304
  "examples": [
305
305
  {
306
- "title": "Leer reminder y obtener token",
306
+ "title": "Read reminder and get token",
307
307
  "code": 'nexo_reminder_get(id="R87")',
308
308
  },
309
309
  ],
310
310
  "common_errors": [
311
- "Intentar editar o borrar un reminder sin llamar antes a `nexo_reminder_get`.",
311
+ "Trying to edit or delete a reminder without calling `nexo_reminder_get` first.",
312
312
  ],
313
313
  }
314
314
  if name in {"nexo_reminder_update", "nexo_reminder_delete", "nexo_reminder_restore", "nexo_reminder_note"}:
315
315
  return {
316
316
  "workflow": [
317
- "Primero llama `nexo_reminder_get(id=\"R87\")` para obtener `READ_TOKEN`.",
318
- f"Luego reutiliza ese `READ_TOKEN` en `{name}(...)`.",
317
+ "First call `nexo_reminder_get(id=\"R87\")` to obtain `READ_TOKEN`.",
318
+ f"Then reuse that `READ_TOKEN` in `{name}(...)`.",
319
319
  ],
320
320
  "examples": [
321
321
  {
322
- "title": "1. Obtener token",
322
+ "title": "1. Get token",
323
323
  "code": 'nexo_reminder_get(id="R87")',
324
324
  },
325
325
  {
326
- "title": "2. Reutilizar READ_TOKEN",
326
+ "title": "2. Reuse READ_TOKEN",
327
327
  "code": f'{name}(id="R87", read_token="TOKEN")',
328
328
  },
329
329
  ],
330
330
  "common_errors": [
331
- "Llamar a esta tool sin `READ_TOKEN` válido.",
332
- "Usar un `READ_TOKEN` de otro reminder o de una lectura antigua.",
331
+ "Calling this tool without a valid `READ_TOKEN`.",
332
+ "Using a `READ_TOKEN` from another reminder or an older read.",
333
333
  ],
334
334
  }
335
335
  if name == "nexo_followup_get":
336
336
  return {
337
337
  "workflow": [
338
- "Devuelve el `READ_TOKEN` necesario para `update`, `delete`, `restore` y `note` sobre ese followup.",
338
+ "Returns the `READ_TOKEN` required for `update`, `delete`, `restore`, and `note` on that followup.",
339
339
  ],
340
340
  "examples": [
341
341
  {
342
- "title": "Leer followup y obtener token",
342
+ "title": "Read followup and get token",
343
343
  "code": 'nexo_followup_get(id="NF45")',
344
344
  },
345
345
  ],
346
346
  "common_errors": [
347
- "Intentar editar o borrar un followup sin llamar antes a `nexo_followup_get`.",
347
+ "Trying to edit or delete a followup without calling `nexo_followup_get` first.",
348
348
  ],
349
349
  }
350
350
  if name in {"nexo_followup_update", "nexo_followup_delete", "nexo_followup_restore", "nexo_followup_note"}:
351
351
  return {
352
352
  "workflow": [
353
- "Primero llama `nexo_followup_get(id=\"NF45\")` para obtener `READ_TOKEN`.",
354
- f"Luego reutiliza ese `READ_TOKEN` en `{name}(...)`.",
353
+ "First call `nexo_followup_get(id=\"NF45\")` to obtain `READ_TOKEN`.",
354
+ f"Then reuse that `READ_TOKEN` in `{name}(...)`.",
355
355
  ],
356
356
  "examples": [
357
357
  {
358
- "title": "1. Obtener token",
358
+ "title": "1. Get token",
359
359
  "code": 'nexo_followup_get(id="NF45")',
360
360
  },
361
361
  {
362
- "title": "2. Reutilizar READ_TOKEN",
362
+ "title": "2. Reuse READ_TOKEN",
363
363
  "code": f'{name}(id="NF45", read_token="TOKEN")',
364
364
  },
365
365
  ],
366
366
  "common_errors": [
367
- "Llamar a esta tool sin `READ_TOKEN` válido.",
368
- "Usar un `READ_TOKEN` de otro followup o de una lectura antigua.",
367
+ "Calling this tool without a valid `READ_TOKEN`.",
368
+ "Using a `READ_TOKEN` from another followup or an older read.",
369
369
  ],
370
370
  }
371
371
  return {}