nexo-brain 2.6.13 → 2.6.15

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.
@@ -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 Claude's Read tool.
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 (kept from collect_transcripts.py) ──────────────
56
+ # ── Transcript collection (Claude Code + Codex) ────────────────────────────
56
57
 
57
58
 
58
- def find_session_dirs() -> list[Path]:
59
- """Find all Claude Code project directories that contain .jsonl files."""
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
- dirs = set()
64
- for jsonl in claude_dir.rglob("*.jsonl"):
65
- dirs.add(jsonl.parent)
66
- return list(dirs)
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 extract_session(jsonl_path: Path) -> dict | None:
70
- """Extract clean transcript from a session JSONL file."""
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
- "session_file": jsonl_path.name,
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
- for sdir in find_session_dirs():
160
- for f in sdir.glob("*.jsonl"):
161
- try:
162
- mtime = datetime.fromtimestamp(f.stat().st_mtime)
163
- except OSError:
164
- continue
165
- if since_dt < mtime <= until_dt:
166
- session = extract_session(f)
167
- if session:
168
- session["modified"] = mtime.isoformat()
169
- sessions.append(session)
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
- sid_short = session["session_file"].replace(".jsonl", "")[:20]
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 find_session_file(session_id: str, date_dir: Path) -> Path | None:
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.replace(".jsonl", "")[:20]
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(session_id: str, date_dir: Path, shared_context_file: Path | None) -> dict | None:
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.replace(".jsonl", "")[:30]
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(session_id, date_dir, shared_context_file)
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 (Claude Opus)
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 (Claude Opus, one call)
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 Opus API directly to analyze
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: The Claude Code session UUID (from session-briefing or hook).
206
- Pass this to enable automatic inter-terminal inbox detection.
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
 
@@ -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: UUID from Claude Code (passed via SessionStart hook file).
73
- Enables automatic inbox detection via PostToolUse hook.
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.0.0 -->
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: identity, profile, format, and autonomy.
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 (4 steps)
9
- 1. `nexo_startup` SID
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 (don't start cold).
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?" 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.
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** generates briefing at `{{NEXO_HOME}}/coordination/session-briefing.txt`. Read during startup.
55
- - **Stop** injects post-mortem instructions. Write diary, buffer entry, followups, proactive seeds.
56
- - **PostToolUse** logs mutations to `session_buffer.jsonl` silently.
57
- - **PreCompact** saves checkpoint. Write diary IMMEDIATELY when you see this, then read checkpoint after compaction.
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 -->