nexo-brain 2.6.11 → 2.6.12

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  from __future__ import annotations
3
3
  """
4
- Deep Sleep v2 -- Phase 2: Extract findings from each session using Claude CLI.
4
+ Deep Sleep v2 -- Phase 2: Extract findings from each session using the configured automation backend.
5
5
 
6
6
  For each session in the context file, sends the extract-prompt.md to Claude
7
7
  and collects structured findings. Merges all per-session results into
@@ -12,35 +12,23 @@ Environment variables:
12
12
  """
13
13
  import json
14
14
  import os
15
- import shutil
16
15
  import subprocess
17
16
  import sys
18
17
  from datetime import datetime
19
18
  from pathlib import Path
20
19
 
21
20
  NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
21
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parents[2])))
22
22
  DEEP_SLEEP_DIR = NEXO_HOME / "operations" / "deep-sleep"
23
23
  PROMPT_FILE = Path(__file__).parent / "extract-prompt.md"
24
24
 
25
- # No timeout -- user pays unlimited Claude Code, sessions can take as long as needed
26
- CLAUDE_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
25
+ if str(NEXO_CODE) not in sys.path:
26
+ sys.path.insert(0, str(NEXO_CODE))
27
27
 
28
+ from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
28
29
 
29
- def find_claude_cli() -> str:
30
- """Find the Claude CLI binary."""
31
- # Check common locations
32
- candidates = [
33
- Path.home() / ".local" / "bin" / "claude",
34
- Path("/usr/local/bin/claude"),
35
- ]
36
- for c in candidates:
37
- if c.exists():
38
- return str(c)
39
- # Try PATH
40
- which = shutil.which("claude")
41
- if which:
42
- return which
43
- return "claude" # Fallback, let it fail with a clear error
30
+ # No timeout -- headless automation can take as long as needed
31
+ CLAUDE_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
44
32
 
45
33
 
46
34
  def extract_json_from_response(text: str) -> dict | None:
@@ -88,10 +76,10 @@ def find_session_file(session_id: str, date_dir: Path) -> Path | None:
88
76
  return None
89
77
 
90
78
 
91
- def analyze_session(session_id: str, date_dir: Path, shared_context_file: Path | None, claude_bin: str) -> dict | None:
92
- """Send a session to Claude CLI for extraction analysis.
79
+ def analyze_session(session_id: str, date_dir: Path, shared_context_file: Path | None) -> dict | None:
80
+ """Send a session to the automation backend for extraction analysis.
93
81
 
94
- Claude CLI reads the small per-session file + shared context file.
82
+ The backend reads the small per-session file + shared context file.
95
83
  Prompt is short — the heavy lifting is in the Read tool calls.
