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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +72 -20
- package/hooks/hooks.json +79 -0
- package/package.json +1 -1
- package/src/agent_runner.py +296 -8
- package/src/cli.py +209 -4
- package/src/client_preferences.py +115 -0
- package/src/client_sync.py +202 -2
- package/src/cognitive/__init__.py +1 -1
- package/src/cognitive/_search.py +39 -19
- package/src/dashboard/app.py +264 -0
- package/src/dashboard/templates/base.html +4 -0
- package/src/dashboard/templates/dashboard.html +59 -1
- package/src/dashboard/templates/protocol.html +199 -0
- package/src/db/__init__.py +23 -1
- package/src/db/_learnings.py +31 -4
- package/src/db/_personal_scripts.py +12 -0
- package/src/db/_protocol.py +303 -0
- package/src/db/_schema.py +248 -0
- package/src/db/_watchers.py +173 -0
- package/src/db/_workflow.py +952 -0
- package/src/doctor/providers/runtime.py +1095 -3
- package/src/evolution_cycle.py +62 -0
- package/src/hook_guardrails.py +308 -0
- package/src/hooks/protocol-guardrail.sh +10 -0
- package/src/nexo_sdk.py +103 -0
- package/src/plugins/cognitive_memory.py +18 -0
- package/src/plugins/cortex.py +55 -35
- package/src/plugins/guard.py +132 -16
- package/src/plugins/protocol.py +911 -0
- package/src/plugins/schedule.py +40 -6
- package/src/plugins/simple_api.py +103 -0
- package/src/plugins/skills.py +67 -0
- package/src/plugins/state_watchers.py +79 -0
- package/src/plugins/workflow.py +588 -0
- package/src/public_contribution.py +86 -12
- package/src/script_registry.py +142 -0
- package/src/scripts/deep-sleep/apply_findings.py +482 -2
- package/src/scripts/deep-sleep/collect.py +49 -4
- package/src/scripts/nexo-agent-run.py +2 -0
- package/src/scripts/nexo-daily-self-audit.py +843 -5
- package/src/scripts/nexo-evolution-run.py +343 -1
- package/src/server.py +92 -6
- package/src/skills_runtime.py +151 -0
- package/src/state_watchers_runtime.py +334 -0
- package/src/tools_learnings.py +345 -7
- package/src/tools_sessions.py +183 -0
- package/templates/CLAUDE.md.template +9 -1
- package/templates/CODEX.AGENTS.md.template +10 -2
|
@@ -120,52 +120,93 @@ def save_public_contribution_config(config: dict) -> dict:
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
def _gh(*args: str, cwd: Path | None = None, timeout: int = 20) -> subprocess.CompletedProcess:
|
|
123
|
+
env = os.environ.copy()
|
|
124
|
+
token = (
|
|
125
|
+
str(env.get("GH_TOKEN") or env.get("GITHUB_TOKEN") or "").strip()
|
|
126
|
+
or _github_token_from_credentials()
|
|
127
|
+
)
|
|
128
|
+
if token:
|
|
129
|
+
env["GH_TOKEN"] = token
|
|
123
130
|
return subprocess.run(
|
|
124
131
|
["gh", *args],
|
|
125
132
|
cwd=str(cwd) if cwd else None,
|
|
126
133
|
capture_output=True,
|
|
127
134
|
text=True,
|
|
128
135
|
timeout=timeout,
|
|
136
|
+
env=env,
|
|
129
137
|
)
|
|
130
138
|
|
|
131
139
|
|
|
140
|
+
def _github_token_from_credentials() -> str:
|
|
141
|
+
try:
|
|
142
|
+
from db import get_credential
|
|
143
|
+
except Exception:
|
|
144
|
+
return ""
|
|
145
|
+
for key in ("token", "gh_token", "github_token"):
|
|
146
|
+
try:
|
|
147
|
+
matches = get_credential("github", key)
|
|
148
|
+
except Exception:
|
|
149
|
+
continue
|
|
150
|
+
for item in matches or []:
|
|
151
|
+
value = str(item.get("value") or "").strip()
|
|
152
|
+
if value:
|
|
153
|
+
return value
|
|
154
|
+
return ""
|
|
155
|
+
|
|
156
|
+
|
|
132
157
|
def github_auth_status() -> dict:
|
|
133
158
|
if not shutil.which("gh"):
|
|
134
|
-
return {"ok": False, "message": "GitHub CLI not found.", "login": ""}
|
|
159
|
+
return {"ok": False, "message": "GitHub CLI not found.", "login": "", "code": "gh_missing"}
|
|
135
160
|
try:
|
|
136
161
|
result = _gh("api", "user", timeout=20)
|
|
137
162
|
except Exception as e:
|
|
138
|
-
return {"ok": False, "message": str(e), "login": ""}
|
|
163
|
+
return {"ok": False, "message": str(e), "login": "", "code": "gh_error"}
|
|
139
164
|
if result.returncode != 0:
|
|
140
|
-
|
|
165
|
+
message = (result.stderr or result.stdout).strip()
|
|
166
|
+
lowered = message.lower()
|
|
167
|
+
code = "auth_missing"
|
|
168
|
+
if "keychain" in lowered:
|
|
169
|
+
code = "keychain_blocked"
|
|
170
|
+
elif "token" in lowered or "authentication" in lowered or "login" in lowered:
|
|
171
|
+
code = "auth_missing"
|
|
172
|
+
return {"ok": False, "message": message, "login": "", "code": code}
|
|
141
173
|
try:
|
|
142
174
|
payload = json.loads(result.stdout or "{}")
|
|
143
175
|
login = str(payload.get("login") or "").strip()
|
|
144
176
|
except Exception:
|
|
145
177
|
login = ""
|
|
146
|
-
return {"ok": bool(login), "message": "", "login": login}
|
|
178
|
+
return {"ok": bool(login), "message": "", "login": login, "code": "ok" if login else "auth_missing"}
|
|
147
179
|
|
|
148
180
|
|
|
149
181
|
def ensure_fork(login: str) -> dict:
|
|
150
182
|
if not login:
|
|
151
|
-
return {"ok": False, "message": "Missing GitHub login.", "fork_repo": ""}
|
|
183
|
+
return {"ok": False, "message": "Missing GitHub login.", "fork_repo": "", "code": "missing_login"}
|
|
152
184
|
fork_repo = f"{login}/nexo"
|
|
153
185
|
if not shutil.which("gh"):
|
|
154
|
-
return {"ok": False, "message": "GitHub CLI not found.", "fork_repo": ""}
|
|
186
|
+
return {"ok": False, "message": "GitHub CLI not found.", "fork_repo": "", "code": "gh_missing"}
|
|
155
187
|
try:
|
|
156
188
|
check = _gh("repo", "view", fork_repo, "--json", "nameWithOwner", timeout=20)
|
|
157
189
|
if check.returncode == 0:
|
|
158
|
-
return {"ok": True, "message": "", "fork_repo": fork_repo}
|
|
190
|
+
return {"ok": True, "message": "", "fork_repo": fork_repo, "code": "ok"}
|
|
159
191
|
create = _gh("repo", "fork", UPSTREAM_REPO, "--clone=false", "--remote=false", timeout=60)
|
|
160
192
|
if create.returncode == 0:
|
|
161
|
-
return {"ok": True, "message": "", "fork_repo": fork_repo}
|
|
193
|
+
return {"ok": True, "message": "", "fork_repo": fork_repo, "code": "ok"}
|
|
162
194
|
return {
|
|
163
195
|
"ok": False,
|
|
164
196
|
"message": (create.stderr or create.stdout or check.stderr or check.stdout).strip(),
|
|
165
197
|
"fork_repo": "",
|
|
198
|
+
"code": "fork_unavailable",
|
|
166
199
|
}
|
|
167
200
|
except Exception as e:
|
|
168
|
-
return {"ok": False, "message": str(e), "fork_repo": ""}
|
|
201
|
+
return {"ok": False, "message": str(e), "fork_repo": "", "code": "fork_error"}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _set_pending_auth(config: dict, message: str) -> dict:
|
|
205
|
+
config["status"] = STATUS_PENDING_AUTH
|
|
206
|
+
config["last_result"] = f"pending_auth:{message}"
|
|
207
|
+
save_public_contribution_config(config)
|
|
208
|
+
config["message"] = message
|
|
209
|
+
return config
|
|
169
210
|
|
|
170
211
|
|
|
171
212
|
def _parse_iso(ts: str | None) -> datetime | None:
|
|
@@ -336,13 +377,45 @@ def refresh_public_contribution_state(config: dict | None = None) -> dict:
|
|
|
336
377
|
config["status"] = STATUS_COOLDOWN
|
|
337
378
|
save_public_contribution_config(config)
|
|
338
379
|
return config
|
|
380
|
+
return _set_pending_auth(
|
|
381
|
+
config,
|
|
382
|
+
f"GitHub Draft PR status check failed: {(result.stderr or result.stdout).strip() or 'unknown gh error'}",
|
|
383
|
+
)
|
|
339
384
|
|
|
340
385
|
cooldown_until = _parse_iso(config.get("cooldown_until"))
|
|
341
386
|
if cooldown_until and cooldown_until > _utcnow():
|
|
342
387
|
config["status"] = STATUS_COOLDOWN
|
|
343
|
-
|
|
388
|
+
save_public_contribution_config(config)
|
|
389
|
+
return config
|
|
390
|
+
|
|
391
|
+
auth = github_auth_status()
|
|
392
|
+
if not auth.get("ok"):
|
|
393
|
+
return _set_pending_auth(
|
|
394
|
+
config,
|
|
395
|
+
auth.get("message") or "GitHub authentication is missing for public contribution.",
|
|
396
|
+
)
|
|
397
|
+
login = str(auth.get("login") or "").strip()
|
|
398
|
+
configured_login = str(config.get("github_user") or "").strip()
|
|
399
|
+
if configured_login and login and configured_login.lower() != login.lower():
|
|
400
|
+
return _set_pending_auth(
|
|
401
|
+
config,
|
|
402
|
+
f"GitHub login drift detected: configured {configured_login}, current {login}. Reconfirm public contribution credentials.",
|
|
403
|
+
)
|
|
404
|
+
if login and not configured_login:
|
|
405
|
+
config["github_user"] = login
|
|
406
|
+
|
|
407
|
+
if not str(config.get("fork_repo") or "").strip():
|
|
408
|
+
fork = ensure_fork(login)
|
|
409
|
+
if not fork.get("ok"):
|
|
410
|
+
return _set_pending_auth(
|
|
411
|
+
config,
|
|
412
|
+
fork.get("message") or "GitHub fork setup is missing for public contribution.",
|
|
413
|
+
)
|
|
414
|
+
config["fork_repo"] = str(fork.get("fork_repo") or "").strip()
|
|
415
|
+
|
|
416
|
+
if config["mode"] == MODE_PENDING_AUTH:
|
|
344
417
|
config["status"] = STATUS_PENDING_AUTH
|
|
345
|
-
|
|
418
|
+
else:
|
|
346
419
|
config["status"] = STATUS_ACTIVE
|
|
347
420
|
save_public_contribution_config(config)
|
|
348
421
|
return config
|
|
@@ -351,7 +424,8 @@ def refresh_public_contribution_state(config: dict | None = None) -> dict:
|
|
|
351
424
|
def can_run_public_contribution(config: dict | None = None) -> tuple[bool, str, dict]:
|
|
352
425
|
config = refresh_public_contribution_state(config)
|
|
353
426
|
if config["mode"] == MODE_PENDING_AUTH or config["status"] == STATUS_PENDING_AUTH:
|
|
354
|
-
|
|
427
|
+
detail = str(config.get("message") or config.get("last_result") or "").strip()
|
|
428
|
+
return False, detail or "github authentication or fork setup is pending", config
|
|
355
429
|
if config["mode"] != MODE_DRAFT_PRS or not config.get("enabled"):
|
|
356
430
|
return False, "public contribution is disabled", config
|
|
357
431
|
if config["status"] == STATUS_PAUSED_OPEN_PR:
|
package/src/script_registry.py
CHANGED
|
@@ -662,6 +662,105 @@ def _canonical_schedule_value(schedule_type: str, schedule_value: str | dict | l
|
|
|
662
662
|
return str(schedule_value or "")
|
|
663
663
|
|
|
664
664
|
|
|
665
|
+
def _extract_launchctl_value(output: str, prefixes: str | tuple[str, ...]) -> str | None:
|
|
666
|
+
if isinstance(prefixes, str):
|
|
667
|
+
prefixes = (prefixes,)
|
|
668
|
+
for raw_line in output.splitlines():
|
|
669
|
+
line = raw_line.strip()
|
|
670
|
+
for prefix in prefixes:
|
|
671
|
+
if line.startswith(prefix):
|
|
672
|
+
return line[len(prefix):].strip()
|
|
673
|
+
return None
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _launchctl_service_state(label: str) -> dict:
|
|
677
|
+
state = {
|
|
678
|
+
"loaded": None,
|
|
679
|
+
"pid": "",
|
|
680
|
+
"state": "",
|
|
681
|
+
"last_exit_status": "",
|
|
682
|
+
"error": "",
|
|
683
|
+
}
|
|
684
|
+
if platform.system() != "Darwin":
|
|
685
|
+
return state
|
|
686
|
+
|
|
687
|
+
try:
|
|
688
|
+
result = subprocess.run(
|
|
689
|
+
["launchctl", "print", f"gui/{os.getuid()}/{label}"],
|
|
690
|
+
capture_output=True,
|
|
691
|
+
text=True,
|
|
692
|
+
timeout=3,
|
|
693
|
+
)
|
|
694
|
+
except Exception as exc:
|
|
695
|
+
return {**state, "loaded": False, "error": str(exc)}
|
|
696
|
+
|
|
697
|
+
output = (result.stdout or "") + (result.stderr or "")
|
|
698
|
+
if result.returncode != 0 or "Could not find service" in output:
|
|
699
|
+
return {**state, "loaded": False, "error": output.strip() or "not loaded"}
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
"loaded": True,
|
|
703
|
+
"pid": _extract_launchctl_value(output, ("pid = ", "PID = ")) or "",
|
|
704
|
+
"state": _extract_launchctl_value(output, "state = ") or "",
|
|
705
|
+
"last_exit_status": _extract_launchctl_value(
|
|
706
|
+
output,
|
|
707
|
+
("last exit code = ", "last exit status = ", "LastExitStatus = "),
|
|
708
|
+
) or "",
|
|
709
|
+
"error": "",
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _keep_alive_runtime_snapshot(record: dict) -> dict:
|
|
714
|
+
if record.get("schedule_type") != "keep_alive":
|
|
715
|
+
return {
|
|
716
|
+
"runtime_state": "unknown",
|
|
717
|
+
"runtime_summary": "",
|
|
718
|
+
"runtime_problems": [],
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
label = record.get("launchd_label") or f"com.nexo.{record.get('cron_id', '')}"
|
|
722
|
+
service = _launchctl_service_state(str(label))
|
|
723
|
+
problems: list[str] = []
|
|
724
|
+
|
|
725
|
+
if service.get("loaded") is False:
|
|
726
|
+
problems.append("keep_alive service not loaded in launchd")
|
|
727
|
+
return {
|
|
728
|
+
"runtime_state": "stale",
|
|
729
|
+
"runtime_summary": "keep_alive service not loaded",
|
|
730
|
+
"runtime_problems": problems,
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
pid = str(service.get("pid", "") or "").strip()
|
|
734
|
+
service_state = str(service.get("state", "") or "").strip().lower()
|
|
735
|
+
last_exit = str(service.get("last_exit_status", "") or "").strip()
|
|
736
|
+
if pid:
|
|
737
|
+
return {
|
|
738
|
+
"runtime_state": "alive",
|
|
739
|
+
"runtime_summary": f"running with pid {pid}",
|
|
740
|
+
"runtime_problems": [],
|
|
741
|
+
}
|
|
742
|
+
if service_state in {"running", "spawned"}:
|
|
743
|
+
return {
|
|
744
|
+
"runtime_state": "alive",
|
|
745
|
+
"runtime_summary": f"launchd state {service_state}",
|
|
746
|
+
"runtime_problems": [],
|
|
747
|
+
}
|
|
748
|
+
if last_exit and last_exit != "0":
|
|
749
|
+
problems.append(f"keep_alive daemon exited with status {last_exit}")
|
|
750
|
+
return {
|
|
751
|
+
"runtime_state": "degraded",
|
|
752
|
+
"runtime_summary": f"last exit {last_exit}",
|
|
753
|
+
"runtime_problems": problems,
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
problems.append("keep_alive service is loaded but has no active pid")
|
|
757
|
+
return {
|
|
758
|
+
"runtime_state": "degraded",
|
|
759
|
+
"runtime_summary": "loaded but not running",
|
|
760
|
+
"runtime_problems": problems,
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
|
|
665
764
|
def _discover_personal_schedule_records() -> list[dict]:
|
|
666
765
|
"""Inspect macOS LaunchAgents and return raw personal schedule records."""
|
|
667
766
|
if platform.system() != "Darwin":
|
|
@@ -737,6 +836,12 @@ def audit_personal_schedules() -> dict:
|
|
|
737
836
|
"healthy": 0,
|
|
738
837
|
"problems": 0,
|
|
739
838
|
"managed_registered": 0,
|
|
839
|
+
"keep_alive": 0,
|
|
840
|
+
"runtime_alive": 0,
|
|
841
|
+
"runtime_degraded": 0,
|
|
842
|
+
"runtime_duplicated": 0,
|
|
843
|
+
"runtime_stale": 0,
|
|
844
|
+
"runtime_unknown": 0,
|
|
740
845
|
}
|
|
741
846
|
|
|
742
847
|
for record in _discover_personal_schedule_records():
|
|
@@ -790,6 +895,7 @@ def audit_personal_schedules() -> dict:
|
|
|
790
895
|
schedule_state = "orphaned"
|
|
791
896
|
|
|
792
897
|
audited_record = dict(record)
|
|
898
|
+
runtime_snapshot = _keep_alive_runtime_snapshot(record)
|
|
793
899
|
audited_record.update({
|
|
794
900
|
"schedule_origin": schedule_origin,
|
|
795
901
|
"schedule_declared": declared_valid,
|
|
@@ -799,6 +905,7 @@ def audit_personal_schedules() -> dict:
|
|
|
799
905
|
"problems": problems,
|
|
800
906
|
"script_name": script.get("name", "") if script else "",
|
|
801
907
|
"declared_schedule": declared if script else {},
|
|
908
|
+
**runtime_snapshot,
|
|
802
909
|
})
|
|
803
910
|
audited.append(audited_record)
|
|
804
911
|
summary[schedule_origin] += 1
|
|
@@ -808,6 +915,41 @@ def audit_personal_schedules() -> dict:
|
|
|
808
915
|
else:
|
|
809
916
|
summary["problems"] += 1
|
|
810
917
|
|
|
918
|
+
duplicate_cron_ids: dict[str, int] = {}
|
|
919
|
+
duplicate_script_paths: dict[str, int] = {}
|
|
920
|
+
for record in audited:
|
|
921
|
+
if record.get("schedule_type") != "keep_alive":
|
|
922
|
+
continue
|
|
923
|
+
cron_id = str(record.get("cron_id", "") or "")
|
|
924
|
+
script_path = str(record.get("script_path", "") or "")
|
|
925
|
+
if cron_id:
|
|
926
|
+
duplicate_cron_ids[cron_id] = duplicate_cron_ids.get(cron_id, 0) + 1
|
|
927
|
+
if script_path:
|
|
928
|
+
duplicate_script_paths[script_path] = duplicate_script_paths.get(script_path, 0) + 1
|
|
929
|
+
|
|
930
|
+
for record in audited:
|
|
931
|
+
if record.get("schedule_type") == "keep_alive":
|
|
932
|
+
cron_id = str(record.get("cron_id", "") or "")
|
|
933
|
+
script_path = str(record.get("script_path", "") or "")
|
|
934
|
+
duplicated = (
|
|
935
|
+
(cron_id and duplicate_cron_ids.get(cron_id, 0) > 1)
|
|
936
|
+
or (script_path and duplicate_script_paths.get(script_path, 0) > 1)
|
|
937
|
+
)
|
|
938
|
+
if duplicated:
|
|
939
|
+
runtime_problems = list(record.get("runtime_problems", []))
|
|
940
|
+
runtime_problems.append("duplicate keep_alive schedules discovered for the same cron/script")
|
|
941
|
+
record["runtime_state"] = "duplicated"
|
|
942
|
+
record["runtime_summary"] = "multiple keep_alive schedules discovered"
|
|
943
|
+
record["runtime_problems"] = runtime_problems
|
|
944
|
+
|
|
945
|
+
if record.get("schedule_type") == "keep_alive":
|
|
946
|
+
summary["keep_alive"] += 1
|
|
947
|
+
runtime_state = str(record.get("runtime_state", "unknown") or "unknown")
|
|
948
|
+
key = f"runtime_{runtime_state}"
|
|
949
|
+
if key not in summary:
|
|
950
|
+
summary[key] = 0
|
|
951
|
+
summary[key] += 1
|
|
952
|
+
|
|
811
953
|
return {
|
|
812
954
|
"schedules": audited,
|
|
813
955
|
"summary": summary,
|