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.
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/cognitive.cpython-314.pyc +0 -0
- package/src/__pycache__/db.cpython-314.pyc +0 -0
- package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
- package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
- package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/server.cpython-314.pyc +0 -0
- package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
- package/src/cognitive.py +63 -14
- package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
- package/src/dashboard/app.py +1 -1
- package/src/db.py +29 -13
- package/src/evolution_cycle.py +72 -94
- package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
- package/src/hooks/session-start.sh +5 -2
- package/src/hooks/session-stop.sh +79 -133
- package/src/knowledge_graph.py +3 -3
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
- package/src/plugins/artifact_registry.py +450 -0
- package/src/plugins/cognitive_memory.py +9 -9
- package/src/plugins/episodic_memory.py +8 -8
- package/src/plugins/evolution.py +4 -4
- package/src/plugins/guard.py +26 -2
- package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/nexo-brain-activation.sh +140 -0
- package/src/scripts/nexo-evolution-run.py +1 -1
- package/src/scripts/nexo-followup-hygiene.py +107 -0
- package/src/scripts/nexo-postmortem-consolidator.py +25 -25
- package/src/scripts/nexo-pre-commit.py +118 -0
- package/src/scripts/nexo-proactive-dashboard.py +342 -0
- package/src/scripts/nexo-runtime-preflight.py +270 -0
- package/src/scripts/nexo-send-email.py +25 -0
- package/src/scripts/nexo-send-reply.py +176 -0
- package/src/scripts/nexo-snapshot-restore.sh +34 -0
- package/src/scripts/nexo-watchdog-smoke.py +114 -0
- package/src/server.py +5 -5
- package/src/tools_coordination.py +3 -3
- package/src/tools_learnings.py +1 -1
- package/src/tools_menu.py +31 -49
- package/src/tools_reminders_crud.py +1 -1
- package/src/tools_sessions.py +12 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/migrate 2.py +0 -207
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# nexo-brain-activation.sh — NF24: Activación Espontánea
|
|
3
|
+
# Lee user_model.json, detecta shifts significativos y genera insights
|
|
4
|
+
# para el arranque de NEXO. Si no hay nada relevante: exit 0 sin output.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
BRAIN_DIR="~/.nexo/brain"
|
|
9
|
+
MODEL_FILE="$BRAIN_DIR/user_model.json"
|
|
10
|
+
SUMMARIES_DIR="$BRAIN_DIR/daily_summaries"
|
|
11
|
+
|
|
12
|
+
# --- Guardia: archivo imprescindible ---
|
|
13
|
+
if [[ ! -f "$MODEL_FILE" ]]; then
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# --- Python inline para parsear JSON y analizar ---
|
|
18
|
+
python3 - <<'PYEOF'
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
import os
|
|
22
|
+
import glob
|
|
23
|
+
from datetime import datetime, timedelta
|
|
24
|
+
|
|
25
|
+
BRAIN_DIR = os.path.expanduser("~/.nexo/brain")
|
|
26
|
+
MODEL_FILE = os.path.join(BRAIN_DIR, "user_model.json")
|
|
27
|
+
SUMMARIES_DIR = os.path.join(BRAIN_DIR, "daily_summaries")
|
|
28
|
+
|
|
29
|
+
# ── Cargar modelo ──────────────────────────────────────────────────────────
|
|
30
|
+
try:
|
|
31
|
+
with open(MODEL_FILE) as f:
|
|
32
|
+
model = json.load(f)
|
|
33
|
+
except Exception:
|
|
34
|
+
sys.exit(0)
|
|
35
|
+
|
|
36
|
+
insights = []
|
|
37
|
+
|
|
38
|
+
# ── 1. Analizar evolution_log — últimas 3-5 entradas ─────────────────────
|
|
39
|
+
evolution = model.get("evolution_log", [])
|
|
40
|
+
recent = evolution[-5:] if len(evolution) >= 5 else evolution
|
|
41
|
+
|
|
42
|
+
# Detectar entradas de los últimos 3 días
|
|
43
|
+
today = datetime.now().date()
|
|
44
|
+
cutoff = today - timedelta(days=3)
|
|
45
|
+
|
|
46
|
+
recent_obs = []
|
|
47
|
+
for entry in recent:
|
|
48
|
+
try:
|
|
49
|
+
entry_date = datetime.strptime(entry["date"], "%Y-%m-%d").date()
|
|
50
|
+
if entry_date >= cutoff:
|
|
51
|
+
recent_obs.append(entry)
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
if recent_obs:
|
|
56
|
+
for obs in recent_obs[-2:]: # max 2 most recent
|
|
57
|
+
insights.append(obs["observation"].strip())
|
|
58
|
+
|
|
59
|
+
# ── 2. Detectar cambios de traits >0.1 entre entradas (si hay histórico) ──
|
|
60
|
+
# El modelo actual solo tiene snapshot actual; si en el futuro hay histórico
|
|
61
|
+
# en evolution_log con trait deltas, se puede ampliar aquí.
|
|
62
|
+
# Detect the most extreme trait as the dominant identity signal.
|
|
63
|
+
traits = model.get("traits", {})
|
|
64
|
+
if traits:
|
|
65
|
+
dominant = max(traits, key=lambda k: traits[k])
|
|
66
|
+
dominant_val = traits[dominant]
|
|
67
|
+
weakest = min(traits, key=lambda k: traits[k])
|
|
68
|
+
weakest_val = traits[weakest]
|
|
69
|
+
# Solo reportar si es muy extremo (>0.9 o <0.2) para no saturar
|
|
70
|
+
if dominant_val >= 0.9:
|
|
71
|
+
insights.append(f"Trait dominante: {dominant}={dominant_val} (máximo registrado)")
|
|
72
|
+
if weakest_val <= 0.2:
|
|
73
|
+
insights.append(f"Trait en mínimo: {weakest}={weakest_val} (baja tolerancia activa)")
|
|
74
|
+
|
|
75
|
+
# ── 3. Contradicciones recientes (últimos 3 días) ─────────────────────────
|
|
76
|
+
contradictions = model.get("contradictions", [])
|
|
77
|
+
recent_contradictions = []
|
|
78
|
+
for c in contradictions:
|
|
79
|
+
try:
|
|
80
|
+
c_date = datetime.strptime(c["date"], "%Y-%m-%d").date()
|
|
81
|
+
if c_date >= cutoff:
|
|
82
|
+
recent_contradictions.append(c)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
for c in recent_contradictions:
|
|
87
|
+
insights.append(f"Contradicción detectada: {c['description'].strip()}")
|
|
88
|
+
|
|
89
|
+
# ── 4. Foco activo actual ─────────────────────────────────────────────────
|
|
90
|
+
current_focus = model.get("current_focus", [])
|
|
91
|
+
# Solo incluir si hay focos (y no es el arranque inicial)
|
|
92
|
+
if len(current_focus) > 0 and len(insights) == 0:
|
|
93
|
+
# Si no hay otros insights, reportar foco como contexto mínimo
|
|
94
|
+
focus_str = ", ".join(current_focus)
|
|
95
|
+
insights.append(f"Foco actual: {focus_str}")
|
|
96
|
+
|
|
97
|
+
# ── 5. Goals: cambios active/dormant ─────────────────────────────────────
|
|
98
|
+
goals_active = model.get("goals_active", [])
|
|
99
|
+
goals_dormant = model.get("goals_dormant", [])
|
|
100
|
+
# Si hay goals dormant, mencionarlos (pueden necesitar reactivación)
|
|
101
|
+
if goals_dormant:
|
|
102
|
+
insights.append(f"Goal dormant: {goals_dormant[0]}")
|
|
103
|
+
|
|
104
|
+
# ── Leer último daily summary ─────────────────────────────────────────────
|
|
105
|
+
summary_text = ""
|
|
106
|
+
try:
|
|
107
|
+
files = sorted(glob.glob(os.path.join(SUMMARIES_DIR, "*.md")))
|
|
108
|
+
if files:
|
|
109
|
+
with open(files[-1]) as f:
|
|
110
|
+
lines = [l.rstrip() for l in f.readlines() if l.strip()]
|
|
111
|
+
# Take up to 3 lines of content (exclude header)
|
|
112
|
+
content_lines = [l for l in lines if not l.startswith("# Resumen")]
|
|
113
|
+
summary_text = " | ".join(content_lines[:3])
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
# ── Output ────────────────────────────────────────────────────────────────
|
|
118
|
+
# Si no hay insights reales, salir sin output
|
|
119
|
+
if not insights and not summary_text:
|
|
120
|
+
sys.exit(0)
|
|
121
|
+
|
|
122
|
+
# Deduplicar y limitar
|
|
123
|
+
seen = set()
|
|
124
|
+
unique_insights = []
|
|
125
|
+
for ins in insights:
|
|
126
|
+
key = ins[:60]
|
|
127
|
+
if key not in seen:
|
|
128
|
+
seen.add(key)
|
|
129
|
+
unique_insights.append(ins)
|
|
130
|
+
|
|
131
|
+
if unique_insights:
|
|
132
|
+
print("BRAIN_INSIGHTS:")
|
|
133
|
+
for ins in unique_insights[:5]:
|
|
134
|
+
print(f"- {ins}")
|
|
135
|
+
|
|
136
|
+
if summary_text:
|
|
137
|
+
print("RECENT_SUMMARY:")
|
|
138
|
+
print(summary_text[:400])
|
|
139
|
+
|
|
140
|
+
PYEOF
|
|
@@ -319,7 +319,7 @@ def _create_review_followup(conn: sqlite3.Connection, cycle_num: int,
|
|
|
319
319
|
public_items = [i for i in items if i.get("scope") == "public"]
|
|
320
320
|
local_items = [i for i in items if i.get("scope") != "public"]
|
|
321
321
|
|
|
322
|
-
lines = [f"Evolution Cycle #{cycle_num} — {len(items)}
|
|
322
|
+
lines = [f"Evolution Cycle #{cycle_num} — {len(items)} proposals to review."]
|
|
323
323
|
lines.append(f"Análisis: {analysis[:200]}")
|
|
324
324
|
lines.append("")
|
|
325
325
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
NEXO Followup Hygiene — Weekly cleanup of followup/reminder statuses.
|
|
4
|
+
|
|
5
|
+
Runs Sundays via LaunchAgent (or manually). Tasks:
|
|
6
|
+
1. Normalize dirty statuses (COMPLETED YYYY-MM-DD → COMPLETED)
|
|
7
|
+
2. Flag PENDING followups >14 days without updates as STALE
|
|
8
|
+
3. Generate summary of orphaned/forgotten followups for synthesis
|
|
9
|
+
|
|
10
|
+
No CLI needed — this is pure mechanical cleanup.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sqlite3
|
|
15
|
+
import sys
|
|
16
|
+
from datetime import datetime, date, timedelta
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
NEXO_DB = Path.home() / ".nexo" / "nexo-mcp" / "nexo.db"
|
|
20
|
+
COORD_DIR = Path.home() / ".nexo" / "coordination"
|
|
21
|
+
LOG_FILE = Path.home() / ".nexo" / "logs" / "followup-hygiene.log"
|
|
22
|
+
|
|
23
|
+
TODAY = date.today().isoformat()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def log(msg):
|
|
27
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
28
|
+
line = f"[{ts}] {msg}"
|
|
29
|
+
print(line, flush=True)
|
|
30
|
+
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
with open(LOG_FILE, "a") as f:
|
|
32
|
+
f.write(line + "\n")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main():
|
|
36
|
+
log("=== Followup Hygiene starting ===")
|
|
37
|
+
|
|
38
|
+
if not NEXO_DB.exists():
|
|
39
|
+
log("nexo.db not found")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
43
|
+
conn.row_factory = sqlite3.Row
|
|
44
|
+
|
|
45
|
+
# 1. Normalize dirty statuses
|
|
46
|
+
dirty_f = conn.execute("SELECT COUNT(*) FROM followups WHERE status LIKE 'COMPLETED %'").fetchone()[0]
|
|
47
|
+
dirty_r = conn.execute("SELECT COUNT(*) FROM reminders WHERE status LIKE 'COMPLETED %'").fetchone()[0]
|
|
48
|
+
|
|
49
|
+
if dirty_f > 0:
|
|
50
|
+
conn.execute("UPDATE followups SET status='COMPLETED' WHERE status LIKE 'COMPLETED %'")
|
|
51
|
+
log(f"Normalized {dirty_f} dirty followup statuses")
|
|
52
|
+
|
|
53
|
+
if dirty_r > 0:
|
|
54
|
+
conn.execute("UPDATE reminders SET status='COMPLETED' WHERE status LIKE 'COMPLETED %'")
|
|
55
|
+
log(f"Normalized {dirty_r} dirty reminder statuses")
|
|
56
|
+
|
|
57
|
+
# 2. Flag stale followups (PENDING >14 days, no updates)
|
|
58
|
+
cutoff = (date.today() - timedelta(days=14)).isoformat()
|
|
59
|
+
stale = conn.execute(
|
|
60
|
+
"SELECT id, description, date, updated_at FROM followups "
|
|
61
|
+
"WHERE status NOT LIKE 'COMPLETED%' AND status NOT LIKE 'COMPLETED%' "
|
|
62
|
+
"AND date != '' AND date < ? "
|
|
63
|
+
"ORDER BY date",
|
|
64
|
+
(cutoff,)
|
|
65
|
+
).fetchall()
|
|
66
|
+
|
|
67
|
+
if stale:
|
|
68
|
+
log(f"Found {len(stale)} stale followups (>14 days overdue):")
|
|
69
|
+
for s in stale[:10]:
|
|
70
|
+
log(f" {s['id']}: {s['description'][:60]} (due: {s['date']})")
|
|
71
|
+
|
|
72
|
+
# 3. Orphaned followups (no date, no recent update)
|
|
73
|
+
orphans = conn.execute(
|
|
74
|
+
"SELECT id, description FROM followups "
|
|
75
|
+
"WHERE status NOT LIKE 'COMPLETED%' AND status NOT LIKE 'COMPLETED%' "
|
|
76
|
+
"AND (date IS NULL OR date = '') "
|
|
77
|
+
"ORDER BY id"
|
|
78
|
+
).fetchall()
|
|
79
|
+
|
|
80
|
+
if orphans:
|
|
81
|
+
log(f"Found {len(orphans)} orphaned followups (no date):")
|
|
82
|
+
for o in orphans[:10]:
|
|
83
|
+
log(f" {o['id']}: {o['description'][:60]}")
|
|
84
|
+
|
|
85
|
+
conn.commit()
|
|
86
|
+
conn.close()
|
|
87
|
+
|
|
88
|
+
# 4. Write summary for synthesis
|
|
89
|
+
summary = {
|
|
90
|
+
"date": TODAY,
|
|
91
|
+
"dirty_normalized": dirty_f + dirty_r,
|
|
92
|
+
"stale_count": len(stale) if stale else 0,
|
|
93
|
+
"orphan_count": len(orphans) if orphans else 0,
|
|
94
|
+
"stale_ids": [s["id"] for s in stale[:20]] if stale else [],
|
|
95
|
+
"orphan_ids": [o["id"] for o in orphans[:20]] if orphans else [],
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
summary_file = COORD_DIR / "followup-hygiene-summary.json"
|
|
99
|
+
summary_file.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
summary_file.write_text(json.dumps(summary, indent=2))
|
|
101
|
+
|
|
102
|
+
log(f"Summary: {dirty_f + dirty_r} normalized, {len(stale) if stale else 0} stale, {len(orphans) if orphans else 0} orphans")
|
|
103
|
+
log("=== Followup Hygiene complete ===")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
main()
|
|
@@ -68,7 +68,7 @@ def collect_data() -> dict:
|
|
|
68
68
|
conn = sqlite3.connect(str(NEXO_DB))
|
|
69
69
|
conn.row_factory = sqlite3.Row
|
|
70
70
|
|
|
71
|
-
#
|
|
71
|
+
# Today diaries with self-critique
|
|
72
72
|
rows = conn.execute(
|
|
73
73
|
"SELECT id, session_id, summary, self_critique, user_signals, "
|
|
74
74
|
"mental_state, domain, created_at "
|
|
@@ -106,7 +106,7 @@ def consolidate_with_cli(data: dict) -> bool:
|
|
|
106
106
|
|
|
107
107
|
diaries_with_critique = [
|
|
108
108
|
d for d in data["diaries"]
|
|
109
|
-
if d.get("self_critique") and not (d["self_critique"] or "").strip().lower().startswith("
|
|
109
|
+
if d.get("self_critique") and not (d["self_critique"] or "").strip().lower().startswith("no self-critique")
|
|
110
110
|
]
|
|
111
111
|
|
|
112
112
|
if not diaries_with_critique:
|
|
@@ -118,13 +118,13 @@ def consolidate_with_cli(data: dict) -> bool:
|
|
|
118
118
|
if len(diaries_json) > 12000:
|
|
119
119
|
diaries_json = diaries_json[:12000] + "\n... (truncado)"
|
|
120
120
|
|
|
121
|
-
prompt = f"""
|
|
122
|
-
|
|
121
|
+
prompt = f"""You are NEXO nightly consolidator. Your job is to review self-critiques
|
|
122
|
+
from today and decide which deserve to become permanent rules (feedback_postmortem_*.md).
|
|
123
123
|
|
|
124
124
|
FECHA: {data['date']}
|
|
125
|
-
|
|
125
|
+
SESSIONS TODAY: {len(data['diaries'])} total, {len(diaries_with_critique)} with self-critique
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
DIARIES WITH SELF-CRITIQUE:
|
|
128
128
|
{diaries_json}
|
|
129
129
|
|
|
130
130
|
FEEDBACKS POSTMORTEM QUE YA EXISTEN ({len(data['existing_feedbacks'])}):
|
|
@@ -138,44 +138,44 @@ INSTRUCCIONES:
|
|
|
138
138
|
1. Lee cada self_critique y entiende su SIGNIFICADO (no cuentes palabras).
|
|
139
139
|
|
|
140
140
|
2. PROMOVER a feedback permanente SOLO SI:
|
|
141
|
-
-
|
|
141
|
+
- A pattern appears in 2+ different sessions today (by meaning, not literal text)
|
|
142
142
|
- O the user corrigió explícitamente (user_signals contiene corrección)
|
|
143
|
-
- Y
|
|
143
|
+
- Y the self-critique contains a CONCRETE ACTION that prevents a future error
|
|
144
144
|
- Y NO existe ya un feedback similar en los existentes
|
|
145
145
|
|
|
146
146
|
3. NO promover si:
|
|
147
|
-
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
147
|
+
- It is a negative response ("No pasó nada", "sesión limpia")
|
|
148
|
+
- It is generic without concrete action
|
|
149
|
+
- A feedback already exists covering the same topic
|
|
150
150
|
|
|
151
|
-
4.
|
|
151
|
+
4. For each rule to promote, create the file with Write en {MEMORY_DIR}/:
|
|
152
152
|
Nombre: feedback_postmortem_[slug_descriptivo].md
|
|
153
153
|
Formato:
|
|
154
154
|
---
|
|
155
|
-
name: [
|
|
156
|
-
description:
|
|
155
|
+
name: [descriptive title]
|
|
156
|
+
description: Behavioral rule extracted from self-critique — recurring pattern
|
|
157
157
|
type: feedback
|
|
158
158
|
---
|
|
159
159
|
|
|
160
|
-
[
|
|
160
|
+
[Clear description of the pattern and rule]
|
|
161
161
|
|
|
162
|
-
**Why:** [
|
|
163
|
-
**How to apply:** [
|
|
162
|
+
**Why:** [Why this matters — with evidence from sessions]
|
|
163
|
+
**How to apply:** [When and how to apply this rule]
|
|
164
164
|
|
|
165
|
-
5.
|
|
165
|
+
5. Write the daily summary en ~/.nexo/coordination/postmortem-daily.md:
|
|
166
166
|
# Post-Mortem Daily — {data['date']}
|
|
167
|
-
|
|
167
|
+
Sessions: X | Self-critiques: Y | Promoted: Z
|
|
168
168
|
|
|
169
|
-
##
|
|
170
|
-
[
|
|
169
|
+
## Self-critiques of the day (summary)
|
|
170
|
+
[Brief list]
|
|
171
171
|
|
|
172
172
|
## Promovido a memoria permanente
|
|
173
|
-
[
|
|
173
|
+
[What you promoted and why]
|
|
174
174
|
|
|
175
|
-
##
|
|
176
|
-
[
|
|
175
|
+
## Discarded (and why)
|
|
176
|
+
[What you did NOT promote and why]
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
Execute without asking."""
|
|
179
179
|
|
|
180
180
|
log(f"Stage 2: Invoking Claude CLI (opus) with {len(diaries_with_critique)} critiques...")
|
|
181
181
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env /Library/Frameworks/Python.framework/Versions/3.12/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
NEXO Pre-Commit Validator — Dynamic version
|
|
4
|
+
Queries nexo.db learnings to surface relevant warnings/blockers before commit.
|
|
5
|
+
Runs standalone (no MCP dependency, just sqlite3).
|
|
6
|
+
"""
|
|
7
|
+
import sqlite3
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
NEXO_DB = Path.home() / ".nexo" / "nexo-mcp" / "nexo.db"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_staged_files():
|
|
16
|
+
"""Get list of staged files."""
|
|
17
|
+
result = subprocess.run(
|
|
18
|
+
['git', 'diff', '--cached', '--name-only'],
|
|
19
|
+
capture_output=True, text=True
|
|
20
|
+
)
|
|
21
|
+
return [f for f in result.stdout.strip().split('\n') if f]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def check_file(filepath, conn):
|
|
25
|
+
"""Dynamic checks from learnings DB."""
|
|
26
|
+
errors, warnings = [], []
|
|
27
|
+
|
|
28
|
+
filename = Path(filepath).name
|
|
29
|
+
parent_dir = Path(filepath).parent.name
|
|
30
|
+
|
|
31
|
+
# 1. Find learnings mentioning this file or directory
|
|
32
|
+
file_learnings = conn.execute(
|
|
33
|
+
"SELECT id, title, content FROM learnings WHERE INSTR(content, ?) > 0 OR INSTR(content, ?) > 0",
|
|
34
|
+
(filename, parent_dir)
|
|
35
|
+
).fetchall()
|
|
36
|
+
|
|
37
|
+
# 2. Check repetition count for each matching learning
|
|
38
|
+
for row in file_learnings:
|
|
39
|
+
lid, title = row[0], row[1]
|
|
40
|
+
rep_count = conn.execute(
|
|
41
|
+
"SELECT COUNT(*) FROM error_repetitions WHERE original_learning_id = ?",
|
|
42
|
+
(lid,)
|
|
43
|
+
).fetchone()[0]
|
|
44
|
+
|
|
45
|
+
if rep_count >= 5:
|
|
46
|
+
errors.append(f"BLOCKED #{lid} ({rep_count}x repeated): {title[:80]}")
|
|
47
|
+
elif rep_count >= 3:
|
|
48
|
+
warnings.append(f"WARNING #{lid} ({rep_count}x repeated): {title[:80]}")
|
|
49
|
+
|
|
50
|
+
# 3. Universal rules (SIEMPRE/NUNCA/ANTES) matching this file
|
|
51
|
+
universal = conn.execute(
|
|
52
|
+
"SELECT id, title, content FROM learnings WHERE "
|
|
53
|
+
"(content LIKE '%SIEMPRE%' OR content LIKE '%NUNCA%' OR content LIKE '%ANTES%') "
|
|
54
|
+
"AND (INSTR(content, ?) > 0 OR INSTR(content, ?) > 0)",
|
|
55
|
+
(filename, parent_dir)
|
|
56
|
+
).fetchall()
|
|
57
|
+
|
|
58
|
+
for row in universal:
|
|
59
|
+
lid, title = row[0], row[1]
|
|
60
|
+
# Don't duplicate if already found above
|
|
61
|
+
if not any(f"#{lid}" in e for e in errors + warnings):
|
|
62
|
+
warnings.append(f"RULE #{lid}: {title[:80]}")
|
|
63
|
+
|
|
64
|
+
return errors, warnings
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main():
|
|
68
|
+
"""Run pre-commit validation."""
|
|
69
|
+
staged = get_staged_files()
|
|
70
|
+
if not staged:
|
|
71
|
+
print("No staged files to check")
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
if not NEXO_DB.exists():
|
|
75
|
+
print("nexo.db not found — skipping dynamic checks")
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
conn = sqlite3.connect(str(NEXO_DB), timeout=5)
|
|
79
|
+
|
|
80
|
+
all_errors = {}
|
|
81
|
+
all_warnings = {}
|
|
82
|
+
|
|
83
|
+
for filepath in staged:
|
|
84
|
+
errors, warnings = check_file(filepath, conn)
|
|
85
|
+
if errors:
|
|
86
|
+
all_errors[filepath] = errors
|
|
87
|
+
if warnings:
|
|
88
|
+
all_warnings[filepath] = warnings
|
|
89
|
+
|
|
90
|
+
conn.close()
|
|
91
|
+
|
|
92
|
+
# Print warnings (non-blocking)
|
|
93
|
+
if all_warnings:
|
|
94
|
+
print("\nWARNINGS:")
|
|
95
|
+
for filepath, warns in all_warnings.items():
|
|
96
|
+
print(f"\n {filepath}:")
|
|
97
|
+
for w in warns:
|
|
98
|
+
print(f" - {w}")
|
|
99
|
+
|
|
100
|
+
# Print errors (blocking)
|
|
101
|
+
if all_errors:
|
|
102
|
+
print("\nBLOCKED — Fix these before committing:\n")
|
|
103
|
+
for filepath, errs in all_errors.items():
|
|
104
|
+
print(f" {filepath}:")
|
|
105
|
+
for e in errs:
|
|
106
|
+
print(f" - {e}")
|
|
107
|
+
print()
|
|
108
|
+
return 1
|
|
109
|
+
|
|
110
|
+
if all_warnings:
|
|
111
|
+
print("\nPre-commit passed with warnings\n")
|
|
112
|
+
else:
|
|
113
|
+
print("Pre-commit validation passed")
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == '__main__':
|
|
118
|
+
sys.exit(main())
|