96
84
  """
97
85
  session_file = find_session_file(session_id, date_dir)
@@ -112,35 +100,23 @@ def analyze_session(session_id: str, date_dir: Path, shared_context_file: Path |
112
100
  prompt += shared_ctx_instruction
113
101
 
114
102
  try:
115
- env = os.environ.copy()
116
- env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
117
- env.pop("CLAUDECODE", None)
118
- env.pop("CLAUDE_CODE", None)
119
-
120
103
  JSON_SYSTEM_PROMPT = (
121
104
  "You are a JSON-only analyst. Your ENTIRE response must be a single valid JSON object. "
122
105
  "No text before it. No text after it. No markdown fences. No explanations. "
123
106
  "If you want to summarize, put it inside the JSON fields. Start with { and end with }."
124
107
  )
125
108
 
126
- result = subprocess.run(
127
- [
128
- claude_bin,
129
- "-p", prompt,
130
- "--model", "opus",
131
- "--output-format", "text",
132
- "--append-system-prompt", JSON_SYSTEM_PROMPT,
133
- "--allowedTools",
134
- "Read,Grep,Bash"
135
- ],
136
- capture_output=True,
137
- text=True,
109
+ result = run_automation_prompt(
110
+ prompt,
111
+ model="opus",
138
112
  timeout=CLAUDE_TIMEOUT,
139
- env=env
113
+ output_format="text",
114
+ append_system_prompt=JSON_SYSTEM_PROMPT,
115
+ allowed_tools="Read,Grep,Bash",
140
116
  )
141
117
 
142
118
  if result.returncode != 0:
143
- print(f" Claude CLI error (exit {result.returncode}): {result.stderr[:300]}", file=sys.stderr)
119
+ print(f" Automation backend error (exit {result.returncode}): {result.stderr[:300]}", file=sys.stderr)
144
120
  return None
145
121
 
146
122
  # Filter out stop hook contamination (e.g. "Post-mortem completo.")
@@ -160,11 +136,12 @@ def analyze_session(session_id: str, date_dir: Path, shared_context_file: Path |
160
136
  f"Required schema: session_id, findings[], emotional_timeline[], "
161
137
  f"abandoned_projects[], skill_candidates[], productivity_score, protocol_summary"
162
138
  )
163
- convert_result = subprocess.run(
164
- [claude_bin, "-p", convert_prompt, "--model", "sonnet",
165
- "--output-format", "text",
166
- "--append-system-prompt", JSON_SYSTEM_PROMPT],
167
- capture_output=True, text=True, timeout=120, env=env
139
+ convert_result = run_automation_prompt(
140
+ convert_prompt,
141
+ model="sonnet",
142
+ timeout=120,
143
+ output_format="text",
144
+ append_system_prompt=JSON_SYSTEM_PROMPT,
168
145
  )
169
146
  if convert_result.returncode == 0:
170
147
  parsed = extract_json_from_response(convert_result.stdout)
@@ -180,13 +157,12 @@ def analyze_session(session_id: str, date_dir: Path, shared_context_file: Path |
180
157
 
181
158
  return parsed
182
159
 
160
+ except AutomationBackendUnavailableError as exc:
161
+ print(f" Automation backend unavailable: {exc}", file=sys.stderr)
162
+ return None
183
163
  except subprocess.TimeoutExpired:
184
- print(f" Claude CLI timeout ({CLAUDE_TIMEOUT}s)", file=sys.stderr)
164
+ print(f" Automation backend timeout ({CLAUDE_TIMEOUT}s)", file=sys.stderr)
185
165
  return None
186
- except FileNotFoundError:
187
- print(f" Claude CLI not found at: {claude_bin}", file=sys.stderr)
188
- print(" Install: npm install -g @anthropic-ai/claude-code", file=sys.stderr)
189
- sys.exit(1)
190
166
 
191
167
 
192
168
  def main():
@@ -235,9 +211,8 @@ def main():
235
211
  shared_context_file = None
236
212
  print("[extract] No shared context file")
237
213
 
238
- claude_bin = find_claude_cli()
239
214
  print(f"[extract] Phase 2: Analyzing {len(session_files)} sessions for {target_date}")
240
- print(f"[extract] Claude CLI: {claude_bin}")
215
+ print("[extract] Automation backend: schedule-configured")
241
216
 
242
217
  # Checkpoint directory: one JSON per session, survives crashes
243
218
  checkpoint_dir = date_dir / "checkpoints"
@@ -271,7 +246,7 @@ def main():
271
246
  # Retry loop
272
247
  result = None
273
248
  for attempt in range(1, MAX_RETRIES + 1):
274
- result = analyze_session(session_id, date_dir, shared_context_file, claude_bin)
249
+ result = analyze_session(session_id, date_dir, shared_context_file)
275
250
  if result:
276
251
  break
277
252
  if attempt < MAX_RETRIES:
@@ -12,7 +12,6 @@ Environment variables:
12
12
  """
13
13
  import json
14
14
  import os
15
- import shutil
16
15
  import subprocess
17
16
  import sys
18
17
  from datetime import datetime
@@ -26,22 +25,9 @@ PROMPT_FILE = Path(__file__).parent / "synthesize-prompt.md"
26
25
  if str(NEXO_CODE) not in sys.path:
27
26
  sys.path.insert(0, str(NEXO_CODE))
28
27
 
29
- CLAUDE_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
30
-
28
+ from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
31
29
 
32
- def find_claude_cli() -> str:
33
- """Find the Claude CLI binary."""
34
- candidates = [
35
- Path.home() / ".local" / "bin" / "claude",
36
- Path("/usr/local/bin/claude"),
37
- ]
38
- for c in candidates:
39
- if c.exists():
40
- return str(c)
41
- which = shutil.which("claude")
42
- if which:
43
- return which
44
- return "claude"
30
+ CLAUDE_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
45
31
 
