nexo-brain 2.6.20 → 2.7.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.
@@ -30,6 +30,15 @@ Synthesize across all sessions:
30
30
  - Cross-domain connections where an older learning or session sample explains a current issue
31
31
  - Topics repeatedly mentioned over time but never formalized into a learning or followup
32
32
  - Project pressure that is rising because of repeated diary mentions, open followups, or adverse outcomes
33
+ - For medium/high-severity patterns, propose a concrete fix artifact:
34
+ - script
35
+ - hook
36
+ - checklist
37
+ - validation step
38
+ - workflow change
39
+ - guardrail
40
+
41
+ Do not stop at diagnosis. Turn repeated problems into concrete engineering work.
33
42
 
34
43
  ### 2. Morning Agenda
35
44
  Generate a prioritized agenda for the next morning:
@@ -126,6 +135,14 @@ Merge and deduplicate all findings into a final action list. Each action should
126
135
  - `dedupe_key`: deterministic key for idempotency
127
136
  - `content`: the actual data to write
128
137
 
138
+ When generating `followup_create`, prefer descriptions that start with a concrete verb and include the deliverable:
139
+ - "Add a pre-release validation script ..."
140
+ - "Implement a guard hook that ..."
141
+ - "Create a checklist for ..."
142
+ - "Write a watchdog check that ..."
143
+
144
+ Avoid vague followups that merely restate the diagnosis.
145
+
129
146
  ## Output Format
130
147
 
131
148
  Return ONLY valid JSON. No markdown code fences. No explanation text.
@@ -140,6 +157,12 @@ Return ONLY valid JSON. No markdown code fences. No explanation text.
140
157
  "pattern": "Description of the pattern",
141
158
  "sessions": ["session1.jsonl", "session2.jsonl"],
142
159
  "severity": "low|medium|high",
160
+ "proposed_fix": {
161
+ "title": "Short concrete fix title",
162
+ "description": "Concrete engineering change to make",
163
+ "deliverable": "script|hook|checklist|workflow|guardrail",
164
+ "confidence": 0.0
165
+ },
143
166
  "evidence": [
144
167
  {"type": "transcript", "session_id": "...", "message_index": 42, "quote": "..."}
145
168
  ]
@@ -14,6 +14,7 @@ import json
14
14
  import os
15
15
  import subprocess
16
16
  import sys
17
+ import hashlib
17
18
  from datetime import datetime
18
19
  from pathlib import Path
19
20
 
@@ -28,6 +29,7 @@ if str(NEXO_CODE) not in sys.path:
28
29
  from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
29
30
 
30
31
  CLAUDE_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
32
+ ACTION_VERBS = {"add", "implement", "create", "write", "build", "enforce", "automate", "validate", "guard", "fix", "review"}
31
33
 
32
34
 
33
35
  def extract_json_from_response(text: str) -> dict | None:
@@ -86,6 +88,96 @@ def collect_skill_runtime_candidates(target_date: str) -> tuple[Path, dict]:
86
88
  return output_file, payload
87
89
 
88
90
 
91
+ def _normalize_action_text(value: str) -> str:
92
+ return " ".join(str(value or "").strip().lower().split())
93
+
94
+
95
+ def _looks_concrete_action(text: str) -> bool:
96
+ words = {word.strip(".,:;()[]{}").lower() for word in str(text or "").split()}
97
+ return bool(words & ACTION_VERBS)
98
+
99
+
100
+ def _pattern_followup_from_fix(pattern: dict) -> dict | None:
101
+ severity = str(pattern.get("severity", "") or "").lower()
102
+ sessions = pattern.get("sessions", []) or []
103
+ if severity not in {"medium", "high"} and len(sessions) < 2:
104
+ return None
105
+
106
+ proposed_fix = pattern.get("proposed_fix") or {}
107
+ pattern_text = str(pattern.get("pattern", "") or "").strip()
108
+ title = str(proposed_fix.get("title", "") or "").strip()
109
+ description = str(proposed_fix.get("description", "") or "").strip()
110
+ deliverable = str(proposed_fix.get("deliverable", "") or proposed_fix.get("artifact", "") or "").strip()
111
+
112
+ if title and description:
113
+ if _looks_concrete_action(description):
114
+ followup_description = description
115
+ else:
116
+ followup_description = f"{title}: {description}"
117
+ elif description:
118
+ followup_description = description
119
+ elif title:
120
+ followup_description = title
121
+ elif pattern_text:
122
+ followup_description = (
123
+ f"Implement a concrete guardrail for recurring issue: {pattern_text}. "
124
+ "Deliverable should be a script, hook, checklist, or automated validation that prevents the same failure from repeating."
125
+ )
126
+ else:
127
+ return None
128
+
129
+ if deliverable and deliverable.lower() not in followup_description.lower():
130
+ followup_description = f"{followup_description} Deliverable: {deliverable}."
131
+ if not _looks_concrete_action(followup_description):
132
+ followup_description = f"Implement this fix: {followup_description}"
133
+
134
+ return {
135
+ "action_type": "followup_create",
136
+ "action_class": "auto_apply" if severity == "high" else "draft_for_morning",
137
+ "confidence": round(max(float(proposed_fix.get("confidence", 0.0) or 0.0), 0.86 if severity == "high" else 0.78), 2),
138
+ "impact": "high" if severity == "high" else "medium",
139
+ "reversibility": "reversible",
140
+ "evidence": pattern.get("evidence", []) or [],
141
+ "dedupe_key": "engineering-fix:" + hashlib.md5(
142
+ _normalize_action_text(followup_description).encode("utf-8")
143
+ ).hexdigest()[:16],
144
+ "content": {
145
+ "title": title or f"Engineering fix for: {pattern_text[:90]}",
146
+ "description": followup_description,
147
+ "date": "",
148
+ "reasoning": f"Deep Sleep engineering followup from recurring pattern: {pattern_text}",
149
+ },
150
+ }
151
+
152
+
153
+ def backfill_engineering_actions(payload: dict) -> dict:
154
+ if not isinstance(payload, dict):
155
+ return payload
156
+ actions = payload.get("actions")
157
+ if not isinstance(actions, list):
158
+ actions = []
159
+ payload["actions"] = actions
160
+
161
+ existing_keys = {str(action.get("dedupe_key", "") or "") for action in actions}
162
+ existing_descriptions = {
163
+ _normalize_action_text(action.get("content", {}).get("description", ""))
164
+ for action in actions
165
+ if isinstance(action, dict)
166
+ }
167
+
168
+ for pattern in payload.get("cross_session_patterns", []) or []:
169
+ action = _pattern_followup_from_fix(pattern)
170
+ if not action:
171
+ continue
172
+ description = _normalize_action_text(action["content"]["description"])
173
+ if action["dedupe_key"] in existing_keys or description in existing_descriptions:
174
+ continue
175
+ actions.append(action)
176
+ existing_keys.add(action["dedupe_key"])
177
+ existing_descriptions.add(description)
178
+ return payload
179
+
180
+
89
181
  def main():
