nexo-brain 2.6.13 → 2.6.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +34 -4
- package/bin/nexo-brain.js +1 -0
- package/package.json +3 -2
- package/src/agent_runner.py +19 -0
- package/src/auto_update.py +62 -77
- package/src/bootstrap_docs.py +357 -0
- package/src/cli.py +3 -1
- package/src/client_sync.py +58 -6
- package/src/doctor/providers/runtime.py +172 -0
- package/src/evolution_cycle.py +4 -4
- package/src/hooks/session-start.sh +16 -0
- package/src/scripts/deep-sleep/collect.py +162 -24
- package/src/scripts/deep-sleep/extract.py +33 -6
- package/src/scripts/nexo-daily-self-audit.py +1 -1
- package/src/scripts/nexo-deep-sleep.sh +2 -2
- package/src/scripts/nexo-evolution-run.py +5 -2
- package/src/scripts/nexo-watchdog.sh +1 -1
- package/src/server.py +9 -2
- package/src/tools_sessions.py +3 -2
- package/templates/CLAUDE.md.template +34 -10
- package/templates/CODEX.AGENTS.md.template +45 -0
package/src/evolution_cycle.py
CHANGED
|
@@ -275,19 +275,19 @@ def build_evolution_prompt(week_data: dict, objective: dict) -> str:
|
|
|
275
275
|
if mode == "review":
|
|
276
276
|
mode_desc = "review-only, nothing executes automatically"
|
|
277
277
|
safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/"
|
|
278
|
-
immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, cognitive.py, knowledge_graph.py, tools_*.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md"
|
|
278
|
+
immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, cognitive.py, knowledge_graph.py, tools_*.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, AGENTS.md"
|
|
279
279
|
elif mode == "managed":
|
|
280
280
|
mode_desc = f"owner-managed, max {max_auto} auto-applied changes with rollback and followups"
|
|
281
281
|
safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/, NEXO_CODE/src, repo bin/docs/templates/tests"
|
|
282
|
-
immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, personality.md, user-profile.md"
|
|
282
|
+
immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, AGENTS.md, personality.md, user-profile.md"
|
|
283
283
|
elif mode == "public_core":
|
|
284
284
|
mode_desc = "public core contribution via isolated checkout and Draft PR"
|
|
285
285
|
safe_zones = "isolated public repo checkout only"
|
|
286
|
-
immutable_files = "personal runtime, ~/.nexo/**, local DBs/logs, CLAUDE.md, user-profile.md"
|
|
286
|
+
immutable_files = "personal runtime, ~/.nexo/**, local DBs/logs, CLAUDE.md, AGENTS.md, user-profile.md"
|
|
287
287
|
else:
|
|
288
288
|
mode_desc = f"public auto, max {max_auto} auto-applied changes in personal safe zones"
|
|
289
289
|
safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/"
|
|
290
|
-
immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, cognitive.py, knowledge_graph.py, tools_*.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md"
|
|
290
|
+
immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, cognitive.py, knowledge_graph.py, tools_*.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, AGENTS.md"
|
|
291
291
|
|
|
292
292
|
prompt = f"""You are NEXO Evolution — the weekly self-improvement cycle.
|
|
293
293
|
|
|
@@ -173,6 +173,22 @@ if os.path.exists(audit_file):
|
|
|
173
173
|
except Exception:
|
|
174
174
|
pass
|
|
175
175
|
|
|
176
|
+
# Evolution status
|
|
177
|
+
evolution_file = os.path.join(nexo_home, 'brain', 'evolution-objective.json')
|
|
178
|
+
if os.path.exists(evolution_file):
|
|
179
|
+
try:
|
|
180
|
+
evo = json.load(open(evolution_file))
|
|
181
|
+
lines.append('## Evolution')
|
|
182
|
+
lines.append(f\"Enabled: {bool(evo.get('evolution_enabled', True))}\")
|
|
183
|
+
lines.append(f\"Mode: {evo.get('evolution_mode', 'auto')}\")
|
|
184
|
+
lines.append(f\"Last evolution: {evo.get('last_evolution', 'never')}\")
|
|
185
|
+
lines.append(f\"Total evolutions: {evo.get('total_evolutions', 0)}\")
|
|
186
|
+
if evo.get('disabled_reason'):
|
|
187
|
+
lines.append(f\"Disabled reason: {evo.get('disabled_reason')}\")
|
|
188
|
+
lines.append('')
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
|
|
176
192
|
print('\n'.join(lines))
|
|
177
193
|
" > "$BRIEFING_FILE" 2>/dev/null
|
|
178
194
|
|
|
@@ -4,7 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
Deep Sleep v2 -- Phase 1: Collect all context for overnight analysis.
|
|
5
5
|
|
|
6
6
|
Gathers transcripts, DB data, logs, and discovered files into a single
|
|
7
|
-
plain-text context file that subsequent phases read via
|
|
7
|
+
plain-text context file that subsequent phases read via the configured
|
|
8
|
+
automation backend.
|
|
8
9
|
|
|
9
10
|
Environment variables:
|
|
10
11
|
NEXO_HOME -- root of the NEXO installation (default: ~/.nexo)
|
|
@@ -52,22 +53,43 @@ def _redact_sensitive(text: str) -> str:
|
|
|
52
53
|
return _SENSITIVE_PATTERNS.sub('[REDACTED]', text)
|
|
53
54
|
|
|
54
55
|
|
|
55
|
-
# ── Transcript collection (
|
|
56
|
+
# ── Transcript collection (Claude Code + Codex) ────────────────────────────
|
|
56
57
|
|
|
57
58
|
|
|
58
|
-
def
|
|
59
|
-
|
|
59
|
+
def _session_identifier(client: str, session_file: str) -> str:
|
|
60
|
+
return f"{client}:{session_file}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def find_claude_session_files() -> list[Path]:
|
|
64
|
+
"""Find Claude Code session JSONL files under ~/.claude/projects."""
|
|
60
65
|
claude_dir = Path.home() / ".claude" / "projects"
|
|
61
66
|
if not claude_dir.exists():
|
|
62
67
|
return []
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
return sorted(claude_dir.rglob("*.jsonl"))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def find_codex_session_files() -> list[Path]:
|
|
72
|
+
"""Find Codex session JSONL files under ~/.codex/sessions and archived_sessions."""
|
|
73
|
+
roots = [
|
|
74
|
+
Path.home() / ".codex" / "sessions",
|
|
75
|
+
Path.home() / ".codex" / "archived_sessions",
|
|
76
|
+
]
|
|
77
|
+
files: list[Path] = []
|
|
78
|
+
seen: set[str] = set()
|
|
79
|
+
for root in roots:
|
|
80
|
+
if not root.exists():
|
|
81
|
+
continue
|
|
82
|
+
for jsonl in sorted(root.rglob("*.jsonl")):
|
|
83
|
+
key = jsonl.name
|
|
84
|
+
if key in seen:
|
|
85
|
+
continue
|
|
86
|
+
seen.add(key)
|
|
87
|
+
files.append(jsonl)
|
|
88
|
+
return files
|
|
67
89
|
|
|
68
90
|
|
|
69
|
-
def
|
|
70
|
-
"""Extract clean transcript from a
|
|
91
|
+
def extract_claude_session(jsonl_path: Path) -> dict | None:
|
|
92
|
+
"""Extract clean transcript from a Claude Code JSONL session."""
|
|
71
93
|
messages = []
|
|
72
94
|
tool_uses = []
|
|
73
95
|
user_msg_count = 0
|
|
@@ -136,13 +158,99 @@ def extract_session(jsonl_path: Path) -> dict | None:
|
|
|
136
158
|
return None
|
|
137
159
|
|
|
138
160
|
return {
|
|
139
|
-
"
|
|
161
|
+
"client": "claude_code",
|
|
162
|
+
"session_file": _session_identifier("claude_code", jsonl_path.name),
|
|
163
|
+
"display_name": jsonl_path.name,
|
|
164
|
+
"session_path": str(jsonl_path),
|
|
165
|
+
"message_count": len(messages),
|
|
166
|
+
"user_message_count": user_msg_count,
|
|
167
|
+
"tool_use_count": len(tool_uses),
|
|
168
|
+
"messages": messages,
|
|
169
|
+
"tool_uses": tool_uses,
|
|
170
|
+
"source": "claude_projects",
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def extract_codex_session(jsonl_path: Path) -> dict | None:
|
|
175
|
+
"""Extract clean transcript from a Codex JSONL session."""
|
|
176
|
+
messages = []
|
|
177
|
+
tool_uses = []
|
|
178
|
+
user_msg_count = 0
|
|
179
|
+
session_meta: dict = {}
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
with open(jsonl_path, "r") as f:
|
|
183
|
+
for line_no, line in enumerate(f, 1):
|
|
184
|
+
line = line.strip()
|
|
185
|
+
if not line:
|
|
186
|
+
continue
|
|
187
|
+
try:
|
|
188
|
+
d = json.loads(line)
|
|
189
|
+
except json.JSONDecodeError:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
item_type = d.get("type")
|
|
193
|
+
payload = d.get("payload", {})
|
|
194
|
+
|
|
195
|
+
if item_type == "session_meta" and isinstance(payload, dict):
|
|
196
|
+
session_meta = payload
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
if item_type == "event_msg" and isinstance(payload, dict) and payload.get("type") == "user_message":
|
|
200
|
+
content = str(payload.get("message", "") or "").strip()
|
|
201
|
+
if not content or content.startswith("<environment_context>"):
|
|
202
|
+
continue
|
|
203
|
+
messages.append({
|
|
204
|
+
"role": "user",
|
|
205
|
+
"index": line_no,
|
|
206
|
+
"text": _redact_sensitive(content[:5000]),
|
|
207
|
+
})
|
|
208
|
+
user_msg_count += 1
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
if item_type == "response_item" and isinstance(payload, dict):
|
|
212
|
+
response_type = payload.get("type")
|
|
213
|
+
role = payload.get("role")
|
|
214
|
+
if response_type == "message" and role == "assistant":
|
|
215
|
+
text_parts = []
|
|
216
|
+
for block in payload.get("content", []) or []:
|
|
217
|
+
if isinstance(block, dict) and block.get("type") == "output_text":
|
|
218
|
+
text_parts.append(str(block.get("text", "")))
|
|
219
|
+
combined = "\n".join(part for part in text_parts if part).strip()
|
|
220
|
+
if combined:
|
|
221
|
+
messages.append({
|
|
222
|
+
"role": "assistant",
|
|
223
|
+
"index": line_no,
|
|
224
|
+
"text": _redact_sensitive(combined[:5000]),
|
|
225
|
+
})
|
|
226
|
+
elif response_type == "function_call":
|
|
227
|
+
tool_uses.append({
|
|
228
|
+
"tool": payload.get("name", ""),
|
|
229
|
+
"input_keys": [],
|
|
230
|
+
"file": _redact_sensitive(str(payload.get("arguments", ""))[:100]),
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
except Exception as e:
|
|
234
|
+
print(f" [collect] Error reading {jsonl_path}: {e}", file=sys.stderr)
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
if user_msg_count < MIN_USER_MESSAGES:
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"client": "codex",
|
|
242
|
+
"session_file": _session_identifier("codex", jsonl_path.name),
|
|
243
|
+
"display_name": jsonl_path.name,
|
|
140
244
|
"session_path": str(jsonl_path),
|
|
141
245
|
"message_count": len(messages),
|
|
142
246
|
"user_message_count": user_msg_count,
|
|
143
247
|
"tool_use_count": len(tool_uses),
|
|
144
248
|
"messages": messages,
|
|
145
|
-
"tool_uses": tool_uses
|
|
249
|
+
"tool_uses": tool_uses,
|
|
250
|
+
"source": session_meta.get("source", "codex"),
|
|
251
|
+
"cwd": session_meta.get("cwd", ""),
|
|
252
|
+
"originator": session_meta.get("originator", ""),
|
|
253
|
+
"session_uid": session_meta.get("id", ""),
|
|
146
254
|
}
|
|
147
255
|
|
|
148
256
|
|
|
@@ -156,17 +264,22 @@ def collect_transcripts_since(since_iso: str, until_iso: str = "") -> list[dict]
|
|
|
156
264
|
until_dt = datetime.fromisoformat(until_iso) if until_iso else datetime.now()
|
|
157
265
|
|
|
158
266
|
sessions = []
|
|
159
|
-
|
|
160
|
-
for
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
267
|
+
transcript_files: list[tuple[str, Path]] = [
|
|
268
|
+
("claude_code", path) for path in find_claude_session_files()
|
|
269
|
+
] + [
|
|
270
|
+
("codex", path) for path in find_codex_session_files()
|
|
271
|
+
]
|
|
272
|
+
for client, session_file in transcript_files:
|
|
273
|
+
try:
|
|
274
|
+
mtime = datetime.fromtimestamp(session_file.stat().st_mtime)
|
|
275
|
+
except OSError:
|
|
276
|
+
continue
|
|
277
|
+
if not (since_dt < mtime <= until_dt):
|
|
278
|
+
continue
|
|
279
|
+
session = extract_codex_session(session_file) if client == "codex" else extract_claude_session(session_file)
|
|
280
|
+
if session:
|
|
281
|
+
session["modified"] = mtime.isoformat()
|
|
282
|
+
sessions.append(session)
|
|
170
283
|
sessions.sort(key=lambda s: s["modified"])
|
|
171
284
|
return sessions
|
|
172
285
|
|
|
@@ -353,6 +466,9 @@ def format_transcripts(sessions: list[dict]) -> str:
|
|
|
353
466
|
for i, session in enumerate(sessions):
|
|
354
467
|
lines.append(f"\n{'─' * 60}")
|
|
355
468
|
lines.append(f"SESSION {i + 1}: {session['session_file']}")
|
|
469
|
+
lines.append(f"Client: {session.get('client', 'unknown')}")
|
|
470
|
+
if session.get("source"):
|
|
471
|
+
lines.append(f"Source: {session['source']}")
|
|
356
472
|
lines.append(f"Modified: {session['modified']}")
|
|
357
473
|
lines.append(f"Messages: {session['message_count']}, Tool uses: {session['tool_use_count']}")
|
|
358
474
|
lines.append(f"{'─' * 60}")
|
|
@@ -460,18 +576,27 @@ def main():
|
|
|
460
576
|
|
|
461
577
|
# Individual session files
|
|
462
578
|
session_files_written = []
|
|
579
|
+
session_txt_map = {}
|
|
463
580
|
total_size = len(shared_text.encode("utf-8"))
|
|
464
581
|
for i, session in enumerate(sessions):
|
|
465
|
-
|
|
582
|
+
raw_id = session["session_file"].replace(".jsonl", "").replace(":", "-")
|
|
583
|
+
sid_short = raw_id[:30]
|
|
466
584
|
filename = f"session-{i+1:02d}-{sid_short}.txt"
|
|
467
585
|
session_path = date_dir / filename
|
|
468
586
|
|
|
469
587
|
lines = [
|
|
470
588
|
f"Session: {session['session_file']}",
|
|
589
|
+
f"Display name: {session.get('display_name', session['session_file'])}",
|
|
590
|
+
f"Client: {session.get('client', 'unknown')}",
|
|
591
|
+
f"Source: {session.get('source', 'unknown')}",
|
|
471
592
|
f"Modified: {session['modified']}",
|
|
472
593
|
f"Messages: {session['message_count']}, Tool uses: {session['tool_use_count']}",
|
|
473
594
|
f"{'─' * 60}",
|
|
474
595
|
]
|
|
596
|
+
if session.get("cwd"):
|
|
597
|
+
lines.insert(4, f"CWD: {session['cwd']}")
|
|
598
|
+
if session.get("originator"):
|
|
599
|
+
lines.insert(4, f"Originator: {session['originator']}")
|
|
475
600
|
for msg in session["messages"]:
|
|
476
601
|
role = "USER" if msg["role"] == "user" else "AGENT"
|
|
477
602
|
idx = msg.get("index", "?")
|
|
@@ -487,6 +612,7 @@ def main():
|
|
|
487
612
|
session_text = "\n".join(lines)
|
|
488
613
|
session_path.write_text(session_text, encoding="utf-8")
|
|
489
614
|
session_files_written.append(filename)
|
|
615
|
+
session_txt_map[session["session_file"]] = filename
|
|
490
616
|
total_size += len(session_text.encode("utf-8"))
|
|
491
617
|
print(f" {filename}: {len(session_text) / 1024:.0f} KB")
|
|
492
618
|
|
|
@@ -508,6 +634,18 @@ def main():
|
|
|
508
634
|
"sessions_found": len(sessions),
|
|
509
635
|
"session_files": [s["session_file"] for s in sessions],
|
|
510
636
|
"session_txt_files": session_files_written,
|
|
637
|
+
"session_txt_map": session_txt_map,
|
|
638
|
+
"session_manifest": [
|
|
639
|
+
{
|
|
640
|
+
"session_id": s["session_file"],
|
|
641
|
+
"display_name": s.get("display_name", s["session_file"]),
|
|
642
|
+
"client": s.get("client", "unknown"),
|
|
643
|
+
"source": s.get("source", ""),
|
|
644
|
+
"session_path": s.get("session_path", ""),
|
|
645
|
+
"session_txt_file": session_txt_map.get(s["session_file"], ""),
|
|
646
|
+
}
|
|
647
|
+
for s in sessions
|
|
648
|
+
],
|
|
511
649
|
"total_messages": sum(s["message_count"] for s in sessions),
|
|
512
650
|
"total_tool_uses": sum(s["tool_use_count"] for s in sessions),
|
|
513
651
|
"followups_active": len(followups),
|
|
@@ -66,23 +66,43 @@ def extract_json_from_response(text: str) -> dict | None:
|
|
|
66
66
|
return None
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def
|
|
69
|
+
def _safe_session_slug(session_id: str) -> str:
|
|
70
|
+
return (
|
|
71
|
+
session_id
|
|
72
|
+
.replace(".jsonl", "")
|
|
73
|
+
.replace(":", "-")
|
|
74
|
+
.replace("/", "-")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def find_session_file(session_id: str, date_dir: Path, session_txt_map: dict[str, str] | None = None) -> Path | None:
|
|
70
79
|
"""Find the individual .txt file for a session."""
|
|
80
|
+
if session_txt_map:
|
|
81
|
+
mapped = session_txt_map.get(session_id)
|
|
82
|
+
if mapped:
|
|
83
|
+
candidate = date_dir / mapped
|
|
84
|
+
if candidate.exists():
|
|
85
|
+
return candidate
|
|
71
86
|
if date_dir and date_dir.exists():
|
|
72
|
-
sid_short = session_id
|
|
87
|
+
sid_short = _safe_session_slug(session_id)[:20]
|
|
73
88
|
for f in sorted(date_dir.glob("session-*.txt")):
|
|
74
89
|
if sid_short in f.name:
|
|
75
90
|
return f
|
|
76
91
|
return None
|
|
77
92
|
|
|
78
93
|
|
|
79
|
-
def analyze_session(
|
|
94
|
+
def analyze_session(
|
|
95
|
+
session_id: str,
|
|
96
|
+
date_dir: Path,
|
|
97
|
+
shared_context_file: Path | None,
|
|
98
|
+
session_txt_map: dict[str, str] | None = None,
|
|
99
|
+
) -> dict | None:
|
|
80
100
|
"""Send a session to the automation backend for extraction analysis.
|
|
81
101
|
|
|
82
102
|
The backend reads the small per-session file + shared context file.
|
|
83
103
|
Prompt is short — the heavy lifting is in the Read tool calls.
|
|
84
104
|
"""
|
|
85
|
-
session_file = find_session_file(session_id, date_dir)
|
|
105
|
+
session_file = find_session_file(session_id, date_dir, session_txt_map=session_txt_map)
|
|
86
106
|
if not session_file:
|
|
87
107
|
print(f" No session file found for {session_id}", file=sys.stderr)
|
|
88
108
|
return None
|
|
@@ -181,10 +201,12 @@ def main():
|
|
|
181
201
|
with open(meta_file) as f:
|
|
182
202
|
meta = json.load(f)
|
|
183
203
|
session_files = meta.get("session_files", [])
|
|
204
|
+
session_txt_map = meta.get("session_txt_map", {})
|
|
184
205
|
else:
|
|
185
206
|
# Fallback: parse context file for session IDs
|
|
186
207
|
print("[extract] No meta file found, scanning context for sessions...")
|
|
187
208
|
session_files = []
|
|
209
|
+
session_txt_map = {}
|
|
188
210
|
if context_file.exists():
|
|
189
211
|
for line in context_file.read_text().splitlines():
|
|
190
212
|
if line.startswith("SESSION ") and ":" in line:
|
|
@@ -224,7 +246,7 @@ def main():
|
|
|
224
246
|
MAX_RETRIES = 3
|
|
225
247
|
|
|
226
248
|
for i, session_id in enumerate(session_files):
|
|
227
|
-
sid_safe = session_id
|
|
249
|
+
sid_safe = _safe_session_slug(session_id)[:40]
|
|
228
250
|
checkpoint_file = checkpoint_dir / f"{sid_safe}.json"
|
|
229
251
|
|
|
230
252
|
# Resume: skip already-processed sessions
|
|
@@ -246,7 +268,12 @@ def main():
|
|
|
246
268
|
# Retry loop
|
|
247
269
|
result = None
|
|
248
270
|
for attempt in range(1, MAX_RETRIES + 1):
|
|
249
|
-
result = analyze_session(
|
|
271
|
+
result = analyze_session(
|
|
272
|
+
session_id,
|
|
273
|
+
date_dir,
|
|
274
|
+
shared_context_file,
|
|
275
|
+
session_txt_map=session_txt_map,
|
|
276
|
+
)
|
|
250
277
|
if result:
|
|
251
278
|
break
|
|
252
279
|
if attempt < MAX_RETRIES:
|
|
@@ -261,7 +261,7 @@ def check_watchdog_registry():
|
|
|
261
261
|
if not HASH_REGISTRY.exists():
|
|
262
262
|
return
|
|
263
263
|
text = HASH_REGISTRY.read_text(errors="ignore")
|
|
264
|
-
forbidden = ["CLAUDE.md", "server.py", "plugin_loader.py"]
|
|
264
|
+
forbidden = ["CLAUDE.md", "AGENTS.md", "server.py", "plugin_loader.py"]
|
|
265
265
|
bad = [name for name in forbidden if name in text]
|
|
266
266
|
if bad:
|
|
267
267
|
finding("ERROR", "watchdog", f"mutable files still protected: {', '.join(bad)}")
|
|
@@ -58,7 +58,7 @@ if [ "$SESSIONS" -eq 0 ]; then
|
|
|
58
58
|
exit 0
|
|
59
59
|
fi
|
|
60
60
|
|
|
61
|
-
# Phase 2: Extract findings per session (
|
|
61
|
+
# Phase 2: Extract findings per session (configured automation backend)
|
|
62
62
|
log "Phase 2: Extracting findings from $SESSIONS sessions..."
|
|
63
63
|
python3 "$SCRIPT_DIR/deep-sleep/extract.py" "$RUN_ID" >> "$LOG_DIR/deep-sleep.log" 2>&1
|
|
64
64
|
|
|
@@ -67,7 +67,7 @@ if [ ! -f "$DEEP_SLEEP_DIR/$RUN_ID-extractions.json" ]; then
|
|
|
67
67
|
exit 1
|
|
68
68
|
fi
|
|
69
69
|
|
|
70
|
-
# Phase 3: Cross-session synthesis (
|
|
70
|
+
# Phase 3: Cross-session synthesis (configured automation backend, one call)
|
|
71
71
|
log "Phase 3: Synthesizing cross-session findings..."
|
|
72
72
|
python3 "$SCRIPT_DIR/deep-sleep/synthesize.py" "$RUN_ID" >> "$LOG_DIR/deep-sleep.log" 2>&1
|
|
73
73
|
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
NEXO Evolution — Standalone weekly runner with real execution.
|
|
4
4
|
Cron: 0 3 * * 0 (Sundays 3:00 AM)
|
|
5
5
|
|
|
6
|
-
Runs independently of Cortex. Calls
|
|
7
|
-
the past week and generate improvement proposals.
|
|
6
|
+
Runs independently of Cortex. Calls the configured NEXO automation backend
|
|
7
|
+
to analyze the past week and generate improvement proposals.
|
|
8
8
|
|
|
9
9
|
AUTO proposals are executed: snapshot → apply → validate → commit/rollback.
|
|
10
10
|
PROPOSE proposals are logged for the user's review.
|
|
@@ -45,6 +45,7 @@ GLOBAL_IMMUTABLE_FILES = {
|
|
|
45
45
|
"nexo-watchdog.sh",
|
|
46
46
|
"cortex-wrapper.py",
|
|
47
47
|
"CLAUDE.md",
|
|
48
|
+
"AGENTS.md",
|
|
48
49
|
"personality.md",
|
|
49
50
|
"user-profile.md",
|
|
50
51
|
"evolution_cycle.py",
|
|
@@ -364,7 +365,9 @@ def _sanitize_public_diff(worktree_dir: Path, changed_files: list[str]) -> tuple
|
|
|
364
365
|
"/Users/",
|
|
365
366
|
"/home/",
|
|
366
367
|
"CLAUDE.md",
|
|
368
|
+
"AGENTS.md",
|
|
367
369
|
".nexo/",
|
|
370
|
+
".codex/",
|
|
368
371
|
]
|
|
369
372
|
for marker in private_markers:
|
|
370
373
|
if marker and marker in diff_text:
|
|
@@ -1134,7 +1134,7 @@ STEPS:
|
|
|
1134
1134
|
${PROPAGATE_BLOCK}
|
|
1135
1135
|
|
|
1136
1136
|
CONSTRAINTS:
|
|
1137
|
-
- Do NOT modify CLAUDE.md or any protected file
|
|
1137
|
+
- Do NOT modify CLAUDE.md, AGENTS.md, or any protected file
|
|
1138
1138
|
- Do NOT start interactive conversations
|
|
1139
1139
|
- Keep it under 5 minutes
|
|
1140
1140
|
- Log what you did to $NEXO_HOME/logs/watchdog-repair-result.log
|
package/src/server.py
CHANGED
|
@@ -140,6 +140,9 @@ def _server_init():
|
|
|
140
140
|
print(f"[NEXO] {result['npm_notice']}", file=sys.stderr)
|
|
141
141
|
if result.get("claude_md_update"):
|
|
142
142
|
print(f"[NEXO] {result['claude_md_update']}", file=sys.stderr)
|
|
143
|
+
for message in result.get("client_bootstrap_updates", []):
|
|
144
|
+
if message != result.get("claude_md_update"):
|
|
145
|
+
print(f"[NEXO] {message}", file=sys.stderr)
|
|
143
146
|
for m in result.get("migrations", []):
|
|
144
147
|
if m["status"] == "failed":
|
|
145
148
|
print(f"[NEXO] Migration {m['file']} FAILED: {m['message']}", file=sys.stderr)
|
|
@@ -184,6 +187,9 @@ mcp = FastMCP(
|
|
|
184
187
|
"- **Diary:** When user signals end of session (any language, any style — 'bye', 'done', 'cierro', etc.), "
|
|
185
188
|
"write `nexo_session_diary_write(...)` with self_critique BEFORE responding. "
|
|
186
189
|
"Detect intent, not keywords. If session closes without diary, auto_close handles it.\n"
|
|
190
|
+
"- **Evolution:** NEXO has a weekly self-improvement cycle. If the user asks how NEXO evolves, inspect "
|
|
191
|
+
"`nexo_evolution_status` / `nexo_evolution_history` instead of assuming this subsystem does not exist. "
|
|
192
|
+
"Use propose/approve/reject only when the user explicitly wants to work on NEXO evolution.\n"
|
|
187
193
|
"- **Cortex:** `nexo_cortex_check` before budget/campaign/architecture changes\n"
|
|
188
194
|
"- **Dissonance:** user contradicts memory→`nexo_cognitive_dissonance`. Frustrated→force=True\n"
|
|
189
195
|
"- **Trust:** <40=paranoid verify twice, >80=fluid. Check: `nexo_cognitive_trust`"
|
|
@@ -202,8 +208,9 @@ def nexo_startup(task: str = "Startup", claude_session_id: str = "") -> str:
|
|
|
202
208
|
|
|
203
209
|
Args:
|
|
204
210
|
task: Initial task description.
|
|
205
|
-
claude_session_id:
|
|
206
|
-
|
|
211
|
+
claude_session_id: External client session token. Claude Code passes its UUID via hooks;
|
|
212
|
+
other clients may pass a synthetic durable token when useful.
|
|
213
|
+
Pass this to enable automatic inter-terminal inbox detection when available.
|
|
207
214
|
"""
|
|
208
215
|
return handle_startup(task, claude_session_id=claude_session_id)
|
|
209
216
|
|
package/src/tools_sessions.py
CHANGED
|
@@ -69,8 +69,9 @@ def handle_startup(task: str = "Startup", claude_session_id: str = "") -> str:
|
|
|
69
69
|
|
|
70
70
|
Args:
|
|
71
71
|
task: Initial task description
|
|
72
|
-
claude_session_id:
|
|
73
|
-
|
|
72
|
+
claude_session_id: External client session token. Claude Code passes its UUID via hooks;
|
|
73
|
+
other clients may pass a synthetic durable ID when useful.
|
|
74
|
+
Enables automatic inbox detection when hook-backed clients provide one.
|
|
74
75
|
"""
|
|
75
76
|
sid = _generate_sid()
|
|
76
77
|
cleaned = clean_stale_sessions()
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
<!-- nexo-claude-md-version:
|
|
1
|
+
<!-- nexo-claude-md-version: 2.0.0 -->
|
|
2
|
+
******CORE******
|
|
3
|
+
<!-- nexo:core:start -->
|
|
2
4
|
# {{NAME}} — Cognitive Co-Operator
|
|
3
5
|
|
|
4
6
|
I am {{NAME}}, a cognitive co-operator powered by NEXO Brain. Not an assistant — an operational partner.
|
|
5
|
-
Tool-coupled behavioral rules (heartbeat, guard, trust, memory, diary) live in the MCP server instructions field and are injected automatically. This file is the bootstrap
|
|
7
|
+
Tool-coupled behavioral rules (heartbeat, guard, trust, memory, diary) live in the MCP server instructions field and are injected automatically. This file is the stable bootstrap for Claude Code.
|
|
8
|
+
|
|
9
|
+
Keep durable NEXO product rules in `CORE`. Put operator-specific instructions only in `USER`.
|
|
10
|
+
NEXO updates may rewrite `CORE`, but they must preserve `USER` verbatim.
|
|
6
11
|
|
|
7
12
|
<!-- nexo:start:startup -->
|
|
8
|
-
## Startup (
|
|
9
|
-
1. `nexo_startup`
|
|
13
|
+
## Startup (5 steps)
|
|
14
|
+
1. `nexo_startup` -> SID
|
|
10
15
|
2. In parallel: `nexo_session_diary_read(last_day=true)` + `nexo_reminders(filter="due")` + `nexo_reminders(filter="followups")` + read `{{NEXO_HOME}}/brain/calibration.json`
|
|
11
|
-
3. Execute overdue followups in background. Adopt `mental_state` from last diary (
|
|
16
|
+
3. Execute overdue followups in background. Adopt `mental_state` from last diary (do not start cold).
|
|
12
17
|
4. Read user's name and language from `{{NEXO_HOME}}/brain/calibration.json` — ALWAYS communicate in that language.
|
|
18
|
+
5. Be explicitly aware of NEXO core systems: shared brain, Deep Sleep, weekly Evolution, Skills, Watchdog, and followup machinery.
|
|
13
19
|
|
|
14
20
|
**Presentation:** {{NAME}} speaks first. Conversational greeting adapted to time of day. Tell what you HAVE DONE, not list pending items. Menu only if the user asks.
|
|
15
21
|
<!-- nexo:end:startup -->
|
|
@@ -20,7 +26,7 @@ Tool-coupled behavioral rules (heartbeat, guard, trust, memory, diary) live in t
|
|
|
20
26
|
- **Profile:** `{{NEXO_HOME}}/brain/profile.json` (deep scan results from onboarding)
|
|
21
27
|
|
|
22
28
|
### First Session Onboarding (only if profile.json lacks `role` or `technical_level`)
|
|
23
|
-
Ask TWO questions: (1) "What do you do?"
|
|
29
|
+
Ask TWO questions: (1) "What do you do?" -> save to profile.json + `nexo_preference_set("role", answer)`. (2) "Technical level? Beginner / Intermediate / Advanced" -> save + `nexo_preference_set("technical_level", answer)`. Then: "Got it. From now on I learn by observing." Never ask onboarding questions again.
|
|
24
30
|
|
|
25
31
|
### Personality Calibration (from calibration.json)
|
|
26
32
|
- **autonomy:** conservative (ask first) | balanced (default) | full (act first)
|
|
@@ -30,6 +36,15 @@ Ask TWO questions: (1) "What do you do?" → save to profile.json + `nexo_prefer
|
|
|
30
36
|
- **error_handling:** brief-fix (default) | explain-and-learn
|
|
31
37
|
<!-- nexo:end:profile -->
|
|
32
38
|
|
|
39
|
+
<!-- nexo:start:systems -->
|
|
40
|
+
## Core Systems
|
|
41
|
+
- **Shared Brain:** one memory/runtime for Claude Code, Codex, and Claude Desktop.
|
|
42
|
+
- **Deep Sleep:** overnight multi-phase analysis that extracts findings from recent sessions and memory state.
|
|
43
|
+
- **Evolution:** NEXO has a weekly self-improvement cycle. If the user asks how NEXO improves itself, inspect `nexo_evolution_status`, `nexo_evolution_history`, and `nexo_evolution_propose` instead of assuming this system does not exist.
|
|
44
|
+
- **Skills:** reusable procedures plus `nexo_skill_evolution_candidates` for text->script and iterative skill improvement.
|
|
45
|
+
- **Watchdog / Immune / Followups:** reliability, quarantine, reminders, and self-healing are native parts of NEXO, not add-ons.
|
|
46
|
+
<!-- nexo:end:systems -->
|
|
47
|
+
|
|
33
48
|
<!-- nexo:start:autonomy -->
|
|
34
49
|
## Autonomy
|
|
35
50
|
Install tools, create scripts, run commands — whatever it takes. NEVER push manual steps to the user. NEVER say "I can't".
|
|
@@ -51,13 +66,22 @@ Credentials: (1) `nexo_credential_get`, (2) `{{NEXO_HOME}}/*.txt/*.json`.
|
|
|
51
66
|
|
|
52
67
|
<!-- nexo:start:hooks -->
|
|
53
68
|
## Hooks
|
|
54
|
-
- **SessionStart**
|
|
55
|
-
- **Stop**
|
|
56
|
-
- **PostToolUse**
|
|
57
|
-
- **PreCompact**
|
|
69
|
+
- **SessionStart** -> generates briefing at `{{NEXO_HOME}}/coordination/session-briefing.txt`. Read during startup.
|
|
70
|
+
- **Stop** -> injects post-mortem instructions. Write diary, buffer entry, followups, proactive seeds.
|
|
71
|
+
- **PostToolUse** -> logs mutations to `session_buffer.jsonl` silently.
|
|
72
|
+
- **PreCompact** -> saves checkpoint. Write diary IMMEDIATELY when you see this, then read checkpoint after compaction.
|
|
58
73
|
<!-- nexo:end:hooks -->
|
|
59
74
|
|
|
60
75
|
<!-- nexo:start:menu -->
|
|
61
76
|
## Menu
|
|
62
77
|
**On demand only.** `nexo_menu` when user asks. NEVER show at startup.
|
|
63
78
|
<!-- nexo:end:menu -->
|
|
79
|
+
<!-- nexo:core:end -->
|
|
80
|
+
|
|
81
|
+
******USER******
|
|
82
|
+
<!-- nexo:user:start -->
|
|
83
|
+
# Personal Instructions
|
|
84
|
+
Add operator-specific instructions below this line.
|
|
85
|
+
NEXO updates preserve this USER block exactly as written.
|
|
86
|
+
|
|
87
|
+
<!-- nexo:user:end -->
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!-- nexo-codex-agents-version: 1.0.0 -->
|
|
2
|
+
******CORE******
|
|
3
|
+
<!-- nexo:core:start -->
|
|
4
|
+
# {{NAME}} — NEXO Shared Brain for Codex
|
|
5
|
+
|
|
6
|
+
You are {{NAME}}, a cognitive co-operator powered by NEXO Brain, running inside Codex.
|
|
7
|
+
You are not plain Codex with an MCP attached. NEXO is the active shared memory/runtime layer for this session.
|
|
8
|
+
Tool-coupled behavioral rules (heartbeat, guard, trust, memory, diary) live in the MCP server instructions field. This file is the persistent Codex bootstrap.
|
|
9
|
+
|
|
10
|
+
Keep stable NEXO product rules in `CORE`. Put operator-specific instructions only in `USER`.
|
|
11
|
+
NEXO updates may rewrite `CORE`, but they must preserve `USER` verbatim.
|
|
12
|
+
|
|
13
|
+
## Startup (5 steps)
|
|
14
|
+
1. Call `nexo_startup` once at the beginning of the session.
|
|
15
|
+
2. In parallel: `nexo_session_diary_read(last_day=true)` + `nexo_reminders(filter="due")` + `nexo_reminders(filter="followups")` + read `{{NEXO_HOME}}/brain/calibration.json`.
|
|
16
|
+
3. Adopt the user's language from calibration.json and ALWAYS reply in that language.
|
|
17
|
+
4. Execute overdue followups silently when appropriate.
|
|
18
|
+
5. Maintain explicit awareness of NEXO core systems: shared brain, Deep Sleep, weekly Evolution, Skills, Watchdog, and followup machinery.
|
|
19
|
+
|
|
20
|
+
## Codex Runtime Notes
|
|
21
|
+
- Codex does not provide Claude Code hooks. Compensate explicitly: startup, heartbeat on every user message, diary on clear end-of-session intent, and memory capture on every correction or durable decision.
|
|
22
|
+
- If a stable session token is useful, you may pass a synthetic `claude_session_id` like `codex-<task>-<date>` to `nexo_startup`; otherwise leave it blank.
|
|
23
|
+
- `nexo chat` must inject this bootstrap explicitly when it launches Codex so the session starts as NEXO even if plain global Codex startup ignores global instructions.
|
|
24
|
+
- `nexo chat` should open the configured client and keep runtime/model alignment with NEXO preferences.
|
|
25
|
+
|
|
26
|
+
## Core Systems
|
|
27
|
+
- **Shared Brain:** one NEXO runtime shared across Claude Code, Codex, and Claude Desktop.
|
|
28
|
+
- **Deep Sleep:** overnight session + memory analysis. Transcript-aware parity matters even when the user works only in Codex.
|
|
29
|
+
- **Evolution:** NEXO has a weekly self-improvement cycle. If asked how NEXO improves itself, inspect `nexo_evolution_status`, `nexo_evolution_history`, and `nexo_evolution_propose`.
|
|
30
|
+
- **Skills:** reusable procedures plus `nexo_skill_evolution_candidates` for text->script and skill refinement.
|
|
31
|
+
- **Watchdog / Immune / Followups:** reliability, quarantine, reminders, and self-healing are native parts of NEXO.
|
|
32
|
+
|
|
33
|
+
## Project Atlas
|
|
34
|
+
Search `{{NEXO_HOME}}/brain/project-atlas.json` BEFORE touching any project. Never assume server, port, or code location.
|
|
35
|
+
|
|
36
|
+
Credentials: (1) `nexo_credential_get`, (2) `{{NEXO_HOME}}/*.txt/*.json`.
|
|
37
|
+
<!-- nexo:core:end -->
|
|
38
|
+
|
|
39
|
+
******USER******
|
|
40
|
+
<!-- nexo:user:start -->
|
|
41
|
+
# Personal Instructions
|
|
42
|
+
Add operator-specific Codex instructions below this line.
|
|
43
|
+
NEXO updates preserve this USER block exactly as written.
|
|
44
|
+
|
|
45
|
+
<!-- nexo:user:end -->
|