46
32
 
47
33
  def extract_json_from_response(text: str) -> dict | None:
@@ -144,34 +130,21 @@ def main():
144
130
  prompt = prompt.replace("{{CONTEXT_FILE}}", str(context_file))
145
131
  prompt = prompt.replace("{{SKILL_RUNTIME_FILE}}", str(runtime_candidates_file))
146
132
 
147
- claude_bin = find_claude_cli()
148
133
  print(f"[synthesize] Phase 3: Synthesizing {total_findings} findings from {target_date}")
149
134
  print(f"[synthesize] Skill runtime candidates: {runtime_candidate_count}")
150
- print(f"[synthesize] Claude CLI: {claude_bin}")
135
+ print("[synthesize] Automation backend: schedule-configured")
151
136
 
152
137
  try:
153
- env = os.environ.copy()
154
- env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
155
- env.pop("CLAUDECODE", None)
156
- env.pop("CLAUDE_CODE", None)
157
-
158
- result = subprocess.run(
159
- [
160
- claude_bin,
161
- "-p", prompt,
162
- "--model", "opus",
163
- "--output-format", "text",
164
- "--allowedTools",
165
- "Read,Grep,Bash"
166
- ],
167
- capture_output=True,
168
- text=True,
138
+ result = run_automation_prompt(
139
+ prompt,
140
+ model="opus",
169
141
  timeout=CLAUDE_TIMEOUT,
170
- env=env
142
+ output_format="text",
143
+ allowed_tools="Read,Grep,Bash",
171
144
  )
172
145
 
173
146
  if result.returncode != 0:
174
- print(f"[synthesize] Claude CLI error (exit {result.returncode}): {result.stderr[:300]}", file=sys.stderr)
147
+ print(f"[synthesize] Automation backend error (exit {result.returncode}): {result.stderr[:300]}", file=sys.stderr)
175
148
  sys.exit(1)
176
149
 
177
150
  # Filter hook contamination
@@ -218,11 +191,14 @@ def main():
218
191
  print(f" Context packets: {n_packets}")
219
192
  print(f"[synthesize] Output: {output_file}")
220
193
 
194
+ except AutomationBackendUnavailableError as exc:
195
+ print(f"[synthesize] Automation backend unavailable: {exc}", file=sys.stderr)
196
+ sys.exit(1)
221
197
  except subprocess.TimeoutExpired:
222
- print(f"[synthesize] Claude CLI timeout ({CLAUDE_TIMEOUT}s)", file=sys.stderr)
198
+ print(f"[synthesize] Automation backend timeout ({CLAUDE_TIMEOUT}s)", file=sys.stderr)
223
199
  sys.exit(1)
224
200
  except FileNotFoundError:
225
- print(f"[synthesize] Claude CLI not found at: {claude_bin}", file=sys.stderr)
201
+ print("[synthesize] Automation backend binary not found.", file=sys.stderr)
226
202
  sys.exit(1)
227
203
 
228
204
 
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ """Small CLI wrapper around the schedule-configured automation backend."""
5
+
6
+ import argparse
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ _SCRIPT_DIR = Path(__file__).resolve().parent
13
+ _DEFAULT_RUNTIME_ROOT = _SCRIPT_DIR.parent
14
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_DEFAULT_RUNTIME_ROOT)))
15
+ if str(NEXO_CODE) not in sys.path:
16
+ sys.path.insert(0, str(NEXO_CODE))
17
+
18
+ from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
19
+
20
+
21
+ def _read_text(path: str | None) -> str:
22
+ if not path:
23
+ return ""
24
+ return Path(path).expanduser().read_text()
25
+
26
+
27
+ def main(argv: list[str] | None = None) -> int:
28
+ parser = argparse.ArgumentParser(description="Run a prompt through the configured NEXO automation backend.")
29
+ parser.add_argument("--prompt", default="", help="Prompt text")
30
+ parser.add_argument("--prompt-file", default="", help="Read prompt text from a file")
31
+ parser.add_argument("--cwd", default="", help="Working directory for the backend")
32
+ parser.add_argument("--model", default="", help="Backend model hint")
33
+ parser.add_argument("--reasoning-effort", default="", help="Backend reasoning effort/profile")
34
+ parser.add_argument("--timeout", type=int, default=21600, help="Timeout in seconds")
35
+ parser.add_argument("--output-format", default="text", help="Requested output format")
36
+ parser.add_argument("--allowed-tools", default="", help="Claude-style allowed tools contract")
37
+ parser.add_argument("--append-system-prompt", default="", help="Extra system prompt text")
38
+ parser.add_argument("--append-system-prompt-file", default="", help="Read extra system prompt from a file")
39
+ args = parser.parse_args(argv)
40
+
41
+ prompt = args.prompt or _read_text(args.prompt_file)
42
+ if not prompt:
43
+ prompt = sys.stdin.read()
44
+ if not prompt.strip():
45
+ print("No prompt provided.", file=sys.stderr)
46
+ return 1
47
+
48
+ append_system_prompt = args.append_system_prompt or _read_text(args.append_system_prompt_file)
49
+
50
+ try:
51
+ result = run_automation_prompt(
52
+ prompt,
53
+ cwd=args.cwd or None,
54
+ model=args.model,
55
+ reasoning_effort=args.reasoning_effort,
56
+ timeout=args.timeout,
57
+ output_format=args.output_format,
58
+ append_system_prompt=append_system_prompt,
59
+ allowed_tools=args.allowed_tools,
60
+ )
61
+ except AutomationBackendUnavailableError as exc:
62
+ print(str(exc), file=sys.stderr)
63
+ return 2
64
+
65
+ if result.stdout:
66
+ print(result.stdout, end="" if result.stdout.endswith("\n") else "\n")
67
+ if result.stderr:
68
+ print(result.stderr, file=sys.stderr, end="" if result.stderr.endswith("\n") else "\n")
69
+ return int(result.returncode)
70
+
71
+
72
+ if __name__ == "__main__":
73
+ raise SystemExit(main())
@@ -19,6 +19,7 @@ _runtime_root = Path(os.environ.get("NEXO_CODE", str(_DEFAULT_RUNTIME_ROOT)))
19
19
  if str(_runtime_root) not in sys.path:
