nexo-brain 2.0.0 → 2.1.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.
- package/README.md +140 -41
- package/package.json +15 -3
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/crons/manifest.json +106 -0
- package/src/crons/sync.py +217 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app.py +16 -2
- package/src/dashboard/templates/dashboard.html +3 -2
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_episodic.py +1 -1
- package/src/db/_reminders.py +9 -5
- package/src/hooks/session-stop.sh +2 -1
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/core_rules.py +34 -17
- package/src/plugins/update.py +18 -0
- package/src/scripts/check-context.py +4 -7
- package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/apply_findings.py +512 -167
- package/src/scripts/deep-sleep/collect.py +480 -0
- package/src/scripts/deep-sleep/extract-prompt.md +233 -0
- package/src/scripts/deep-sleep/extract.py +249 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +168 -0
- package/src/scripts/deep-sleep/synthesize.py +191 -0
- package/src/scripts/nexo-catchup.py +5 -8
- package/src/scripts/nexo-daily-self-audit.py +28 -19
- package/src/scripts/nexo-deep-sleep.sh +31 -16
- package/src/scripts/nexo-evolution-run.py +5 -20
- package/src/scripts/nexo-followup-hygiene.py +4 -2
- package/src/scripts/nexo-github-monitor.py +6 -9
- package/src/scripts/nexo-immune.py +4 -17
- package/src/scripts/nexo-learning-validator.py +0 -29
- package/src/scripts/nexo-postmortem-consolidator.py +9 -20
- package/src/scripts/nexo-proactive-dashboard.py +1 -0
- package/src/scripts/nexo-sleep.py +8 -18
- package/src/scripts/nexo-synthesis.py +8 -19
- package/src/tools_menu.py +1 -1
- package/src/tools_sessions.py +67 -0
- package/src/__pycache__/auto_close_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-314.pyc +0 -0
- package/src/__pycache__/claim_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
- package/src/__pycache__/evolution_cycle.cpython-310.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-310.pyc +0 -0
- package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
- package/src/__pycache__/migrate_embeddings.cpython-310.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-310.pyc +0 -0
- package/src/__pycache__/server.cpython-314.pyc +0 -0
- package/src/__pycache__/storage_router.cpython-310.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/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
- package/src/hooks/__pycache__/auto_capture.cpython-310.pyc +0 -0
- package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/__init__.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/rules/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/rules/__pycache__/migrate.cpython-310.pyc +0 -0
- package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/check-context.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-github-monitor.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-310.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-310.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-310.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-310.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/analyze_session.py +0 -217
- package/src/scripts/deep-sleep/collect_transcripts.py +0 -145
- package/src/scripts/deep-sleep/prompt.md +0 -109
- package/tests/__pycache__/__init__.cpython-310.pyc +0 -0
- package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tests/__pycache__/conftest.cpython-310-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/conftest.cpython-310.pyc +0 -0
- package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_cognitive.cpython-310-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_cognitive.cpython-310.pyc +0 -0
- package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_knowledge_graph.cpython-310-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_knowledge_graph.cpython-310.pyc +0 -0
- package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_migrations.cpython-310-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_migrations.cpython-310.pyc +0 -0
- package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Deep Sleep v2 -- Phase 3: Synthesize extractions into actionable findings.
|
|
4
|
+
|
|
5
|
+
One Claude call that reads all per-session extractions and produces a
|
|
6
|
+
unified synthesis with cross-session patterns, morning agenda, context
|
|
7
|
+
packets, and deduplicated actions.
|
|
8
|
+
|
|
9
|
+
Environment variables:
|
|
10
|
+
NEXO_HOME -- root of the NEXO installation (default: ~/.nexo)
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
21
|
+
DEEP_SLEEP_DIR = NEXO_HOME / "operations" / "deep-sleep"
|
|
22
|
+
PROMPT_FILE = Path(__file__).parent / "synthesize-prompt.md"
|
|
23
|
+
|
|
24
|
+
CLAUDE_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_claude_cli() -> str:
|
|
28
|
+
"""Find the Claude CLI binary."""
|
|
29
|
+
candidates = [
|
|
30
|
+
Path.home() / ".local" / "bin" / "claude",
|
|
31
|
+
Path("/usr/local/bin/claude"),
|
|
32
|
+
]
|
|
33
|
+
for c in candidates:
|
|
34
|
+
if c.exists():
|
|
35
|
+
return str(c)
|
|
36
|
+
which = shutil.which("claude")
|
|
37
|
+
if which:
|
|
38
|
+
return which
|
|
39
|
+
return "claude"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def extract_json_from_response(text: str) -> dict | None:
|
|
43
|
+
"""Parse JSON from Claude's response, handling markdown fences."""
|
|
44
|
+
text = text.strip()
|
|
45
|
+
|
|
46
|
+
if text.startswith("```"):
|
|
47
|
+
lines = text.split("\n")
|
|
48
|
+
end = len(lines)
|
|
49
|
+
for i in range(len(lines) - 1, 0, -1):
|
|
50
|
+
if lines[i].strip() == "```":
|
|
51
|
+
end = i
|
|
52
|
+
break
|
|
53
|
+
text = "\n".join(lines[1:end]).strip()
|
|
54
|
+
|
|
55
|
+
brace_start = text.find("{")
|
|
56
|
+
if brace_start < 0:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
depth = 0
|
|
60
|
+
for i in range(brace_start, len(text)):
|
|
61
|
+
if text[i] == "{":
|
|
62
|
+
depth += 1
|
|
63
|
+
elif text[i] == "}":
|
|
64
|
+
depth -= 1
|
|
65
|
+
if depth == 0:
|
|
66
|
+
try:
|
|
67
|
+
return json.loads(text[brace_start:i + 1])
|
|
68
|
+
except json.JSONDecodeError:
|
|
69
|
+
break
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
target_date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
|
|
75
|
+
|
|
76
|
+
extractions_file = DEEP_SLEEP_DIR / f"{target_date}-extractions.json"
|
|
77
|
+
context_file = DEEP_SLEEP_DIR / f"{target_date}-context.txt"
|
|
78
|
+
|
|
79
|
+
if not extractions_file.exists():
|
|
80
|
+
print(f"[synthesize] No extractions file for {target_date}. Run extract.py first.")
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
# Check if there are any findings worth synthesizing
|
|
84
|
+
with open(extractions_file) as f:
|
|
85
|
+
extractions = json.load(f)
|
|
86
|
+
|
|
87
|
+
total_findings = extractions.get("total_findings", 0)
|
|
88
|
+
if total_findings == 0:
|
|
89
|
+
print(f"[synthesize] No findings to synthesize for {target_date}.")
|
|
90
|
+
# Write minimal synthesis
|
|
91
|
+
output = {
|
|
92
|
+
"date": target_date,
|
|
93
|
+
"sessions_analyzed": extractions.get("sessions_analyzed", 0),
|
|
94
|
+
"cross_session_patterns": [],
|
|
95
|
+
"morning_agenda": [],
|
|
96
|
+
"context_packets": [],
|
|
97
|
+
"actions": [],
|
|
98
|
+
"summary": f"No significant findings for {target_date}."
|
|
99
|
+
}
|
|
100
|
+
output_file = DEEP_SLEEP_DIR / f"{target_date}-synthesis.json"
|
|
101
|
+
with open(output_file, "w") as f:
|
|
102
|
+
json.dump(output, f, indent=2, ensure_ascii=False)
|
|
103
|
+
print(f"[synthesize] Output: {output_file}")
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
# Build prompt
|
|
107
|
+
prompt_template = PROMPT_FILE.read_text()
|
|
108
|
+
prompt = prompt_template.replace("{{EXTRACTIONS_FILE}}", str(extractions_file))
|
|
109
|
+
prompt = prompt.replace("{{CONTEXT_FILE}}", str(context_file))
|
|
110
|
+
|
|
111
|
+
claude_bin = find_claude_cli()
|
|
112
|
+
print(f"[synthesize] Phase 3: Synthesizing {total_findings} findings from {target_date}")
|
|
113
|
+
print(f"[synthesize] Claude CLI: {claude_bin}")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
env = os.environ.copy()
|
|
117
|
+
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
118
|
+
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
[
|
|
121
|
+
claude_bin,
|
|
122
|
+
"-p", prompt,
|
|
123
|
+
"--model", "opus",
|
|
124
|
+
"--output-format", "text",
|
|
125
|
+
"--allowedTools",
|
|
126
|
+
"Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__nexo_startup,mcp__nexo__nexo_learning_search,mcp__nexo__nexo_recall,mcp__nexo__nexo_reminders"
|
|
127
|
+
],
|
|
128
|
+
capture_output=True,
|
|
129
|
+
text=True,
|
|
130
|
+
timeout=CLAUDE_TIMEOUT,
|
|
131
|
+
env=env
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if result.returncode != 0:
|
|
135
|
+
print(f"[synthesize] Claude CLI error (exit {result.returncode}): {result.stderr[:300]}", file=sys.stderr)
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
|
|
138
|
+
# Filter hook contamination
|
|
139
|
+
output_text = "\n".join(
|
|
140
|
+
l for l in result.stdout.strip().splitlines()
|
|
141
|
+
if not l.strip().startswith("Post-mortem")
|
|
142
|
+
)
|
|
143
|
+
parsed = extract_json_from_response(output_text)
|
|
144
|
+
|
|
145
|
+
# Fallback: Opus might have written the file directly via Write tool
|
|
146
|
+
if not parsed:
|
|
147
|
+
for candidate in [
|
|
148
|
+
DEEP_SLEEP_DIR / f"{target_date}-analysis.json",
|
|
149
|
+
DEEP_SLEEP_DIR / f"{target_date}-synthesis.json",
|
|
150
|
+
]:
|
|
151
|
+
if candidate.exists() and candidate.stat().st_size > 100:
|
|
152
|
+
try:
|
|
153
|
+
parsed = json.load(open(candidate))
|
|
154
|
+
print(f"[synthesize] Opus wrote file directly: {candidate}")
|
|
155
|
+
break
|
|
156
|
+
except Exception:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
if not parsed:
|
|
160
|
+
debug_file = DEEP_SLEEP_DIR / f"debug-synthesize-{target_date}.txt"
|
|
161
|
+
debug_file.write_text(result.stdout[:10000])
|
|
162
|
+
print(f"[synthesize] Failed to parse JSON. Raw output saved to {debug_file}", file=sys.stderr)
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
165
|
+
# Write synthesis output
|
|
166
|
+
output_file = DEEP_SLEEP_DIR / f"{target_date}-synthesis.json"
|
|
167
|
+
with open(output_file, "w") as f:
|
|
168
|
+
json.dump(parsed, f, indent=2, ensure_ascii=False)
|
|
169
|
+
|
|
170
|
+
n_actions = len(parsed.get("actions", []))
|
|
171
|
+
n_patterns = len(parsed.get("cross_session_patterns", []))
|
|
172
|
+
n_agenda = len(parsed.get("morning_agenda", []))
|
|
173
|
+
n_packets = len(parsed.get("context_packets", []))
|
|
174
|
+
|
|
175
|
+
print(f"[synthesize] Done.")
|
|
176
|
+
print(f" Actions: {n_actions}")
|
|
177
|
+
print(f" Cross-session patterns: {n_patterns}")
|
|
178
|
+
print(f" Morning agenda items: {n_agenda}")
|
|
179
|
+
print(f" Context packets: {n_packets}")
|
|
180
|
+
print(f"[synthesize] Output: {output_file}")
|
|
181
|
+
|
|
182
|
+
except subprocess.TimeoutExpired:
|
|
183
|
+
print(f"[synthesize] Claude CLI timeout ({CLAUDE_TIMEOUT}s)", file=sys.stderr)
|
|
184
|
+
sys.exit(1)
|
|
185
|
+
except FileNotFoundError:
|
|
186
|
+
print(f"[synthesize] Claude CLI not found at: {claude_bin}", file=sys.stderr)
|
|
187
|
+
sys.exit(1)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|
|
@@ -125,7 +125,7 @@ def run_task(name: str, python: str, script: str, state: dict) -> bool:
|
|
|
125
125
|
try:
|
|
126
126
|
result = subprocess.run(
|
|
127
127
|
[python, script_path],
|
|
128
|
-
capture_output=True, text=True, timeout=
|
|
128
|
+
capture_output=True, text=True, timeout=21600,
|
|
129
129
|
env={**os.environ, "HOME": str(HOME), "NEXO_CATCHUP": "1"}
|
|
130
130
|
)
|
|
131
131
|
if result.returncode == 0:
|
|
@@ -187,10 +187,6 @@ def _cli_post_catchup_assessment(ran: int, skipped: int, state: dict):
|
|
|
187
187
|
if not CLAUDE_CLI.exists():
|
|
188
188
|
log(f"Caught up {ran} tasks, {skipped} already current. (CLI unavailable for assessment)")
|
|
189
189
|
return
|
|
190
|
-
|
|
191
|
-
auth_check = subprocess.run(
|
|
192
|
-
[str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
|
|
193
|
-
capture_output=True, text=True, timeout=15
|
|
194
190
|
)
|
|
195
191
|
if auth_check.returncode != 0:
|
|
196
192
|
# CLI not authenticated, skip gracefully
|
|
@@ -222,14 +218,15 @@ Format:
|
|
|
222
218
|
|
|
223
219
|
log(f"Caught up {ran} tasks — running CLI assessment...")
|
|
224
220
|
env = os.environ.copy()
|
|
221
|
+
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
225
222
|
env.pop("CLAUDECODE", None)
|
|
226
223
|
env.pop("CLAUDE_CODE", None)
|
|
227
224
|
|
|
228
225
|
try:
|
|
229
226
|
result = subprocess.run(
|
|
230
|
-
[str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text",
|
|
231
|
-
"--allowedTools", "Read,Write,Edit,Glob,Grep"],
|
|
232
|
-
capture_output=True, text=True, timeout=
|
|
227
|
+
[str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text",
|
|
228
|
+
"--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
|
|
229
|
+
capture_output=True, text=True, timeout=21600, env=env
|
|
233
230
|
)
|
|
234
231
|
if result.returncode == 0:
|
|
235
232
|
log(f"Assessment written to {assessment_file}")
|
|
@@ -303,6 +303,25 @@ def check_runtime_preflight():
|
|
|
303
303
|
finding("ERROR", "preflight", "runtime preflight failing")
|
|
304
304
|
|
|
305
305
|
|
|
306
|
+
def run_watchdog_smoke():
|
|
307
|
+
"""Run the watchdog smoke test so its summary is fresh before we check it."""
|
|
308
|
+
smoke_script = Path(__file__).resolve().parent / "nexo-watchdog-smoke.py"
|
|
309
|
+
if not smoke_script.exists():
|
|
310
|
+
finding("WARN", "watchdog", f"smoke script not found at {smoke_script}")
|
|
311
|
+
return
|
|
312
|
+
try:
|
|
313
|
+
result = subprocess.run(
|
|
314
|
+
[sys.executable, str(smoke_script)],
|
|
315
|
+
capture_output=True, text=True, timeout=60
|
|
316
|
+
)
|
|
317
|
+
if result.returncode != 0:
|
|
318
|
+
finding("WARN", "watchdog", f"smoke test exited {result.returncode}")
|
|
319
|
+
except subprocess.TimeoutExpired:
|
|
320
|
+
finding("ERROR", "watchdog", "smoke test timed out (60s)")
|
|
321
|
+
except Exception as e:
|
|
322
|
+
finding("WARN", "watchdog", f"smoke test failed: {e}")
|
|
323
|
+
|
|
324
|
+
|
|
306
325
|
def check_watchdog_smoke():
|
|
307
326
|
if not WATCHDOG_SMOKE_SUMMARY.exists():
|
|
308
327
|
return
|
|
@@ -409,9 +428,11 @@ def interpret_findings(raw_findings: list) -> bool:
|
|
|
409
428
|
|
|
410
429
|
findings_json = json.dumps(raw_findings, ensure_ascii=False, indent=1)
|
|
411
430
|
|
|
412
|
-
prompt = f"""
|
|
431
|
+
prompt = f"""FIRST: Call nexo_startup(task='daily self-audit') to register this session.
|
|
432
|
+
|
|
433
|
+
You are NEXO's morning self-audit interpreter. The mechanical checks found
|
|
413
434
|
{len(errors)} errors and {len(warns)} warnings. Your job is to UNDERSTAND what's
|
|
414
|
-
actually wrong, not just list findings.
|
|
435
|
+
actually wrong, not just list findings. Use nexo_learning_add for new findings and nexo_followup_create for action items.
|
|
415
436
|
|
|
416
437
|
RAW FINDINGS:
|
|
417
438
|
{findings_json}
|
|
@@ -441,30 +462,17 @@ Also write the machine-readable summary to {LOG_DIR}/self-audit-summary.json.
|
|
|
441
462
|
Execute without asking."""
|
|
442
463
|
|
|
443
464
|
log("Stage B: Invoking Claude CLI (opus) for interpretation...")
|
|
444
|
-
|
|
445
|
-
# Verify Claude CLI is authenticated before calling
|
|
446
|
-
try:
|
|
447
|
-
auth_check = subprocess.run(
|
|
448
|
-
[str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
|
|
449
|
-
capture_output=True, text=True, timeout=15
|
|
450
|
-
)
|
|
451
|
-
if auth_check.returncode != 0:
|
|
452
|
-
log("Stage B: Claude CLI not available or not authenticated. Skipping Stage B.")
|
|
453
|
-
return False
|
|
454
|
-
except Exception:
|
|
455
|
-
log("Stage B: Claude CLI check failed. Skipping Stage B.")
|
|
456
|
-
return False
|
|
457
|
-
|
|
458
465
|
env = os.environ.copy()
|
|
466
|
+
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
459
467
|
env.pop("CLAUDECODE", None)
|
|
460
468
|
env.pop("CLAUDE_CODE", None)
|
|
461
469
|
|
|
462
470
|
try:
|
|
463
471
|
result = subprocess.run(
|
|
464
472
|
[str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
|
|
465
|
-
"--output-format", "text",
|
|
466
|
-
"--allowedTools", "Read,Write,Edit,Glob,Grep"],
|
|
467
|
-
capture_output=True, text=True, timeout=
|
|
473
|
+
"--output-format", "text",
|
|
474
|
+
"--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
|
|
475
|
+
capture_output=True, text=True, timeout=21600, env=env
|
|
468
476
|
)
|
|
469
477
|
|
|
470
478
|
if result.returncode != 0:
|
|
@@ -507,6 +515,7 @@ def main():
|
|
|
507
515
|
check_restore_activity()
|
|
508
516
|
check_bad_responses()
|
|
509
517
|
check_runtime_preflight()
|
|
518
|
+
run_watchdog_smoke()
|
|
510
519
|
check_watchdog_smoke()
|
|
511
520
|
check_cognitive_health()
|
|
512
521
|
|
|
@@ -24,38 +24,53 @@ log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_DIR/deep-sleep.l
|
|
|
24
24
|
|
|
25
25
|
run_analysis() {
|
|
26
26
|
local DATE="$1"
|
|
27
|
-
log "=== Deep Sleep starting for $DATE ==="
|
|
27
|
+
log "=== Deep Sleep v2 starting for $DATE ==="
|
|
28
28
|
|
|
29
|
-
#
|
|
30
|
-
log "
|
|
31
|
-
python3 "$SCRIPT_DIR/deep-sleep/
|
|
29
|
+
# Phase 1: Collect all context (Python, no LLM)
|
|
30
|
+
log "Phase 1: Collecting context for $DATE..."
|
|
31
|
+
python3 "$SCRIPT_DIR/deep-sleep/collect.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
log "No transcripts file generated for $DATE. Skipping."
|
|
33
|
+
if [ ! -f "$DEEP_SLEEP_DIR/$DATE-context.txt" ]; then
|
|
34
|
+
log "No context file generated for $DATE. Skipping."
|
|
36
35
|
return 0
|
|
37
36
|
fi
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
# Check meta for session count
|
|
39
|
+
SESSIONS=0
|
|
40
|
+
if [ -f "$DEEP_SLEEP_DIR/$DATE-meta.json" ]; then
|
|
41
|
+
SESSIONS=$(python3 -c "import json; print(json.load(open('$DEEP_SLEEP_DIR/$DATE-meta.json'))['sessions_found'])")
|
|
42
|
+
elif [ -f "$DEEP_SLEEP_DIR/$DATE-index.json" ]; then
|
|
43
|
+
SESSIONS=$(python3 -c "import json; print(json.load(open('$DEEP_SLEEP_DIR/$DATE-index.json'))['sessions_found'])")
|
|
44
|
+
fi
|
|
40
45
|
if [ "$SESSIONS" -eq 0 ]; then
|
|
41
46
|
log "No sessions found for $DATE. Skipping."
|
|
42
47
|
return 0
|
|
43
48
|
fi
|
|
44
49
|
|
|
45
|
-
#
|
|
46
|
-
log "
|
|
47
|
-
python3 "$SCRIPT_DIR/deep-sleep/
|
|
50
|
+
# Phase 2: Extract findings per session (Claude Opus)
|
|
51
|
+
log "Phase 2: Extracting findings from $SESSIONS sessions..."
|
|
52
|
+
python3 "$SCRIPT_DIR/deep-sleep/extract.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
|
|
48
53
|
|
|
49
|
-
if [ ! -f "$DEEP_SLEEP_DIR/$DATE-
|
|
50
|
-
log "
|
|
54
|
+
if [ ! -f "$DEEP_SLEEP_DIR/$DATE-extractions.json" ]; then
|
|
55
|
+
log "Extraction failed for $DATE. No output."
|
|
51
56
|
return 1
|
|
52
57
|
fi
|
|
53
58
|
|
|
54
|
-
#
|
|
55
|
-
log "
|
|
59
|
+
# Phase 3: Cross-session synthesis (Claude Opus, one call)
|
|
60
|
+
log "Phase 3: Synthesizing cross-session findings..."
|
|
61
|
+
python3 "$SCRIPT_DIR/deep-sleep/synthesize.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
|
|
62
|
+
|
|
63
|
+
if [ ! -f "$DEEP_SLEEP_DIR/$DATE-synthesis.json" ]; then
|
|
64
|
+
log "Synthesis failed for $DATE. Falling back to extractions only."
|
|
65
|
+
# Fall back: apply extractions directly
|
|
66
|
+
cp "$DEEP_SLEEP_DIR/$DATE-extractions.json" "$DEEP_SLEEP_DIR/$DATE-synthesis.json"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Phase 4: Apply findings
|
|
70
|
+
log "Phase 4: Applying findings..."
|
|
56
71
|
python3 "$SCRIPT_DIR/deep-sleep/apply_findings.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
|
|
57
72
|
|
|
58
|
-
log "=== Deep Sleep complete for $DATE ==="
|
|
73
|
+
log "=== Deep Sleep v2 complete for $DATE ==="
|
|
59
74
|
return 0
|
|
60
75
|
}
|
|
61
76
|
|
|
@@ -100,36 +100,21 @@ def set_consecutive_failures(count: int):
|
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
# ── Claude CLI call ──────────────────────────────────────────────────────
|
|
103
|
-
CLI_TIMEOUT =
|
|
103
|
+
CLI_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
def verify_claude_cli() -> bool:
|
|
107
|
-
"""Verify Claude CLI is available and authenticated with a real prompt test."""
|
|
108
|
-
try:
|
|
109
|
-
auth_check = subprocess.run(
|
|
110
|
-
[str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
|
|
111
|
-
capture_output=True, text=True, timeout=15
|
|
112
|
-
)
|
|
113
|
-
if auth_check.returncode != 0:
|
|
114
|
-
stderr = auth_check.stderr[:200] if auth_check.stderr else ""
|
|
115
|
-
log(f"Claude CLI not authenticated or unavailable: {stderr}")
|
|
116
|
-
return False
|
|
117
|
-
return True
|
|
118
|
-
except Exception as e:
|
|
119
|
-
log(f"Claude CLI check failed: {e}")
|
|
120
|
-
return False
|
|
121
|
-
|
|
122
|
-
|
|
123
107
|
def call_claude_cli(prompt: str) -> str:
|
|
124
108
|
"""Call claude -p prompt --model opus via subprocess. Returns stdout text."""
|
|
125
109
|
env = os.environ.copy()
|
|
110
|
+
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
126
111
|
env.pop("CLAUDECODE", None)
|
|
127
112
|
env.pop("CLAUDE_CODE", None)
|
|
128
113
|
|
|
129
114
|
result = subprocess.run(
|
|
130
115
|
[str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
|
|
131
|
-
"--output-format", "text",
|
|
132
|
-
"--allowedTools", "Read,Write,Edit,Glob,Grep,Bash"],
|
|
116
|
+
"--output-format", "text",
|
|
117
|
+
"--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
|
|
133
118
|
capture_output=True,
|
|
134
119
|
text=True,
|
|
135
120
|
timeout=CLI_TIMEOUT,
|
|
@@ -424,7 +409,7 @@ def run():
|
|
|
424
409
|
return
|
|
425
410
|
|
|
426
411
|
# Call Opus via claude -p
|
|
427
|
-
log("Calling claude -p --model opus
|
|
412
|
+
log("Calling claude -p --model opus...")
|
|
428
413
|
try:
|
|
429
414
|
raw_response = call_claude_cli(prompt)
|
|
430
415
|
except Exception as e:
|
|
@@ -61,7 +61,8 @@ def main():
|
|
|
61
61
|
cutoff = (date.today() - timedelta(days=14)).isoformat()
|
|
62
62
|
stale = conn.execute(
|
|
63
63
|
"SELECT id, description, date, updated_at FROM followups "
|
|
64
|
-
"WHERE status NOT LIKE 'COMPLETED%'
|
|
64
|
+
"WHERE status NOT LIKE 'COMPLETED%' "
|
|
65
|
+
"AND status NOT IN ('DELETED','archived','blocked','waiting') "
|
|
65
66
|
"AND date != '' AND date < ? "
|
|
66
67
|
"ORDER BY date",
|
|
67
68
|
(cutoff,)
|
|
@@ -75,7 +76,8 @@ def main():
|
|
|
75
76
|
# 3. Orphaned followups (no date, no recent update)
|
|
76
77
|
orphans = conn.execute(
|
|
77
78
|
"SELECT id, description FROM followups "
|
|
78
|
-
"WHERE status NOT LIKE 'COMPLETED%'
|
|
79
|
+
"WHERE status NOT LIKE 'COMPLETED%' "
|
|
80
|
+
"AND status NOT IN ('DELETED','archived','blocked','waiting') "
|
|
79
81
|
"AND (date IS NULL OR date = '') "
|
|
80
82
|
"ORDER BY id"
|
|
81
83
|
).fetchall()
|
|
@@ -36,7 +36,7 @@ def gh_api(endpoint: str) -> dict | list | None:
|
|
|
36
36
|
try:
|
|
37
37
|
result = subprocess.run(
|
|
38
38
|
["gh", "api", endpoint],
|
|
39
|
-
capture_output=True, text=True, timeout=
|
|
39
|
+
capture_output=True, text=True, timeout=21600
|
|
40
40
|
)
|
|
41
41
|
if result.returncode == 0:
|
|
42
42
|
return json.loads(result.stdout)
|
|
@@ -109,7 +109,7 @@ def collect_data():
|
|
|
109
109
|
try:
|
|
110
110
|
result = subprocess.run(
|
|
111
111
|
["gh", "api", f"repos/{REPO}/compare/{tag}...main"],
|
|
112
|
-
capture_output=True, text=True, timeout=
|
|
112
|
+
capture_output=True, text=True, timeout=21600
|
|
113
113
|
)
|
|
114
114
|
if result.returncode == 0:
|
|
115
115
|
compare = json.loads(result.stdout)
|
|
@@ -157,24 +157,21 @@ Return as JSON:
|
|
|
157
157
|
"alerts": ["alert1", ...],
|
|
158
158
|
"release_recommendation": "text or null"
|
|
159
159
|
}}"""
|
|
160
|
-
|
|
161
|
-
auth_check = subprocess.run(
|
|
162
|
-
[str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
|
|
163
|
-
capture_output=True, text=True, timeout=15
|
|
164
160
|
)
|
|
165
161
|
if auth_check.returncode != 0:
|
|
166
162
|
# CLI not authenticated, skip gracefully
|
|
167
163
|
return ""
|
|
168
164
|
|
|
169
165
|
env = os.environ.copy()
|
|
166
|
+
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
170
167
|
env.pop("CLAUDECODE", None)
|
|
171
168
|
env.pop("CLAUDE_CODE", None)
|
|
172
169
|
|
|
173
170
|
result = subprocess.run(
|
|
174
171
|
[str(CLAUDE_CLI), "-p", prompt,
|
|
175
|
-
"--model", "opus", "--output-format", "text",
|
|
176
|
-
"--allowedTools", "Read,Write,Edit,Glob,Grep"],
|
|
177
|
-
capture_output=True, text=True, timeout=
|
|
172
|
+
"--model", "opus", "--output-format", "text",
|
|
173
|
+
"--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
|
|
174
|
+
capture_output=True, text=True, timeout=21600, env=env
|
|
178
175
|
)
|
|
179
176
|
|
|
180
177
|
if result.returncode != 0:
|
|
@@ -901,30 +901,17 @@ Raw findings:
|
|
|
901
901
|
Write the report. Be concise — max 40 lines."""
|
|
902
902
|
|
|
903
903
|
print("\n[TRIAGE] Running CLI interpretation...")
|
|
904
|
-
|
|
905
|
-
# Verify Claude CLI is authenticated before calling
|
|
906
|
-
try:
|
|
907
|
-
auth_check = subprocess.run(
|
|
908
|
-
[str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
|
|
909
|
-
capture_output=True, text=True, timeout=15
|
|
910
|
-
)
|
|
911
|
-
if auth_check.returncode != 0:
|
|
912
|
-
print("[TRIAGE] Claude CLI not available or not authenticated. Skipping triage.")
|
|
913
|
-
return
|
|
914
|
-
except Exception:
|
|
915
|
-
print("[TRIAGE] Claude CLI check failed. Skipping triage.")
|
|
916
|
-
return
|
|
917
|
-
|
|
918
904
|
env = os.environ.copy()
|
|
905
|
+
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
919
906
|
env.pop("CLAUDECODE", None)
|
|
920
907
|
env.pop("CLAUDE_CODE", None)
|
|
921
908
|
|
|
922
909
|
try:
|
|
923
910
|
result = subprocess.run(
|
|
924
911
|
[str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
|
|
925
|
-
"--output-format", "text",
|
|
926
|
-
"--allowedTools", "Read,Write,Edit,Glob,Grep"],
|
|
927
|
-
capture_output=True, text=True, timeout=
|
|
912
|
+
"--output-format", "text",
|
|
913
|
+
"--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
|
|
914
|
+
capture_output=True, text=True, timeout=21600, env=env
|
|
928
915
|
)
|
|
929
916
|
if result.returncode == 0:
|
|
930
917
|
print(f"[TRIAGE] Report written to {triage_file}")
|
|
@@ -111,35 +111,6 @@ Rules:
|
|
|
111
111
|
|
|
112
112
|
# Try CLI first, fall back to mechanical similarity
|
|
113
113
|
if CLAUDE_CLI.exists():
|
|
114
|
-
auth_check = subprocess.run(
|
|
115
|
-
[str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
|
|
116
|
-
capture_output=True, text=True, timeout=15
|
|
117
|
-
)
|
|
118
|
-
if auth_check.returncode != 0:
|
|
119
|
-
# CLI not authenticated, skip gracefully
|
|
120
|
-
return {"known": False, "confidence": 0, "recommendation": "CLI not authenticated — skipped validation", "matching_learnings": []}
|
|
121
|
-
|
|
122
|
-
env = os.environ.copy()
|
|
123
|
-
env.pop("CLAUDECODE", None)
|
|
124
|
-
env.pop("CLAUDE_CODE", None)
|
|
125
|
-
|
|
126
|
-
try:
|
|
127
|
-
result = subprocess.run(
|
|
128
|
-
[str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text", "--bare",
|
|
129
|
-
"--allowedTools", "Read,Write,Edit,Glob,Grep"],
|
|
130
|
-
capture_output=True, text=True, timeout=60, env=env
|
|
131
|
-
)
|
|
132
|
-
if result.returncode == 0:
|
|
133
|
-
text = result.stdout.strip()
|
|
134
|
-
# Strip markdown fences if present
|
|
135
|
-
if "```json" in text:
|
|
136
|
-
text = text.split("```json")[1].split("```")[0]
|
|
137
|
-
elif "```" in text:
|
|
138
|
-
text = text.split("```")[1].split("```")[0]
|
|
139
|
-
return json.loads(text.strip())
|
|
140
|
-
except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception):
|
|
141
|
-
pass # Fall through to mechanical fallback
|
|
142
|
-
|
|
143
114
|
# Fallback: mechanical SequenceMatcher (original logic)
|
|
144
115
|
return _mechanical_validate(finding, learnings)
|
|
145
116
|
|
|
@@ -113,7 +113,7 @@ def consolidate_with_cli(data: dict) -> bool:
|
|
|
113
113
|
|
|
114
114
|
diaries_with_critique = [
|
|
115
115
|
d for d in data["diaries"]
|
|
116
|
-
if d.get("self_critique") and not (d["self_critique"]
|
|
116
|
+
if d.get("self_critique") and not str(d["self_critique"]).strip().lower().startswith("no self-critique")
|
|
117
117
|
]
|
|
118
118
|
|
|
119
119
|
if not diaries_with_critique:
|
|
@@ -125,8 +125,10 @@ def consolidate_with_cli(data: dict) -> bool:
|
|
|
125
125
|
if len(diaries_json) > 12000:
|
|
126
126
|
diaries_json = diaries_json[:12000] + "\n... (truncated)"
|
|
127
127
|
|
|
128
|
-
prompt = f"""
|
|
129
|
-
|
|
128
|
+
prompt = f"""FIRST: Call nexo_startup(task='nightly postmortem consolidation') to register this session.
|
|
129
|
+
|
|
130
|
+
You are NEXO's nightly consolidator. Your job is to review the self-critiques
|
|
131
|
+
from today and decide which deserve to become permanent rules. Use nexo_learning_add for permanent rules and nexo_followup_create for action items.
|
|
130
132
|
|
|
131
133
|
DATE: {data['date']}
|
|
132
134
|
SESSIONS TODAY: {len(data['diaries'])} total, {len(diaries_with_critique)} with self-critique
|
|
@@ -185,30 +187,17 @@ INSTRUCTIONS:
|
|
|
185
187
|
Execute without asking."""
|
|
186
188
|
|
|
187
189
|
log(f"Stage 2: Invoking Claude CLI (opus) with {len(diaries_with_critique)} critiques...")
|
|
188
|
-
|
|
189
|
-
# Verify Claude CLI is authenticated before calling
|
|
190
|
-
try:
|
|
191
|
-
auth_check = subprocess.run(
|
|
192
|
-
[str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
|
|
193
|
-
capture_output=True, text=True, timeout=15
|
|
194
|
-
)
|
|
195
|
-
if auth_check.returncode != 0:
|
|
196
|
-
log("Stage 2: Claude CLI not available or not authenticated. Skipping Stage 2.")
|
|
197
|
-
return False
|
|
198
|
-
except Exception:
|
|
199
|
-
log("Stage 2: Claude CLI check failed. Skipping Stage 2.")
|
|
200
|
-
return False
|
|
201
|
-
|
|
202
190
|
env = os.environ.copy()
|
|
191
|
+
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
203
192
|
env.pop("CLAUDECODE", None)
|
|
204
193
|
env.pop("CLAUDE_CODE", None)
|
|
205
194
|
|
|
206
195
|
try:
|
|
207
196
|
result = subprocess.run(
|
|
208
197
|
[str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
|
|
209
|
-
"--output-format", "text",
|
|
210
|
-
"--allowedTools", "Read,Write,Edit,Glob,Grep"],
|
|
211
|
-
capture_output=True, text=True, timeout=
|
|
198
|
+
"--output-format", "text",
|
|
199
|
+
"--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
|
|
200
|
+
capture_output=True, text=True, timeout=21600, env=env
|
|
212
201
|
)
|
|
213
202
|
|
|
214
203
|
if result.returncode != 0:
|
|
@@ -38,6 +38,7 @@ def check_overdue_followups() -> list[dict]:
|
|
|
38
38
|
SELECT id, description, date, created_at, reasoning
|
|
39
39
|
FROM followups
|
|
40
40
|
WHERE status NOT LIKE 'COMPLETED%'
|
|
41
|
+
AND status NOT IN ('DELETED','archived','blocked','waiting')
|
|
41
42
|
AND date IS NOT NULL AND date != ''
|
|
42
43
|
ORDER BY date ASC
|
|
43
44
|
""").fetchall()
|