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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +19 -11
- package/bin/nexo-brain.js +483 -56
- package/package.json +4 -1
- package/src/agent_runner.py +322 -0
- package/src/auto_update.py +12 -3
- package/src/cli.py +22 -10
- package/src/client_preferences.py +394 -0
- package/src/client_sync.py +78 -0
- package/src/cron_recovery.py +8 -1
- package/src/crons/manifest.json +6 -0
- package/src/crons/sync.py +14 -1
- package/src/doctor/providers/runtime.py +109 -1
- package/src/plugins/update.py +5 -1
- package/src/runtime_power.py +23 -0
- package/src/scripts/check-context.py +102 -100
- package/src/scripts/deep-sleep/extract.py +29 -54
- package/src/scripts/deep-sleep/synthesize.py +14 -38
- package/src/scripts/nexo-agent-run.py +73 -0
- package/src/scripts/nexo-catchup.py +15 -19
- package/src/scripts/nexo-daily-self-audit.py +17 -14
- package/src/scripts/nexo-evolution-run.py +25 -55
- package/src/scripts/nexo-immune.py +17 -15
- package/src/scripts/nexo-learning-validator.py +90 -58
- package/src/scripts/nexo-postmortem-consolidator.py +15 -14
- package/src/scripts/nexo-sleep.py +20 -14
- package/src/scripts/nexo-synthesis.py +19 -12
- package/src/scripts/nexo-update.sh +28 -2
- package/src/scripts/nexo-watchdog.sh +34 -10
- package/templates/nexo_helper.py +45 -0
- package/templates/plugin-template.py +4 -0
- package/templates/script-template.py +13 -2
- package/templates/skill-script-template.py +8 -0
|
@@ -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
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
92
|
-
"""Send a session to
|
|
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
|
-
|
|
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 =
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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"
|
|
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 =
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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"
|
|
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(
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
28
|
+
from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
|
|
31
29
|
|
|
32
|
-
|
|
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(
|
|
135
|
+
print("[synthesize] Automation backend: schedule-configured")
|
|
151
136
|
|
|
152
137
|
try:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
142
|
+
output_format="text",
|
|
143
|
+
allowed_tools="Read,Grep,Bash",
|
|
171
144
|
)
|
|
172
145
|
|
|
173
146
|
if result.returncode != 0:
|
|
174
|
-
print(f"[synthesize]
|
|
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]
|
|
198
|
+
print(f"[synthesize] Automation backend timeout ({CLAUDE_TIMEOUT}s)", file=sys.stderr)
|
|
223
199
|
sys.exit(1)
|
|
224
200
|
except FileNotFoundError:
|
|
225
|
-
print(
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 =
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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 (
|
|
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 (
|
|
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 =
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
# ──
|
|
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
|
-
# ──
|
|
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
|
|
206
|
-
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
903
|
+
# Verify the configured automation backend is available before calling
|
|
934
904
|
if not verify_claude_cli():
|
|
935
|
-
log("
|
|
905
|
+
log("Automation backend not available or not authenticated. Skipping evolution run.")
|
|
936
906
|
return
|
|
937
907
|
|
|
938
|
-
# Call
|
|
939
|
-
log("Calling
|
|
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"
|
|
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
|
|
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 =
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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:
|