20
20
  sys.path.insert(0, str(_runtime_root))
21
21
 
22
+ from agent_runner import AutomationBackendUnavailableError, probe_automation_backend, run_automation_prompt
22
23
  from cron_recovery import catchup_candidates
23
24
 
24
25
  HOME = Path.home()
@@ -237,16 +238,12 @@ def main():
237
238
 
238
239
  def _cli_post_catchup_assessment(ran: int, skipped: int, state: dict):
239
240
  """When 3+ tasks were missed, use CLI to assess if there are concerns."""
240
- if not CLAUDE_CLI.exists():
241
- log(f"Caught up {ran} tasks, {skipped} already current. (CLI unavailable for assessment)")
242
- return
243
- auth_check = subprocess.run(
244
- [str(CLAUDE_CLI), "-p", "reply OK", "--output-format", "text"],
245
- capture_output=True, text=True, timeout=30
246
- )
247
- if auth_check.returncode != 0:
248
- # CLI not authenticated, skip gracefully
249
- print(f"[{datetime.now().strftime('%H:%M:%S')}] Claude CLI not authenticated. Skipping CLI analysis.")
241
+ probe = probe_automation_backend(timeout=30)
242
+ if not probe.get("ok"):
243
+ print(
244
+ f"[{datetime.now().strftime('%H:%M:%S')}] "
245
+ f"Automation backend unavailable. Skipping CLI analysis. ({probe.get('reason') or probe.get('stderr') or 'not ready'})"
246
+ )
250
247
  return
251
248
 
252
249
  assessment_file = LOG_DIR / "catchup-assessment.md"
