nexo-brain 2.6.5 → 2.6.7
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 +911 -143
- package/bin/nexo-brain.js +256 -0
- package/package.json +1 -1
- package/src/auto_close_sessions.py +24 -4
- package/src/auto_update.py +45 -6
- package/src/cli.py +136 -3
- package/src/db/_episodic.py +5 -16
- package/src/doctor/providers/runtime.py +5 -0
- package/src/evolution_cycle.py +51 -1
- package/src/plugins/episodic_memory.py +1 -1
- package/src/plugins/personal_plugins.py +135 -0
- package/src/plugins/update.py +25 -3
- package/src/public_contribution.py +396 -0
- package/src/runtime_power.py +416 -0
- package/src/scripts/nexo-evolution-run.py +394 -2
- package/templates/plugin-template.py +36 -0
|
@@ -13,6 +13,7 @@ PROPOSE proposals are logged for the user's review.
|
|
|
13
13
|
import json
|
|
14
14
|
import os
|
|
15
15
|
import py_compile
|
|
16
|
+
import re
|
|
16
17
|
import sqlite3
|
|
17
18
|
import subprocess
|
|
18
19
|
import sys
|
|
@@ -108,8 +109,11 @@ def _normalize_mode(mode: str) -> str:
|
|
|
108
109
|
"core": "managed",
|
|
109
110
|
"hybrid": "managed",
|
|
110
111
|
"manual": "review",
|
|
112
|
+
"public": "public_core",
|
|
113
|
+
"contributor": "public_core",
|
|
114
|
+
"draft_prs": "public_core",
|
|
111
115
|
}
|
|
112
|
-
return aliases.get(value, value if value in {"auto", "review", "managed"} else "auto")
|
|
116
|
+
return aliases.get(value, value if value in {"auto", "review", "managed", "public_core"} else "auto")
|
|
113
117
|
|
|
114
118
|
|
|
115
119
|
def _immutable_files_for_mode(mode: str) -> set[str]:
|
|
@@ -140,6 +144,15 @@ def _resolve_claude_cli() -> Path:
|
|
|
140
144
|
return Path.home() / ".local" / "bin" / "claude"
|
|
141
145
|
|
|
142
146
|
CLAUDE_CLI = _resolve_claude_cli()
|
|
147
|
+
PUBLIC_ALLOWED_PREFIXES = (
|
|
148
|
+
"src/",
|
|
149
|
+
"bin/",
|
|
150
|
+
"tests/",
|
|
151
|
+
"templates/",
|
|
152
|
+
"hooks/",
|
|
153
|
+
"migrations/",
|
|
154
|
+
".claude-plugin/",
|
|
155
|
+
)
|
|
143
156
|
|
|
144
157
|
# ── Logging ──────────────────────────────────────────────────────────────
|
|
145
158
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -158,7 +171,17 @@ def log(msg: str):
|
|
|
158
171
|
sys.path.insert(0, str(NEXO_CODE))
|
|
159
172
|
from evolution_cycle import (
|
|
160
173
|
load_objective, save_objective, get_week_data, build_evolution_prompt,
|
|
161
|
-
dry_run_restore_test, max_auto_changes, create_snapshot
|
|
174
|
+
dry_run_restore_test, max_auto_changes, create_snapshot, build_public_contribution_prompt
|
|
175
|
+
)
|
|
176
|
+
from public_contribution import (
|
|
177
|
+
CONTRIB_ARTIFACTS_DIR,
|
|
178
|
+
CONTRIB_REPO_DIR,
|
|
179
|
+
CONTRIB_WORKTREES_DIR,
|
|
180
|
+
UPSTREAM_REPO,
|
|
181
|
+
can_run_public_contribution,
|
|
182
|
+
load_public_contribution_config,
|
|
183
|
+
mark_active_pr,
|
|
184
|
+
mark_public_contribution_result,
|
|
162
185
|
)
|
|
163
186
|
|
|
164
187
|
|
|
@@ -213,6 +236,364 @@ def call_claude_cli(prompt: str) -> str:
|
|
|
213
236
|
return result.stdout
|
|
214
237
|
|
|
215
238
|
|
|
239
|
+
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,
|
|
262
|
+
timeout=CLI_TIMEOUT,
|
|
263
|
+
env=env,
|
|
264
|
+
)
|
|
265
|
+
if result.returncode != 0:
|
|
266
|
+
raise RuntimeError(f"claude CLI exited {result.returncode}: {result.stderr[:500]}")
|
|
267
|
+
return result.stdout
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _git(cwd: Path, *args: str, timeout: int = 60) -> subprocess.CompletedProcess:
|
|
271
|
+
return subprocess.run(
|
|
272
|
+
["git", *args],
|
|
273
|
+
cwd=str(cwd),
|
|
274
|
+
capture_output=True,
|
|
275
|
+
text=True,
|
|
276
|
+
timeout=timeout,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _gh(*args: str, cwd: Path | None = None, timeout: int = 60) -> subprocess.CompletedProcess:
|
|
281
|
+
return subprocess.run(
|
|
282
|
+
["gh", *args],
|
|
283
|
+
cwd=str(cwd) if cwd else None,
|
|
284
|
+
capture_output=True,
|
|
285
|
+
text=True,
|
|
286
|
+
timeout=timeout,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _branch_slug(text: str) -> str:
|
|
291
|
+
raw = re.sub(r"[^a-z0-9._-]+", "-", text.lower()).strip("-")
|
|
292
|
+
return raw[:48] or "proposal"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _ensure_public_repo_cache(config: dict) -> None:
|
|
296
|
+
CONTRIB_REPO_DIR.parent.mkdir(parents=True, exist_ok=True)
|
|
297
|
+
if not (CONTRIB_REPO_DIR / ".git").exists():
|
|
298
|
+
clone = _git(CONTRIB_REPO_DIR.parent, "clone", f"https://github.com/{config['upstream_repo']}.git", str(CONTRIB_REPO_DIR), timeout=180)
|
|
299
|
+
if clone.returncode != 0:
|
|
300
|
+
raise RuntimeError(clone.stderr.strip() or clone.stdout.strip() or "git clone failed")
|
|
301
|
+
fetch = _git(CONTRIB_REPO_DIR, "fetch", "origin", timeout=120)
|
|
302
|
+
if fetch.returncode != 0:
|
|
303
|
+
raise RuntimeError(fetch.stderr.strip() or fetch.stdout.strip() or "git fetch failed")
|
|
304
|
+
|
|
305
|
+
remote_url = f"https://github.com/{config['fork_repo']}.git"
|
|
306
|
+
current = _git(CONTRIB_REPO_DIR, "remote", "get-url", "fork", timeout=10)
|
|
307
|
+
if current.returncode != 0:
|
|
308
|
+
add = _git(CONTRIB_REPO_DIR, "remote", "add", "fork", remote_url, timeout=10)
|
|
309
|
+
if add.returncode != 0:
|
|
310
|
+
raise RuntimeError(add.stderr.strip() or add.stdout.strip() or "git remote add fork failed")
|
|
311
|
+
elif current.stdout.strip() != remote_url:
|
|
312
|
+
set_url = _git(CONTRIB_REPO_DIR, "remote", "set-url", "fork", remote_url, timeout=10)
|
|
313
|
+
if set_url.returncode != 0:
|
|
314
|
+
raise RuntimeError(set_url.stderr.strip() or set_url.stdout.strip() or "git remote set-url failed")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _prepare_public_worktree(config: dict, title_hint: str = "evolution") -> tuple[Path, str]:
|
|
318
|
+
_ensure_public_repo_cache(config)
|
|
319
|
+
CONTRIB_WORKTREES_DIR.mkdir(parents=True, exist_ok=True)
|
|
320
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
321
|
+
branch_name = f"contrib/{config['machine_id']}/{timestamp}-{_branch_slug(title_hint)}"
|
|
322
|
+
worktree_dir = CONTRIB_WORKTREES_DIR / f"{timestamp}-{_branch_slug(title_hint)}"
|
|
323
|
+
add = _git(CONTRIB_REPO_DIR, "worktree", "add", "--detach", str(worktree_dir), "origin/main", timeout=120)
|
|
324
|
+
if add.returncode != 0:
|
|
325
|
+
raise RuntimeError(add.stderr.strip() or add.stdout.strip() or "git worktree add failed")
|
|
326
|
+
return worktree_dir, branch_name
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _prime_public_git_identity(worktree_dir: Path, config: dict) -> None:
|
|
330
|
+
github_user = str(config.get("github_user") or "nexo-public-evolution").strip() or "nexo-public-evolution"
|
|
331
|
+
email = f"{github_user}@users.noreply.github.com"
|
|
332
|
+
name = f"{github_user} via NEXO Public Evolution"
|
|
333
|
+
for key, value in (("user.name", name), ("user.email", email)):
|
|
334
|
+
result = _git(worktree_dir, "config", key, value, timeout=15)
|
|
335
|
+
if result.returncode != 0:
|
|
336
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"git config {key} failed")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _remove_public_worktree(worktree_dir: Path) -> None:
|
|
340
|
+
if not worktree_dir.exists():
|
|
341
|
+
return
|
|
342
|
+
_git(CONTRIB_REPO_DIR, "worktree", "remove", str(worktree_dir), "--force", timeout=60)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _parse_summary_json(text: str) -> dict:
|
|
346
|
+
payload = text.strip()
|
|
347
|
+
if "```json" in payload:
|
|
348
|
+
payload = payload.split("```json", 1)[1].split("```", 1)[0]
|
|
349
|
+
elif "```" in payload:
|
|
350
|
+
payload = payload.split("```", 1)[1].split("```", 1)[0]
|
|
351
|
+
try:
|
|
352
|
+
summary = json.loads(payload.strip())
|
|
353
|
+
if isinstance(summary, dict):
|
|
354
|
+
return summary
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
return {}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _changed_public_files(worktree_dir: Path) -> list[str]:
|
|
361
|
+
result = _git(worktree_dir, "status", "--porcelain", timeout=30)
|
|
362
|
+
if result.returncode != 0:
|
|
363
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "git status failed")
|
|
364
|
+
changed: list[str] = []
|
|
365
|
+
for line in result.stdout.splitlines():
|
|
366
|
+
if not line:
|
|
367
|
+
continue
|
|
368
|
+
path_text = line[3:].strip()
|
|
369
|
+
if " -> " in path_text:
|
|
370
|
+
path_text = path_text.split(" -> ", 1)[1].strip()
|
|
371
|
+
if path_text:
|
|
372
|
+
changed.append(path_text)
|
|
373
|
+
return changed
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _is_allowed_public_path(rel_path: str) -> bool:
|
|
377
|
+
return any(rel_path.startswith(prefix) for prefix in PUBLIC_ALLOWED_PREFIXES)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _sanitize_public_diff(worktree_dir: Path, changed_files: list[str]) -> tuple[bool, str]:
|
|
381
|
+
if not changed_files:
|
|
382
|
+
return False, "No repository changes were produced."
|
|
383
|
+
for rel_path in changed_files:
|
|
384
|
+
if not _is_allowed_public_path(rel_path):
|
|
385
|
+
return False, f"Changed path is not allowed for public contribution: {rel_path}"
|
|
386
|
+
|
|
387
|
+
diff = _git(worktree_dir, "diff", "--no-ext-diff", "--", *changed_files, timeout=60)
|
|
388
|
+
if diff.returncode != 0:
|
|
389
|
+
return False, diff.stderr.strip() or diff.stdout.strip() or "git diff failed"
|
|
390
|
+
diff_text = diff.stdout
|
|
391
|
+
private_markers = [
|
|
392
|
+
str(Path.home()),
|
|
393
|
+
str(NEXO_HOME),
|
|
394
|
+
"/Users/",
|
|
395
|
+
"/home/",
|
|
396
|
+
"CLAUDE.md",
|
|
397
|
+
".nexo/",
|
|
398
|
+
]
|
|
399
|
+
for marker in private_markers:
|
|
400
|
+
if marker and marker in diff_text:
|
|
401
|
+
return False, f"Sanitization blocked private marker in diff: {marker}"
|
|
402
|
+
return True, ""
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _run_public_validation(worktree_dir: Path, changed_files: list[str]) -> list[str]:
|
|
406
|
+
validations: list[str] = []
|
|
407
|
+
py_files = [str(worktree_dir / rel_path) for rel_path in changed_files if rel_path.endswith(".py")]
|
|
408
|
+
if py_files:
|
|
409
|
+
result = subprocess.run(
|
|
410
|
+
[sys.executable, "-m", "py_compile", *py_files],
|
|
411
|
+
cwd=str(worktree_dir),
|
|
412
|
+
capture_output=True,
|
|
413
|
+
text=True,
|
|
414
|
+
timeout=120,
|
|
415
|
+
)
|
|
416
|
+
if result.returncode != 0:
|
|
417
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "py_compile failed")
|
|
418
|
+
validations.append("python3 -m py_compile " + " ".join(changed_files))
|
|
419
|
+
|
|
420
|
+
js_files = [str(worktree_dir / rel_path) for rel_path in changed_files if rel_path.endswith(".js")]
|
|
421
|
+
for js_file in js_files:
|
|
422
|
+
result = subprocess.run(
|
|
423
|
+
["node", "--check", js_file],
|
|
424
|
+
cwd=str(worktree_dir),
|
|
425
|
+
capture_output=True,
|
|
426
|
+
text=True,
|
|
427
|
+
timeout=60,
|
|
428
|
+
)
|
|
429
|
+
if result.returncode != 0:
|
|
430
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"node --check failed for {js_file}")
|
|
431
|
+
if js_files:
|
|
432
|
+
validations.append("node --check " + " ".join(changed_files))
|
|
433
|
+
|
|
434
|
+
tests = subprocess.run(
|
|
435
|
+
["pytest", "-q", "tests"],
|
|
436
|
+
cwd=str(worktree_dir),
|
|
437
|
+
capture_output=True,
|
|
438
|
+
text=True,
|
|
439
|
+
timeout=900,
|
|
440
|
+
env={**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1"},
|
|
441
|
+
)
|
|
442
|
+
if tests.returncode != 0:
|
|
443
|
+
raise RuntimeError(tests.stderr.strip() or tests.stdout.strip() or "pytest failed")
|
|
444
|
+
validations.append("PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 pytest -q tests")
|
|
445
|
+
return validations
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _write_public_artifacts(worktree_dir: Path, branch_name: str, summary: dict) -> Path:
|
|
449
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
450
|
+
artifact_dir = CONTRIB_ARTIFACTS_DIR / timestamp
|
|
451
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
452
|
+
diff = _git(worktree_dir, "diff", "--no-ext-diff", "origin/main...HEAD", timeout=60)
|
|
453
|
+
patch_text = diff.stdout if diff.returncode == 0 else ""
|
|
454
|
+
(artifact_dir / "summary.json").write_text(json.dumps(summary, indent=2, ensure_ascii=False) + "\n")
|
|
455
|
+
(artifact_dir / "branch.txt").write_text(branch_name + "\n")
|
|
456
|
+
(artifact_dir / "diff.patch").write_text(patch_text)
|
|
457
|
+
return artifact_dir
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _create_draft_pr(worktree_dir: Path, config: dict, branch_name: str, summary: dict) -> tuple[str, int | None]:
|
|
461
|
+
title = str(summary.get("title") or "chore: public evolution contribution").strip()
|
|
462
|
+
body_lines = [
|
|
463
|
+
summary.get("problem", "Problem: see diff."),
|
|
464
|
+
"",
|
|
465
|
+
"Summary:",
|
|
466
|
+
str(summary.get("summary") or "See diff."),
|
|
467
|
+
"",
|
|
468
|
+
"Tests:",
|
|
469
|
+
]
|
|
470
|
+
tests = summary.get("tests") or []
|
|
471
|
+
if isinstance(tests, list) and tests:
|
|
472
|
+
body_lines.extend(f"- {item}" for item in tests)
|
|
473
|
+
else:
|
|
474
|
+
body_lines.append("- See CI / local validation")
|
|
475
|
+
risks = summary.get("risks") or []
|
|
476
|
+
if isinstance(risks, list) and risks:
|
|
477
|
+
body_lines.extend(["", "Risks:"])
|
|
478
|
+
body_lines.extend(f"- {item}" for item in risks)
|
|
479
|
+
body_lines.extend(["", "Source: automated public core evolution from an opt-in machine."])
|
|
480
|
+
body_file = worktree_dir / ".nexo-public-pr-body.md"
|
|
481
|
+
body_file.write_text("\n".join(body_lines) + "\n")
|
|
482
|
+
head = f"{config['github_user']}:{branch_name}"
|
|
483
|
+
result = _gh(
|
|
484
|
+
"pr",
|
|
485
|
+
"create",
|
|
486
|
+
"--repo",
|
|
487
|
+
config["upstream_repo"],
|
|
488
|
+
"--head",
|
|
489
|
+
head,
|
|
490
|
+
"--base",
|
|
491
|
+
"main",
|
|
492
|
+
"--title",
|
|
493
|
+
title,
|
|
494
|
+
"--body-file",
|
|
495
|
+
str(body_file),
|
|
496
|
+
"--draft",
|
|
497
|
+
cwd=worktree_dir,
|
|
498
|
+
timeout=120,
|
|
499
|
+
)
|
|
500
|
+
if result.returncode != 0:
|
|
501
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "gh pr create failed")
|
|
502
|
+
pr_url = (result.stdout or "").strip().splitlines()[-1].strip()
|
|
503
|
+
match = re.search(r"/pull/(\d+)", pr_url)
|
|
504
|
+
pr_number = int(match.group(1)) if match else None
|
|
505
|
+
return pr_url, pr_number
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def run_public_contribution_cycle(*, objective: dict, cycle_num: int) -> None:
|
|
509
|
+
config = load_public_contribution_config()
|
|
510
|
+
ready, reason, config = can_run_public_contribution(config)
|
|
511
|
+
if not ready:
|
|
512
|
+
log(f"Public core contribution paused: {reason}")
|
|
513
|
+
mark_public_contribution_result(result=f"skipped:{reason}", config=config)
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
if not verify_claude_cli():
|
|
517
|
+
log("Claude CLI not available or not authenticated. Skipping public contribution run.")
|
|
518
|
+
mark_public_contribution_result(result="skipped:claude_cli_unavailable", config=config)
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
worktree_dir: Path | None = None
|
|
522
|
+
branch_name = ""
|
|
523
|
+
summary: dict = {}
|
|
524
|
+
conn = sqlite3.connect(str(NEXO_DB), timeout=10)
|
|
525
|
+
try:
|
|
526
|
+
worktree_dir, branch_name = _prepare_public_worktree(config, title_hint="public-core")
|
|
527
|
+
_prime_public_git_identity(worktree_dir, config)
|
|
528
|
+
prompt = build_public_contribution_prompt(repo_root=str(worktree_dir), cycle_number=cycle_num)
|
|
529
|
+
raw_response = call_public_claude_cli(prompt, cwd=worktree_dir)
|
|
530
|
+
summary = _parse_summary_json(raw_response)
|
|
531
|
+
changed_files = _changed_public_files(worktree_dir)
|
|
532
|
+
ok, reason = _sanitize_public_diff(worktree_dir, changed_files)
|
|
533
|
+
if not ok:
|
|
534
|
+
raise RuntimeError(reason)
|
|
535
|
+
|
|
536
|
+
tests_run = _run_public_validation(worktree_dir, changed_files)
|
|
537
|
+
existing_tests = summary.get("tests")
|
|
538
|
+
summary["tests"] = existing_tests if isinstance(existing_tests, list) and existing_tests else tests_run
|
|
539
|
+
commit_title = str(summary.get("title") or "chore: public evolution contribution").strip()
|
|
540
|
+
|
|
541
|
+
add = _git(worktree_dir, "add", "--", *changed_files, timeout=60)
|
|
542
|
+
if add.returncode != 0:
|
|
543
|
+
raise RuntimeError(add.stderr.strip() or add.stdout.strip() or "git add failed")
|
|
544
|
+
commit = _git(worktree_dir, "commit", "-m", commit_title, timeout=120)
|
|
545
|
+
if commit.returncode != 0:
|
|
546
|
+
raise RuntimeError(commit.stderr.strip() or commit.stdout.strip() or "git commit failed")
|
|
547
|
+
push = _git(worktree_dir, "push", "fork", f"HEAD:refs/heads/{branch_name}", "--force-with-lease", timeout=180)
|
|
548
|
+
if push.returncode != 0:
|
|
549
|
+
raise RuntimeError(push.stderr.strip() or push.stdout.strip() or "git push failed")
|
|
550
|
+
|
|
551
|
+
pr_url, pr_number = _create_draft_pr(worktree_dir, config, branch_name, summary)
|
|
552
|
+
artifact_dir = _write_public_artifacts(worktree_dir, branch_name, summary)
|
|
553
|
+
mark_active_pr(pr_url=pr_url, pr_number=pr_number, branch=branch_name, config=config)
|
|
554
|
+
|
|
555
|
+
conn.execute(
|
|
556
|
+
"INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, reasoning, status, files_changed, test_result) "
|
|
557
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
558
|
+
(
|
|
559
|
+
cycle_num,
|
|
560
|
+
"public_core",
|
|
561
|
+
commit_title,
|
|
562
|
+
"draft_pr",
|
|
563
|
+
summary.get("problem", "Public core contribution"),
|
|
564
|
+
"draft_pr_created",
|
|
565
|
+
json.dumps(changed_files),
|
|
566
|
+
json.dumps({"tests": summary.get("tests", []), "pr_url": pr_url, "artifact_dir": str(artifact_dir)}),
|
|
567
|
+
),
|
|
568
|
+
)
|
|
569
|
+
conn.commit()
|
|
570
|
+
|
|
571
|
+
objective["last_evolution"] = str(date.today())
|
|
572
|
+
objective["total_evolutions"] = cycle_num
|
|
573
|
+
objective["total_proposals_made"] = objective.get("total_proposals_made", 0) + 1
|
|
574
|
+
objective.setdefault("history", []).insert(0, {
|
|
575
|
+
"cycle": cycle_num,
|
|
576
|
+
"date": str(date.today()),
|
|
577
|
+
"mode": "public_core",
|
|
578
|
+
"proposals": 1,
|
|
579
|
+
"auto_count": 0,
|
|
580
|
+
"auto_applied": 0,
|
|
581
|
+
"analysis": (summary.get("summary") or commit_title)[:200],
|
|
582
|
+
"pr_url": pr_url,
|
|
583
|
+
})
|
|
584
|
+
objective["history"] = objective["history"][:12]
|
|
585
|
+
save_objective(objective)
|
|
586
|
+
mark_public_contribution_result(result=f"draft_pr_created:{pr_url}", config=config)
|
|
587
|
+
log(f"Public core contribution complete: Draft PR created at {pr_url}")
|
|
588
|
+
except Exception as exc:
|
|
589
|
+
mark_public_contribution_result(result=f"failed:{exc}", config=config)
|
|
590
|
+
raise
|
|
591
|
+
finally:
|
|
592
|
+
conn.close()
|
|
593
|
+
if worktree_dir is not None:
|
|
594
|
+
_remove_public_worktree(worktree_dir)
|
|
595
|
+
|
|
596
|
+
|
|
216
597
|
# ── File safety validation ───────────────────────────────────────────────
|
|
217
598
|
def is_safe_path(filepath: str, mode: str = "auto") -> bool:
|
|
218
599
|
"""Check if a file path is within safe zones and not immutable.
|
|
@@ -518,6 +899,17 @@ def run():
|
|
|
518
899
|
save_objective(objective)
|
|
519
900
|
return
|
|
520
901
|
|
|
902
|
+
public_config = load_public_contribution_config()
|
|
903
|
+
if str(public_config.get("mode") or "").strip().lower() in {"draft_prs", "pending_auth"}:
|
|
904
|
+
cycle_num = objective.get("total_evolutions", 0) + 1
|
|
905
|
+
try:
|
|
906
|
+
run_public_contribution_cycle(objective=objective, cycle_num=cycle_num)
|
|
907
|
+
set_consecutive_failures(0)
|
|
908
|
+
except Exception as e:
|
|
909
|
+
log(f"Public core contribution failed: {e}")
|
|
910
|
+
set_consecutive_failures(failures + 1)
|
|
911
|
+
return
|
|
912
|
+
|
|
521
913
|
# Dry-run restore test
|
|
522
914
|
log("Running restore dry-run test...")
|
|
523
915
|
if not dry_run_restore_test():
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Personal NEXO MCP plugin scaffold.
|
|
2
|
+
|
|
3
|
+
This file lives in NEXO_HOME/plugins/ and is loaded by the NEXO MCP server.
|
|
4
|
+
Edit the handler below to implement your personal capability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def handle_example_tool(payload_json: str = "{}") -> str:
|
|
13
|
+
"""Example personal MCP tool.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
payload_json: JSON string with your input payload.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
payload = json.loads(payload_json or "{}")
|
|
20
|
+
except Exception as exc:
|
|
21
|
+
return json.dumps({"ok": False, "error": f"invalid json: {exc}"}, ensure_ascii=False)
|
|
22
|
+
|
|
23
|
+
return json.dumps({
|
|
24
|
+
"ok": True,
|
|
25
|
+
"message": "Personal plugin scaffold created. Edit this handler in NEXO_HOME/plugins.",
|
|
26
|
+
"payload": payload,
|
|
27
|
+
}, ensure_ascii=False)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
TOOLS = [
|
|
31
|
+
(
|
|
32
|
+
handle_example_tool,
|
|
33
|
+
"nexo_example_tool",
|
|
34
|
+
"Example personal MCP tool scaffold. Edit it in NEXO_HOME/plugins.",
|
|
35
|
+
),
|
|
36
|
+
]
|