90
182
  target_date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
91
183
 
@@ -177,6 +269,8 @@ def main():
177
269
  print(f"[synthesize] Failed to parse JSON. Raw output saved to {debug_file}", file=sys.stderr)
178
270
  sys.exit(1)
179
271
 
272
+ parsed = backfill_engineering_actions(parsed)
273
+
180
274
  # Write synthesis output
181
275
  output_file = DEEP_SLEEP_DIR / f"{target_date}-synthesis.json"
182
276
  with open(output_file, "w") as f:
@@ -16,6 +16,36 @@ from pathlib import Path
16
16
 
17
17
 
18
18
  NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
19
+ DEFAULT_ALLOWED_TOOLS = "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"
20
+
21
+
22
+ def _load_schedule() -> dict:
23
+ schedule_path = NEXO_HOME / "config" / "schedule.json"
24
+ if not schedule_path.is_file():
25
+ return {}
26
+ try:
27
+ return json.loads(schedule_path.read_text())
28
+ except Exception:
29
+ return {}
30
+
31
+
32
+ def _resolve_automation_backend() -> str:
33
+ data = _load_schedule()
34
+ return str(data.get("automation_backend", "claude_code") or "claude_code")
35
+
36
+
37
+ def _load_bootstrap_prompt() -> str:
38
+ backend = _resolve_automation_backend()
39
+ if backend == "codex":
40
+ path = Path.home() / ".codex" / "AGENTS.md"
41
+ else:
42
+ path = Path.home() / ".claude" / "CLAUDE.md"
43
+ if not path.is_file():
44
+ return ""
45
+ try:
46
+ return path.read_text()
47
+ except Exception:
48
+ return ""
19
49
 
20
50
 
21
51
  def run_nexo(args: list[str]) -> str:
@@ -57,6 +87,9 @@ def run_automation_text(
57
87
  model: str = "",
58
88
  reasoning_effort: str = "",
59
89
  cwd: str = "",
90
+ allowed_tools: str = DEFAULT_ALLOWED_TOOLS,
91
+ append_system_prompt: str = "",
92
+ include_bootstrap: bool = True,
60
93
  ) -> str:
61
94
  """Run the configured NEXO automation backend and return text output.
62
95
 
@@ -75,6 +108,17 @@ def run_automation_text(
75
108
  cmd.extend(["--reasoning-effort", reasoning_effort])
76
109
  if cwd:
77
110
  cmd.extend(["--cwd", cwd])
111
+ merged_system_prompt = []
112
+ if include_bootstrap:
113
+ bootstrap = _load_bootstrap_prompt()
114
+ if bootstrap:
115
+ merged_system_prompt.append(bootstrap)
116
+ if append_system_prompt:
117
+ merged_system_prompt.append(append_system_prompt)
118
+ if merged_system_prompt:
119
+ cmd.extend(["--append-system-prompt", "\n\n".join(merged_system_prompt)])
120
+ if allowed_tools:
121
+ cmd.extend(["--allowed-tools", allowed_tools])
78
122
 
79
123
  env = os.environ.copy()
80
124
  env.setdefault("NEXO_HOME", str(NEXO_HOME))