@@ -273,21 +270,20 @@ Format:
273
270
  - Recommendation: ..."""
274
271
 
275
272
  log(f"Caught up {ran} tasks — running CLI assessment...")
276
- env = os.environ.copy()
277
- env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
278
- env.pop("CLAUDECODE", None)
279
- env.pop("CLAUDE_CODE", None)
280
-
281
273
  try:
282
- result = subprocess.run(
283
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text",
284
- "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
285
- capture_output=True, text=True, timeout=21600, env=env
274
+ result = run_automation_prompt(
275
+ prompt,
276
+ model="opus",
277
+ timeout=21600,
278
+ output_format="text",
279
+ allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
286
280
  )
287
281
  if result.returncode == 0:
288
282
  log(f"Assessment written to {assessment_file}")
289
283
  else:
290
284
  log(f"CLI assessment exited {result.returncode}")
285
+ except AutomationBackendUnavailableError as e:
286
+ log(f"CLI assessment skipped: {e}")
291
287
  except subprocess.TimeoutExpired:
292
288
  log("CLI assessment timed out (90s)")
293
289
  except Exception as e:
@@ -6,7 +6,7 @@ Stage A — Mechanical checks (Python pure, unchanged):
6
6
  18 checks: overdue reminders, disk space, DB size, stale sessions, guard stats,
7
7
  cognitive health, snapshot drift, etc. All pure queries, no intelligence needed.
8
8
 
9
- Stage B — Interpretation (Claude CLI opus):
9
+ Stage B — Interpretation (automation backend):
10
10
  Takes the raw findings from Stage A and UNDERSTANDS them:
11
11
  - Groups related findings
12
12
  - Identifies root causes
@@ -30,6 +30,10 @@ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
30
30
  _script_dir = Path(__file__).resolve().parent
31
31
  _repo_src = _script_dir.parent # src/scripts/ -> src/
32
32
  NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
33
+ if str(NEXO_CODE) not in sys.path:
34
+ sys.path.insert(0, str(NEXO_CODE))
35
+
36
+ from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
33
37
 
34
38
  LOG_DIR = NEXO_HOME / "logs"
35
39
  LOG_DIR.mkdir(parents=True, exist_ok=True)
@@ -432,7 +436,7 @@ def check_cognitive_health():
432
436
 
433
437
 
434
438
  # ═══════════════════════════════════════════════════════════════════════════════
435
- # Stage B: Interpretation (Claude CLI opus) — NEW in v2
439
+ # Stage B: Interpretation (automation backend) — NEW in v2
436
440
  # ═══════════════════════════════════════════════════════════════════════════════
437
441
 
438
442
  def interpret_findings(raw_findings: list) -> bool:
@@ -479,20 +483,16 @@ via sqlite3 nexo.db 'UPDATE...'" is useful.
479
483
 
480
484
  Also write the machine-readable summary to {LOG_DIR}/self-audit-summary.json.
481
485
 
482
- Execute without asking."""
483
-
484
- log("Stage B: Invoking Claude CLI (opus) for interpretation...")
485
- env = os.environ.copy()
486
- env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
487
- env.pop("CLAUDECODE", None)
488
- env.pop("CLAUDE_CODE", None)
486
+ Execute without asking."""
489
487
 
488
+ log("Stage B: Invoking automation backend for interpretation...")
490
489
  try:
491
- result = subprocess.run(
492
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
493
- "--output-format", "text",
494
- "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
495
- capture_output=True, text=True, timeout=21600, env=env
490
+ result = run_automation_prompt(
491
+ prompt,
492
+ model="opus",
493
+ timeout=21600,
494
+ output_format="text",
495
+ allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
496
496
  )
497
497
 
498
498
  if result.returncode != 0:
@@ -502,6 +502,9 @@ Execute without asking."""
502
502
  log(f"Stage B: Interpretation complete ({len(result.stdout or '')} chars)")
503
503
  return True
504
504
 
505
+ except AutomationBackendUnavailableError as e:
506
+ log(f"Stage B: automation backend unavailable: {e}")
507
+ return False
505
508
  except subprocess.TimeoutExpired:
506
509
  log("Stage B: CLI timed out")
507
510
  return False
@@ -122,7 +122,7 @@ def _immutable_files_for_mode(mode: str) -> set[str]:
122
122
  return set(GLOBAL_IMMUTABLE_FILES)
123
123
  return set(GLOBAL_IMMUTABLE_FILES) | set(STANDARD_MODE_IMMUTABLE_FILES)
124
124
 
