nexo-brain 2.6.21 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +72 -20
  3. package/hooks/hooks.json +79 -0
  4. package/package.json +1 -1
  5. package/src/agent_runner.py +296 -8
  6. package/src/cli.py +209 -4
  7. package/src/client_preferences.py +115 -0
  8. package/src/client_sync.py +202 -2
  9. package/src/cognitive/__init__.py +1 -1
  10. package/src/cognitive/_search.py +39 -19
  11. package/src/dashboard/app.py +264 -0
  12. package/src/dashboard/templates/base.html +4 -0
  13. package/src/dashboard/templates/dashboard.html +59 -1
  14. package/src/dashboard/templates/protocol.html +199 -0
  15. package/src/db/__init__.py +23 -1
  16. package/src/db/_learnings.py +31 -4
  17. package/src/db/_personal_scripts.py +12 -0
  18. package/src/db/_protocol.py +303 -0
  19. package/src/db/_schema.py +248 -0
  20. package/src/db/_watchers.py +173 -0
  21. package/src/db/_workflow.py +952 -0
  22. package/src/doctor/providers/runtime.py +1095 -3
  23. package/src/evolution_cycle.py +62 -0
  24. package/src/hook_guardrails.py +308 -0
  25. package/src/hooks/protocol-guardrail.sh +10 -0
  26. package/src/nexo_sdk.py +103 -0
  27. package/src/plugins/cognitive_memory.py +18 -0
  28. package/src/plugins/cortex.py +55 -35
  29. package/src/plugins/guard.py +132 -16
  30. package/src/plugins/protocol.py +911 -0
  31. package/src/plugins/schedule.py +40 -6
  32. package/src/plugins/simple_api.py +103 -0
  33. package/src/plugins/skills.py +67 -0
  34. package/src/plugins/state_watchers.py +79 -0
  35. package/src/plugins/workflow.py +588 -0
  36. package/src/public_contribution.py +86 -12
  37. package/src/script_registry.py +142 -0
  38. package/src/scripts/deep-sleep/apply_findings.py +482 -2
  39. package/src/scripts/deep-sleep/collect.py +49 -4
  40. package/src/scripts/nexo-agent-run.py +2 -0
  41. package/src/scripts/nexo-daily-self-audit.py +843 -5
  42. package/src/scripts/nexo-evolution-run.py +343 -1
  43. package/src/server.py +92 -6
  44. package/src/skills_runtime.py +151 -0
  45. package/src/state_watchers_runtime.py +334 -0
  46. package/src/tools_learnings.py +345 -7
  47. package/src/tools_sessions.py +183 -0
  48. package/templates/CLAUDE.md.template +9 -1
  49. package/templates/CODEX.AGENTS.md.template +10 -2
@@ -390,6 +390,68 @@ Quality over quantity. One strong improvement is better than three weak ones.
390
390
  """
391
391
 
392
392
 
393
+ def build_public_pr_review_prompt(
394
+ *,
395
+ pr_number: int,
396
+ title: str,
397
+ author: str,
398
+ url: str,
399
+ body: str,
400
+ files: list[str],
401
+ diff_text: str,
402
+ ) -> str:
403
+ """Prompt for peer-reviewing another public evolution PR.
404
+
405
+ This is used only when this machine already has its own Draft PR open, so
406
+ Evolution can still add value without opening a second PR.
407
+ """
408
+
409
+ rendered_files = "\n".join(f"- {path}" for path in files[:40]) if files else "- (no file list provided)"
410
+ trimmed_diff = (diff_text or "").strip()
411
+ if len(trimmed_diff) > 80000:
412
+ trimmed_diff = trimmed_diff[:80000] + "\n\n[diff truncated by NEXO]"
413
+
414
+ return f"""You are NEXO Public Evolution Review.
415
+
416
+ You are reviewing another opt-in public evolution PR. You must NOT merge, rebase,
417
+ push, or edit the PR. Your only job is to decide whether it deserves an approval
418
+ or whether it should receive a review comment without approval.
419
+
420
+ STRICT RULES:
421
+ - Review only this PR:
422
+ - Number: #{pr_number}
423
+ - Author: {author}
424
+ - URL: {url}
425
+ - Base the review only on the provided title, body, file list, and diff
426
+ - Do not assume hidden context
427
+ - If confidence is not strong, choose `comment`, not `approve`
428
+ - If the diff is too incomplete, too risky, or too ambiguous, choose `skip`
429
+ - Never suggest merge authority; maintainers decide that later
430
+ - Keep the review concise, technical, and useful
431
+
432
+ PR TITLE:
433
+ {title}
434
+
435
+ PR BODY:
436
+ {body or "(empty)"}
437
+
438
+ FILES CHANGED:
439
+ {rendered_files}
440
+
441
+ DIFF:
442
+ ```diff
443
+ {trimmed_diff or "(empty diff)"}
444
+ ```
445
+
446
+ Return ONLY valid JSON:
447
+ {{
448
+ "decision": "approve|comment|skip",
449
+ "summary": "one-line verdict",
450
+ "body": "the exact markdown text to post as the review body"
451
+ }}
452
+ """
453
+
454
+
393
455
  def max_auto_changes(total_evolutions: int) -> int:
394
456
  """Progressive trust: 1 for first 4 cycles, 2 for next 4, then 3."""
395
457
  if total_evolutions < 4:
@@ -0,0 +1,308 @@
1
+ from __future__ import annotations
2
+
3
+ """Post-tool guardrails for conditioned file learnings."""
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from db import create_protocol_debt, get_db
10
+ from plugins.guard import _load_conditioned_learnings, _normalize_path_token
11
+
12
+ READ_LIKE_TOOLS = {"Read"}
13
+ WRITE_LIKE_TOOLS = {"Edit", "MultiEdit", "Write"}
14
+ DELETE_LIKE_TOOLS = {"Delete"}
15
+
16
+
17
+ def _operation_kind(tool_name: str) -> str:
18
+ if tool_name in READ_LIKE_TOOLS:
19
+ return "read"
20
+ if tool_name in WRITE_LIKE_TOOLS:
21
+ return "write"
22
+ if tool_name in DELETE_LIKE_TOOLS:
23
+ return "delete"
24
+ return "other"
25
+
26
+
27
+ def _normalize_file_path(path: str) -> str:
28
+ return _normalize_path_token(str(Path(path)))
29
+
30
+
31
+ def _extract_touched_files(tool_input) -> list[str]:
32
+ files: list[str] = []
33
+ if not isinstance(tool_input, dict):
34
+ return files
35
+
36
+ def add(candidate) -> None:
37
+ if isinstance(candidate, str) and candidate.strip():
38
+ files.append(candidate.strip())
39
+
40
+ add(tool_input.get("file_path"))
41
+ add(tool_input.get("path"))
42
+
43
+ for key in ("paths", "file_paths", "files"):
44
+ value = tool_input.get(key)
45
+ if isinstance(value, list):
46
+ for item in value:
47
+ if isinstance(item, str):
48
+ add(item)
49
+ elif isinstance(item, dict):
50
+ add(item.get("file_path"))
51
+ add(item.get("path"))
52
+
53
+ unique: list[str] = []
54
+ seen = set()
55
+ for item in files:
56
+ normalized = _normalize_file_path(item)
57
+ if normalized and normalized not in seen:
58
+ seen.add(normalized)
59
+ unique.append(item)
60
+ return unique
61
+
62
+
63
+ def _resolve_nexo_sid(conn, external_session_id: str) -> str:
64
+ if not external_session_id.strip():
65
+ return ""
66
+ row = conn.execute(
67
+ """SELECT sid
68
+ FROM sessions
69
+ WHERE external_session_id = ? OR claude_session_id = ?
70
+ ORDER BY last_update_epoch DESC
71
+ LIMIT 1""",
72
+ (external_session_id.strip(), external_session_id.strip()),
73
+ ).fetchone()
74
+ return str(row["sid"]) if row else ""
75
+
76
+
77
+ def _find_open_task_for_file(conn, sid: str, filepath: str) -> dict | None:
78
+ target = _normalize_file_path(filepath)
79
+ rows = conn.execute(
80
+ """SELECT task_id, files, guard_has_blocking
81
+ FROM protocol_tasks
82
+ WHERE session_id = ? AND status = 'open'
83
+ ORDER BY opened_at DESC""",
84
+ (sid,),
85
+ ).fetchall()
86
+ for row in rows:
87
+ try:
88
+ files = json.loads(row["files"] or "[]")
89
+ except Exception:
90
+ files = []
91
+ for item in files if isinstance(files, list) else []:
92
+ if _normalize_file_path(str(item)) == target:
93
+ return dict(row)
94
+ return None
95
+
96
+
97
+ def _find_open_debt(conn, *, session_id: str, task_id: str, debt_type: str, file_token: str) -> dict | None:
98
+ row = conn.execute(
99
+ """SELECT *
100
+ FROM protocol_debt
101
+ WHERE status = 'open'
102
+ AND session_id = ?
103
+ AND task_id = ?
104
+ AND debt_type = ?
105
+ AND INSTR(evidence, ?) > 0
106
+ ORDER BY id DESC
107
+ LIMIT 1""",
108
+ (session_id, task_id, debt_type, file_token),
109
+ ).fetchone()
110
+ return dict(row) if row else None
111
+
112
+
113
+ def _find_task_guard_blocking_debt(conn, task_id: str) -> dict | None:
114
+ row = conn.execute(
115
+ """SELECT *
116
+ FROM protocol_debt
117
+ WHERE status = 'open'
118
+ AND task_id = ?
119
+ AND debt_type = 'unacknowledged_guard_blocking'
120
+ ORDER BY id DESC
121
+ LIMIT 1""",
122
+ (task_id,),
123
+ ).fetchone()
124
+ return dict(row) if row else None
125
+
126
+
127
+ def _ensure_protocol_debt(
128
+ conn,
129
+ *,
130
+ session_id: str,
131
+ task_id: str,
132
+ debt_type: str,
133
+ severity: str,
134
+ evidence: str,
135
+ file_token: str,
136
+ ) -> dict:
137
+ existing = _find_open_debt(
138
+ conn,
139
+ session_id=session_id,
140
+ task_id=task_id,
141
+ debt_type=debt_type,
142
+ file_token=file_token,
143
+ )
144
+ if existing:
145
+ return existing
146
+ return create_protocol_debt(
147
+ session_id,
148
+ debt_type,
149
+ severity=severity,
150
+ task_id=task_id,
151
+ evidence=evidence,
152
+ )
153
+
154
+
155
+ def process_tool_event(payload: dict) -> dict:
156
+ tool_name = str(payload.get("tool_name", "")).strip()
157
+ op = _operation_kind(tool_name)
158
+ if op == "other":
159
+ return {"ok": True, "skipped": True, "reason": "tool not monitored"}
160
+
161
+ tool_input = payload.get("tool_input")
162
+ files = _extract_touched_files(tool_input)
163
+ if not files:
164
+ return {"ok": True, "skipped": True, "reason": "no touched files found"}
165
+
166
+ conn = get_db()
167
+ sid = _resolve_nexo_sid(conn, str(payload.get("session_id", "")))
168
+ if not sid:
169
+ return {"ok": True, "skipped": True, "reason": "session not mapped to nexo"}
170
+
171
+ conditioned = _load_conditioned_learnings(conn, files)
172
+ warnings: list[dict] = []
173
+ violations: list[dict] = []
174
+
175
+ for filepath in files:
176
+ hits = conditioned.get(filepath) or []
177
+ if not hits:
178
+ continue
179
+ learning_ids = [int(row["id"]) for row in hits]
180
+ task = _find_open_task_for_file(conn, sid, filepath)
181
+
182
+ if op == "read":
183
+ if not task:
184
+ evidence = (
185
+ f"{tool_name} read conditioned file {filepath} linked to learning IDs {learning_ids} "
186
+ "without an open protocol task."
187
+ )
188
+ debt = _ensure_protocol_debt(
189
+ conn,
190
+ session_id=sid,
191
+ task_id="",
192
+ debt_type="conditioned_file_read_without_protocol",
193
+ severity="warn",
194
+ evidence=evidence,
195
+ file_token=filepath,
196
+ )
197
+ warnings.append(
198
+ {
199
+ "file": filepath,
200
+ "learning_ids": learning_ids,
201
+ "debt_id": debt.get("id"),
202
+ "debt_type": "conditioned_file_read_without_protocol",
203
+ "message": "Read conditioned file outside protocol task; review the file rules before any write/delete step.",
204
+ }
205
+ )
206
+ continue
207
+
208
+ if not task:
209
+ evidence = (
210
+ f"{tool_name} touched conditioned file {filepath} linked to learning IDs {learning_ids} "
211
+ f"without an open protocol task."
212
+ )
213
+ debt = _ensure_protocol_debt(
214
+ conn,
215
+ session_id=sid,
216
+ task_id="",
217
+ debt_type="conditioned_file_touch_without_protocol",
218
+ severity="error",
219
+ evidence=evidence,
220
+ file_token=filepath,
221
+ )
222
+ violations.append(
223
+ {
224
+ "file": filepath,
225
+ "learning_ids": learning_ids,
226
+ "task_id": "",
227
+ "debt_id": debt.get("id"),
228
+ "debt_type": "conditioned_file_touch_without_protocol",
229
+ }
230
+ )
231
+ continue
232
+
233
+ guard_debt = _find_task_guard_blocking_debt(conn, task["task_id"])
234
+ if guard_debt:
235
+ evidence = (
236
+ f"{tool_name} touched conditioned file {filepath} linked to learning IDs {learning_ids} "
237
+ f"before acknowledging blocking guard debt for task {task['task_id']}."
238
+ )
239
+ debt = _ensure_protocol_debt(
240
+ conn,
241
+ session_id=sid,
242
+ task_id=task["task_id"],
243
+ debt_type="conditioned_file_touch_without_guard_ack",
244
+ severity="error",
245
+ evidence=evidence,
246
+ file_token=filepath,
247
+ )
248
+ violations.append(
249
+ {
250
+ "file": filepath,
251
+ "learning_ids": learning_ids,
252
+ "task_id": task["task_id"],
253
+ "debt_id": debt.get("id"),
254
+ "debt_type": "conditioned_file_touch_without_guard_ack",
255
+ }
256
+ )
257
+
258
+ return {
259
+ "ok": True,
260
+ "session_id": sid,
261
+ "tool_name": tool_name,
262
+ "operation": op,
263
+ "warnings": warnings,
264
+ "violations": violations,
265
+ "status": "violation" if violations else ("warn" if warnings else "clean"),
266
+ }
267
+
268
+
269
+ def format_hook_message(result: dict) -> str:
270
+ if not result.get("violations") and not result.get("warnings"):
271
+ return ""
272
+ lines = ["NEXO DISCIPLINE:"]
273
+ for item in result.get("warnings", []):
274
+ if item.get("debt_id"):
275
+ lines.append(
276
+ f"- REVIEW FILE RULES: {item['file']} -> learnings {item['learning_ids']}. "
277
+ f"{item['message']} (debt={item['debt_type']}, debt_id={item['debt_id']})"
278
+ )
279
+ else:
280
+ lines.append(
281
+ f"- REVIEW FILE RULES: {item['file']} -> learnings {item['learning_ids']}. "
282
+ f"{item['message']}"
283
+ )
284
+ for item in result.get("violations", []):
285
+ lines.append(
286
+ f"- DEBT RECORDED: {item['debt_type']} on {item['file']} "
287
+ f"(task={item['task_id'] or 'none'}, debt_id={item['debt_id']}, learnings={item['learning_ids']})"
288
+ )
289
+ return "\n".join(lines)
290
+
291
+
292
+ def main() -> int:
293
+ raw = sys.stdin.read()
294
+ if not raw.strip():
295
+ return 0
296
+ try:
297
+ payload = json.loads(raw)
298
+ except Exception:
299
+ return 0
300
+ result = process_tool_event(payload)
301
+ message = format_hook_message(result)
302
+ if message:
303
+ print(message)
304
+ return 0
305
+
306
+
307
+ if __name__ == "__main__":
308
+ raise SystemExit(main())
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ # NEXO PostToolUse hook — conditioned file discipline guardrail
3
+
4
+ INPUT=$(cat || true)
5
+ [ -z "$INPUT" ] && exit 0
6
+
7
+ NEXO_CODE="${NEXO_CODE:-${HOME}/.nexo}"
8
+ python3 "$NEXO_CODE/hook_guardrails.py" <<< "$INPUT" 2>/dev/null || true
9
+
10
+ exit 0
@@ -0,0 +1,103 @@
1
+ """Minimal Python SDK for the public NEXO mental model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass
11
+ class NEXOClient:
12
+ """Tiny Python wrapper around `nexo call` for common public operations."""
13
+
14
+ nexo_bin: str = "nexo"
15
+
16
+ def call(self, tool: str, payload: dict | None = None) -> dict | list | str:
17
+ result = subprocess.run(
18
+ [
19
+ self.nexo_bin,
20
+ "call",
21
+ tool,
22
+ "--input",
23
+ json.dumps(payload or {}, ensure_ascii=False),
24
+ "--json-output",
25
+ ],
26
+ capture_output=True,
27
+ text=True,
28
+ check=False,
29
+ )
30
+ if result.returncode != 0:
31
+ raise RuntimeError((result.stderr or result.stdout or f"{tool} failed").strip())
32
+ text = (result.stdout or "").strip()
33
+ if not text:
34
+ return {}
35
+ try:
36
+ return json.loads(text)
37
+ except json.JSONDecodeError:
38
+ return {"result": text}
39
+
40
+ def remember(
41
+ self,
42
+ content: str,
43
+ *,
44
+ title: str = "",
45
+ domain: str = "",
46
+ source_type: str = "note",
47
+ tags: str = "",
48
+ bypass_gate: bool = True,
49
+ ) -> dict | list | str:
50
+ return self.call(
51
+ "nexo_remember",
52
+ {
53
+ "content": content,
54
+ "title": title,
55
+ "domain": domain,
56
+ "source_type": source_type,
57
+ "tags": tags,
58
+ "bypass_gate": bypass_gate,
59
+ },
60
+ )
61
+
62
+ def recall(self, query: str, *, days: int = 30) -> dict | list | str:
63
+ return self.call("nexo_memory_recall", {"query": query, "days": days})
64
+
65
+ def consolidate(
66
+ self,
67
+ *,
68
+ max_insights: int = 12,
69
+ threshold: float = 0.9,
70
+ dry_run: bool = False,
71
+ ) -> dict | list | str:
72
+ return self.call(
73
+ "nexo_consolidate",
74
+ {
75
+ "max_insights": max_insights,
76
+ "threshold": threshold,
77
+ "dry_run": dry_run,
78
+ },
79
+ )
80
+
81
+ def run_workflow(
82
+ self,
83
+ sid: str,
84
+ goal: str,
85
+ *,
86
+ steps: list[dict] | str,
87
+ goal_id: str = "",
88
+ shared_state: dict | str | None = None,
89
+ owner: str = "",
90
+ idempotency_key: str = "",
91
+ ) -> dict | list | str:
92
+ return self.call(
93
+ "nexo_run_workflow",
94
+ {
95
+ "sid": sid,
96
+ "goal": goal,
97
+ "steps": steps if isinstance(steps, str) else json.dumps(steps, ensure_ascii=False),
98
+ "goal_id": goal_id,
99
+ "shared_state": shared_state if isinstance(shared_state, str) else json.dumps(shared_state or {}, ensure_ascii=False),
100
+ "owner": owner,
101
+ "idempotency_key": idempotency_key,
102
+ },
103
+ )
@@ -533,6 +533,23 @@ def handle_cognitive_trigger_check(text: str, use_semantic: bool = False) -> str
533
533
  return "\n".join(lines)
534
534
 
535
535
 
536
+ def handle_cognitive_trigger_preview(text: str, use_semantic: bool = False) -> str:
537
+ """Preview prospective trigger matches without firing them."""
538
+ matches = cognitive.preview_triggers(text, use_semantic=use_semantic)
539
+ if not matches:
540
+ return "No anticipatory warnings."
541
+
542
+ lines = [f"ANTICIPATORY WARNINGS: {len(matches)}", ""]
543
+ for match in matches:
544
+ lines.append(f" #{match['id']} [{match['match_type']}] pattern='{match['pattern']}'")
545
+ lines.append(f" ACTION: {match['action']}")
546
+ if match.get("context"):
547
+ lines.append(f" context: {match['context']}")
548
+ lines.append("")
549
+
550
+ return "\n".join(lines)
551
+
552
+
536
553
  def handle_cognitive_trigger_delete(trigger_id: int) -> str:
537
554
  """Delete a prospective memory trigger.
538
555
 
@@ -570,6 +587,7 @@ TOOLS = [
570
587
  (handle_cognitive_quarantine_process, "nexo_cognitive_quarantine_process", "Run quarantine promotion cycle — evaluate pending items against policy."),
571
588
  (handle_cognitive_trigger_create, "nexo_cognitive_trigger_create", "Create a prospective memory trigger — 'when X is mentioned, remind about Y'."),
572
589
  (handle_cognitive_trigger_list, "nexo_cognitive_trigger_list", "List prospective triggers by status (armed/fired/all)."),
590
+ (handle_cognitive_trigger_preview, "nexo_cognitive_trigger_preview", "Preview anticipatory trigger matches without firing them."),
573
591
  (handle_cognitive_trigger_check, "nexo_cognitive_trigger_check", "Check text against armed triggers. Returns fired triggers with actions."),
574
592
  (handle_cognitive_trigger_delete, "nexo_cognitive_trigger_delete", "Delete a prospective trigger by ID."),
575
593
  (handle_cognitive_trigger_rearm, "nexo_cognitive_trigger_rearm", "Re-arm a fired trigger so it can fire again."),
@@ -15,6 +15,7 @@ v0.1: Single MCP tool + middleware validation.
15
15
  """
16
16
 
17
17
  import json
18
+ import secrets
18
19
  import time
19
20
 
20
21
 
@@ -135,6 +136,51 @@ def _tools_for_mode(mode: str) -> list[str]:
135
136
  return ["all"]
136
137
 
137
138
 
139
+ def _parse_json_list(value) -> list:
140
+ try:
141
+ parsed = json.loads(value) if isinstance(value, str) else value
142
+ return parsed if isinstance(parsed, list) else []
143
+ except (json.JSONDecodeError, TypeError):
144
+ return []
145
+
146
+
147
+ def evaluate_cortex_state(state: dict) -> dict:
148
+ """Return structured Cortex evaluation for internal callers."""
149
+ result = _validate_state(state)
150
+ result["check_id"] = f"CTX-{int(time.time())}-{secrets.randbelow(100000)}"
151
+ result["expires_at_epoch"] = int(time.time()) + 1200
152
+ return result
153
+
154
+
155
+ def _log_cortex_activation(goal: str, task_type: str, result: dict):
156
+ try:
157
+ conn = _get_db()
158
+ conn.execute(
159
+ """CREATE TABLE IF NOT EXISTS cortex_log (
160
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
161
+ goal TEXT,
162
+ task_type TEXT,
163
+ mode TEXT,
164
+ warnings TEXT,
165
+ trust_score INTEGER,
166
+ created_at TEXT DEFAULT (datetime('now'))
167
+ )"""
168
+ )
169
+ conn.execute(
170
+ "INSERT INTO cortex_log (goal, task_type, mode, warnings, trust_score) VALUES (?, ?, ?, ?, ?)",
171
+ (
172
+ goal[:200],
173
+ task_type,
174
+ result["mode"],
175
+ json.dumps(result["warnings"]),
176
+ result["trust_score"],
177
+ ),
178
+ )
179
+ conn.commit()
180
+ except Exception:
181
+ pass
182
+
183
+
138
184
  def handle_cortex_check(
139
185
  goal: str,
140
186
  task_type: str = "answer",
@@ -172,31 +218,25 @@ def handle_cortex_check(
172
218
  Returns:
173
219
  Mode (ask/propose/act), available tools, warnings, and relevant Core Rules
174
220
  """
175
- # Parse JSON arrays safely
176
- def _parse(s):
177
- try:
178
- v = json.loads(s) if isinstance(s, str) else s
179
- return v if isinstance(v, list) else []
180
- except (json.JSONDecodeError, TypeError):
181
- return []
182
-
183
221
  state = {
184
222
  "goal": goal.strip() if goal else "",
185
223
  "task_type": task_type if task_type in ("answer", "analyze", "edit", "execute", "delegate") else "answer",
186
- "plan": _parse(plan),
187
- "known_facts": _parse(known_facts),
188
- "unknowns": _parse(unknowns),
189
- "constraints": _parse(constraints),
190
- "evidence_refs": _parse(evidence_refs),
224
+ "plan": _parse_json_list(plan),
225
+ "known_facts": _parse_json_list(known_facts),
226
+ "unknowns": _parse_json_list(unknowns),
227
+ "constraints": _parse_json_list(constraints),
228
+ "evidence_refs": _parse_json_list(evidence_refs),
191
229
  "verification_step": verification_step.strip() if verification_step else "",
192
230
  }
193
231
 
194
- result = _validate_state(state)
232
+ result = evaluate_cortex_state(state)
195
233
 
196
234
  # Format response
197
235
  lines = [
198
236
  f"CORTEX CHECK — mode: {result['mode'].upper()}",
199
237
  f"Trust: {result['trust_score']}/100",
238
+ f"Check ID: {result['check_id']}",
239
+ f"Valid until epoch: {result['expires_at_epoch']}",
200
240
  ]
201
241
 
202
242
  if result["mode"] == "act":
@@ -223,27 +263,7 @@ def handle_cortex_check(
223
263
  lines.append("")
224
264
  lines.append(f"Tools available: {', '.join(result['tools_available'])}")
225
265
 
226
- # Log cortex activation for metrics
227
- try:
228
- conn = _get_db()
229
- conn.execute(
230
- """CREATE TABLE IF NOT EXISTS cortex_log (
231
- id INTEGER PRIMARY KEY AUTOINCREMENT,
232
- goal TEXT,
233
- task_type TEXT,
234
- mode TEXT,
235
- warnings TEXT,
236
- trust_score INTEGER,
237
- created_at TEXT DEFAULT (datetime('now'))
238
- )"""
239
- )
240
- conn.execute(
241
- "INSERT INTO cortex_log (goal, task_type, mode, warnings, trust_score) VALUES (?, ?, ?, ?, ?)",
242
- (goal[:200], task_type, result["mode"], json.dumps(result["warnings"]), result["trust_score"])
243
- )
244
- conn.commit()
245
- except Exception:
246
- pass
266
+ _log_cortex_activation(goal, task_type, result)
247
267
 
248
268
  return "\n".join(lines)
249
269