125
- # ── Claude CLI path ──────────────────────────────────────────────────────
125
+ # ── Automation backend pathing ───────────────────────────────────────────
126
126
  def _resolve_claude_cli() -> Path:
127
127
  """Find claude CLI: saved path > PATH > common locations."""
128
128
  import shutil as _shutil
@@ -169,6 +169,7 @@ def log(msg: str):
169
169
 
170
170
  # ── Import from evolution_cycle.py (lives in NEXO_CODE, i.e. src/) ──────
171
171
  sys.path.insert(0, str(NEXO_CODE))
172
+ from agent_runner import probe_automation_backend, run_automation_prompt
172
173
  from evolution_cycle import (
173
174
  load_objective, save_objective, get_week_data, build_evolution_prompt,
174
175
  dry_run_restore_test, max_auto_changes, create_snapshot, build_public_contribution_prompt
@@ -197,39 +198,23 @@ def set_consecutive_failures(count: int):
197
198
  save_objective(obj)
198
199
 
199
200
 
200
- # ── Claude CLI call ──────────────────────────────────────────────────────
201
+ # ── Automation backend call ──────────────────────────────────────────────
201
202
  CLI_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
202
203
 
203
204
 
204
205
  def verify_claude_cli() -> bool:
205
- """Check Claude CLI is available and authenticated."""
206
- if not CLAUDE_CLI.exists():
207
- return False
208
- try:
209
- result = subprocess.run(
210
- [str(CLAUDE_CLI), "-p", "reply OK", "--output-format", "text"],
211
- capture_output=True, text=True, timeout=30
212
- )
213
- return result.returncode == 0
214
- except Exception:
215
- return False
206
+ """Check the configured automation backend is available and authenticated."""
207
+ return bool(probe_automation_backend(timeout=30).get("ok"))
216
208
 
217
209
 
218
210
  def call_claude_cli(prompt: str) -> str:
219
- """Call claude -p prompt --model opus via subprocess. Returns stdout text."""
220
- env = os.environ.copy()
221
- env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
222
- env.pop("CLAUDECODE", None)
223
- env.pop("CLAUDE_CODE", None)
224
-
225
- result = subprocess.run(
226
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
227
- "--output-format", "text",
228
- "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
229
- capture_output=True,
230
- text=True,
211
+ """Call the configured automation backend for the managed evolution prompt."""
212
+ result = run_automation_prompt(
213
+ prompt,
214
+ model="opus",
231
215
  timeout=CLI_TIMEOUT,
232
- env=env,
216
+ output_format="text",
217
+ allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
233
218
  )
234
219
  if result.returncode != 0:
235
220
  raise RuntimeError(f"claude CLI exited {result.returncode}: {result.stderr[:500]}")
@@ -237,30 +222,15 @@ def call_claude_cli(prompt: str) -> str:
237
222
 
238
223
 
239
224
  def call_public_claude_cli(prompt: str, *, cwd: Path) -> str:
240
- """Run Claude CLI in an isolated public repo checkout."""
241
- env = os.environ.copy()
242
- env["NEXO_HEADLESS"] = "1"
243
- env["NEXO_PUBLIC_CONTRIBUTION"] = "1"
244
- env.pop("CLAUDECODE", None)
245
- env.pop("CLAUDE_CODE", None)
246
-
247
- result = subprocess.run(
248
- [
249
- str(CLAUDE_CLI),
250
- "-p",
251
- prompt,
252
- "--model",
253
- "opus",
254
- "--output-format",
255
- "text",
256
- "--allowedTools",
257
- "Read,Write,Edit,Glob,Grep,Bash",
258
- ],
259
- cwd=str(cwd),
260
- capture_output=True,
261
- text=True,
225
+ """Run the configured automation backend in an isolated public repo checkout."""
226
+ result = run_automation_prompt(
227
+ prompt,
228
+ cwd=cwd,
229
+ env={"NEXO_PUBLIC_CONTRIBUTION": "1"},
230
+ model="opus",
262
231
  timeout=CLI_TIMEOUT,
263
- env=env,
232
+ output_format="text",
233
+ allowed_tools="Read,Write,Edit,Glob,Grep,Bash",
264
234
  )
265
235
  if result.returncode != 0:
266
236
  raise RuntimeError(f"claude CLI exited {result.returncode}: {result.stderr[:500]}")
@@ -514,7 +484,7 @@ def run_public_contribution_cycle(*, objective: dict, cycle_num: int) -> None:
514
484
  return
515
485
 
516
486
  if not verify_claude_cli():
517
- log("Claude CLI not available or not authenticated. Skipping public contribution run.")
487
+ log("Automation backend not available or not authenticated. Skipping public contribution run.")
518
488
  mark_public_contribution_result(result="skipped:claude_cli_unavailable", config=config)
519
489
  return
520
490
 
@@ -930,17 +900,17 @@ def run():
930
900
  prompt = build_evolution_prompt(week_data, objective)
931
901
  log(f"Prompt built: {len(prompt)} chars")
932
902
 
933
- # Verify Claude CLI is authenticated before calling
903
+ # Verify the configured automation backend is available before calling
934
904
  if not verify_claude_cli():
935
- log("Claude CLI not available or not authenticated. Skipping evolution run.")
905
+ log("Automation backend not available or not authenticated. Skipping evolution run.")
936
906
  return
937
907
 
938
- # Call Opus via claude -p
939
- log("Calling claude -p --model opus...")
908
+ # Call the configured automation backend with the legacy opus task profile
909
+ log("Calling automation backend with the opus task profile...")
940
910
  try:
941
911
  raw_response = call_claude_cli(prompt)
942
912
  except Exception as e:
943
- log(f"claude CLI call failed: {e}")
913
+ log(f"Automation backend call failed: {e}")
944
914
  set_consecutive_failures(failures + 1)
945
915
  return
946
916
 
@@ -24,6 +24,14 @@ from datetime import datetime, date, timedelta
24
24
  from pathlib import Path
25
25
 
26
26
  NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
27
+ _script_dir = Path(__file__).resolve().parent
28
+ _repo_src = _script_dir.parent
29
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
30
+ if str(NEXO_CODE) not in sys.path:
31
+ sys.path.insert(0, str(NEXO_CODE))
32
+
33
+ from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
34
+
27
35
  from urllib.request import Request, urlopen
28
36
  from urllib.error import URLError, HTTPError
29
37
 
@@ -855,11 +863,7 @@ def _run_checks(lock_fd):
855
863
 
856
864
 
857
865
  def _run_cli_triage(all_results: dict, repairs: list, counts: dict):
858
- """Pass all findings to Claude CLI for intelligent triage and recommendations."""
859
- if not CLAUDE_CLI.exists():
860
- print("[SKIP] Claude CLI not found, skipping triage")
861
- return
862
-
866
+ """Pass all findings to the configured automation backend for intelligent triage and recommendations."""
863
867
  triage_file = COORD_DIR / "immune-triage.md"
864
868
  findings_json = json.dumps({
865
869
  "timestamp": NOW.strftime("%Y-%m-%d %H:%M"),
@@ -901,22 +905,20 @@ Raw findings:
901
905
  Write the report. Be concise — max 40 lines."""
902
906
 
903
907
  print("\n[TRIAGE] Running CLI interpretation...")
904
- env = os.environ.copy()
905
- env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
906
- env.pop("CLAUDECODE", None)
907
- env.pop("CLAUDE_CODE", None)
908
-
909
908
  try:
910
- result = subprocess.run(
911
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
912
- "--output-format", "text",
913
- "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
914
- capture_output=True, text=True, timeout=21600, env=env
909
+ result = run_automation_prompt(
910
+ prompt,
911
+ model="opus",
912
+ timeout=21600,
913
+ output_format="text",
914
+ allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
915
915
  )
916
916
  if result.returncode == 0:
917
917
  print(f"[TRIAGE] Report written to {triage_file}")
918
918
  else:
919
919
  print(f"[TRIAGE] CLI exited {result.returncode}: {result.stderr[:200]}")
920
+ except AutomationBackendUnavailableError as e:
921
+ print(f"[TRIAGE] Skipping triage: {e}")
920
922
  except subprocess.TimeoutExpired:
921
923
  print("[TRIAGE] CLI timed out (120s)")
922
924
  except Exception as e: