nexo-brain 2.7.0 → 3.0.1
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 +66 -12
- package/hooks/hooks.json +79 -0
- package/package.json +1 -1
- package/src/agent_runner.py +295 -7
- package/src/cli.py +111 -0
- package/src/client_preferences.py +99 -1
- package/src/client_sync.py +207 -3
- package/src/cognitive/__init__.py +1 -1
- package/src/cognitive/_search.py +39 -19
- package/src/dashboard/app.py +141 -1
- package/src/dashboard/templates/base.html +4 -0
- 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/boot.py +45 -19
- package/src/doctor/providers/runtime.py +923 -8
- 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/requirements.txt +1 -0
- package/src/script_registry.py +142 -0
- package/src/scripts/deep-sleep/apply_findings.py +204 -0
- 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
|
@@ -173,7 +173,8 @@ sys.path.insert(0, str(NEXO_CODE))
|
|
|
173
173
|
from agent_runner import probe_automation_backend, run_automation_prompt
|
|
174
174
|
from evolution_cycle import (
|
|
175
175
|
load_objective, save_objective, get_week_data, build_evolution_prompt,
|
|
176
|
-
dry_run_restore_test, max_auto_changes, create_snapshot,
|
|
176
|
+
dry_run_restore_test, max_auto_changes, create_snapshot,
|
|
177
|
+
build_public_contribution_prompt, build_public_pr_review_prompt,
|
|
177
178
|
)
|
|
178
179
|
from public_contribution import (
|
|
179
180
|
CONTRIB_ARTIFACTS_DIR,
|
|
@@ -184,6 +185,7 @@ from public_contribution import (
|
|
|
184
185
|
load_public_contribution_config,
|
|
185
186
|
mark_active_pr,
|
|
186
187
|
mark_public_contribution_result,
|
|
188
|
+
STATUS_PAUSED_OPEN_PR,
|
|
187
189
|
)
|
|
188
190
|
|
|
189
191
|
|
|
@@ -430,6 +432,304 @@ def _write_public_artifacts(worktree_dir: Path, branch_name: str, summary: dict)
|
|
|
430
432
|
return artifact_dir
|
|
431
433
|
|
|
432
434
|
|
|
435
|
+
def _review_state(review: dict) -> str:
|
|
436
|
+
return str(review.get("state") or review.get("reviewState") or "").strip().upper()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _review_author(review: dict) -> str:
|
|
440
|
+
author = review.get("author") or {}
|
|
441
|
+
if isinstance(author, dict):
|
|
442
|
+
return str(author.get("login") or "").strip().lower()
|
|
443
|
+
return ""
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _is_public_evolution_pr(details: dict) -> bool:
|
|
447
|
+
body = str(details.get("body") or "")
|
|
448
|
+
return "Source: automated public core evolution from an opt-in machine." in body
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _review_already_left_by_user(details: dict, login: str) -> bool:
|
|
452
|
+
login = str(login or "").strip().lower()
|
|
453
|
+
if not login:
|
|
454
|
+
return False
|
|
455
|
+
for review in details.get("reviews") or []:
|
|
456
|
+
if _review_author(review) == login and _review_state(review) in {"APPROVED", "COMMENTED", "CHANGES_REQUESTED"}:
|
|
457
|
+
return True
|
|
458
|
+
return False
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _candidate_paths(details: dict) -> list[str]:
|
|
462
|
+
paths = []
|
|
463
|
+
for item in details.get("files") or []:
|
|
464
|
+
if isinstance(item, dict):
|
|
465
|
+
path = str(item.get("path") or item.get("name") or "").strip()
|
|
466
|
+
if path:
|
|
467
|
+
paths.append(path)
|
|
468
|
+
return paths
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _list_reviewable_public_prs(config: dict, limit: int = 3) -> list[dict]:
|
|
472
|
+
result = _gh(
|
|
473
|
+
"pr",
|
|
474
|
+
"list",
|
|
475
|
+
"--repo",
|
|
476
|
+
config["upstream_repo"],
|
|
477
|
+
"--state",
|
|
478
|
+
"open",
|
|
479
|
+
"--json",
|
|
480
|
+
"number,title,url,isDraft,author",
|
|
481
|
+
"--limit",
|
|
482
|
+
str(max(1, limit * 4)),
|
|
483
|
+
timeout=30,
|
|
484
|
+
)
|
|
485
|
+
if result.returncode != 0:
|
|
486
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "gh pr list failed")
|
|
487
|
+
|
|
488
|
+
github_user = str(config.get("github_user") or "").strip().lower()
|
|
489
|
+
active_pr_number = config.get("active_pr_number")
|
|
490
|
+
candidates: list[dict] = []
|
|
491
|
+
for item in json.loads(result.stdout or "[]"):
|
|
492
|
+
if not item.get("isDraft", False):
|
|
493
|
+
continue
|
|
494
|
+
number = int(item.get("number") or 0)
|
|
495
|
+
if not number or number == active_pr_number:
|
|
496
|
+
continue
|
|
497
|
+
author = item.get("author") or {}
|
|
498
|
+
author_login = str(author.get("login") or "").strip().lower()
|
|
499
|
+
if github_user and author_login == github_user:
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
details_result = _gh(
|
|
503
|
+
"pr",
|
|
504
|
+
"view",
|
|
505
|
+
str(number),
|
|
506
|
+
"--repo",
|
|
507
|
+
config["upstream_repo"],
|
|
508
|
+
"--json",
|
|
509
|
+
"number,title,body,url,isDraft,author,reviews,files",
|
|
510
|
+
timeout=30,
|
|
511
|
+
)
|
|
512
|
+
if details_result.returncode != 0:
|
|
513
|
+
continue
|
|
514
|
+
details = json.loads(details_result.stdout or "{}")
|
|
515
|
+
if not details.get("isDraft", False):
|
|
516
|
+
continue
|
|
517
|
+
if not _is_public_evolution_pr(details):
|
|
518
|
+
continue
|
|
519
|
+
if _review_already_left_by_user(details, github_user):
|
|
520
|
+
continue
|
|
521
|
+
paths = _candidate_paths(details)
|
|
522
|
+
if not paths or any(not _is_allowed_public_path(path) for path in paths):
|
|
523
|
+
continue
|
|
524
|
+
|
|
525
|
+
diff_result = _gh(
|
|
526
|
+
"pr",
|
|
527
|
+
"diff",
|
|
528
|
+
str(number),
|
|
529
|
+
"--repo",
|
|
530
|
+
config["upstream_repo"],
|
|
531
|
+
timeout=60,
|
|
532
|
+
)
|
|
533
|
+
if diff_result.returncode != 0:
|
|
534
|
+
continue
|
|
535
|
+
details["files_changed"] = paths
|
|
536
|
+
details["diff_text"] = diff_result.stdout or ""
|
|
537
|
+
candidates.append(details)
|
|
538
|
+
if len(candidates) >= limit:
|
|
539
|
+
break
|
|
540
|
+
return candidates
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
_DEDUP_STOPWORDS = {
|
|
544
|
+
"the", "and", "for", "with", "from", "into", "after", "before", "public",
|
|
545
|
+
"core", "nexo", "fix", "feat", "chore", "docs", "tests", "runtime", "system",
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _proposal_tokens(text: str) -> set[str]:
|
|
550
|
+
return {
|
|
551
|
+
token
|
|
552
|
+
for token in re.findall(r"[a-z0-9]+", (text or "").lower())
|
|
553
|
+
if len(token) >= 3 and token not in _DEDUP_STOPWORDS
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _public_pr_duplicate_candidate(config: dict, *, title: str, changed_files: list[str]) -> dict | None:
|
|
558
|
+
try:
|
|
559
|
+
candidates = _list_reviewable_public_prs(config, limit=12)
|
|
560
|
+
except Exception:
|
|
561
|
+
return None
|
|
562
|
+
wanted_files = {str(path).strip().lower() for path in (changed_files or []) if str(path).strip()}
|
|
563
|
+
wanted_tokens = _proposal_tokens(title)
|
|
564
|
+
best_match = None
|
|
565
|
+
best_score = 0.0
|
|
566
|
+
for candidate in candidates:
|
|
567
|
+
candidate_files = {
|
|
568
|
+
str(path).strip().lower() for path in (candidate.get("files_changed") or []) if str(path).strip()
|
|
569
|
+
}
|
|
570
|
+
shared_files = wanted_files & candidate_files
|
|
571
|
+
candidate_tokens = _proposal_tokens(str(candidate.get("title") or ""))
|
|
572
|
+
shared_tokens = wanted_tokens & candidate_tokens
|
|
573
|
+
token_score = 0.0
|
|
574
|
+
if wanted_tokens and candidate_tokens:
|
|
575
|
+
token_score = len(shared_tokens) / max(1, min(len(wanted_tokens), len(candidate_tokens)))
|
|
576
|
+
score = 0.0
|
|
577
|
+
if shared_files and token_score >= 0.34:
|
|
578
|
+
score = 1.0
|
|
579
|
+
elif shared_files:
|
|
580
|
+
score = 0.75
|
|
581
|
+
elif token_score >= 0.8:
|
|
582
|
+
score = 0.7
|
|
583
|
+
if score > best_score:
|
|
584
|
+
best_score = score
|
|
585
|
+
best_match = {
|
|
586
|
+
"number": candidate.get("number"),
|
|
587
|
+
"title": candidate.get("title"),
|
|
588
|
+
"url": candidate.get("url"),
|
|
589
|
+
"score": round(score, 2),
|
|
590
|
+
"shared_files": sorted(shared_files),
|
|
591
|
+
"shared_tokens": sorted(shared_tokens),
|
|
592
|
+
}
|
|
593
|
+
return best_match if best_score >= 0.75 else None
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _parse_public_review_json(text: str) -> dict:
|
|
597
|
+
payload = text.strip()
|
|
598
|
+
if "```json" in payload:
|
|
599
|
+
payload = payload.split("```json", 1)[1].split("```", 1)[0]
|
|
600
|
+
elif "```" in payload:
|
|
601
|
+
payload = payload.split("```", 1)[1].split("```", 1)[0]
|
|
602
|
+
try:
|
|
603
|
+
data = json.loads(payload.strip())
|
|
604
|
+
except Exception:
|
|
605
|
+
data = {}
|
|
606
|
+
return data if isinstance(data, dict) else {}
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _submit_public_pr_review(config: dict, pr_number: int, decision: str, body: str) -> str:
|
|
610
|
+
clean_decision = str(decision or "").strip().lower()
|
|
611
|
+
clean_body = str(body or "").strip()
|
|
612
|
+
if clean_decision == "approve":
|
|
613
|
+
result = _gh(
|
|
614
|
+
"pr",
|
|
615
|
+
"review",
|
|
616
|
+
str(pr_number),
|
|
617
|
+
"--repo",
|
|
618
|
+
config["upstream_repo"],
|
|
619
|
+
"--approve",
|
|
620
|
+
"--body",
|
|
621
|
+
clean_body or "Scoped public-core change looks correct from automated peer review.",
|
|
622
|
+
timeout=60,
|
|
623
|
+
)
|
|
624
|
+
if result.returncode != 0:
|
|
625
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "gh pr review --approve failed")
|
|
626
|
+
return "approved_review"
|
|
627
|
+
if clean_decision == "comment":
|
|
628
|
+
result = _gh(
|
|
629
|
+
"pr",
|
|
630
|
+
"review",
|
|
631
|
+
str(pr_number),
|
|
632
|
+
"--repo",
|
|
633
|
+
config["upstream_repo"],
|
|
634
|
+
"--comment",
|
|
635
|
+
"--body",
|
|
636
|
+
clean_body or "Automated peer review left a note but did not approve.",
|
|
637
|
+
timeout=60,
|
|
638
|
+
)
|
|
639
|
+
if result.returncode != 0:
|
|
640
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "gh pr review --comment failed")
|
|
641
|
+
return "commented_review"
|
|
642
|
+
return "review_skipped"
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _write_public_review_artifacts(pr_number: int, candidate: dict, review: dict) -> Path:
|
|
646
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
647
|
+
artifact_dir = CONTRIB_ARTIFACTS_DIR / f"review-{timestamp}-pr{pr_number}"
|
|
648
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
649
|
+
(artifact_dir / "candidate.json").write_text(json.dumps(candidate, indent=2, ensure_ascii=False) + "\n")
|
|
650
|
+
(artifact_dir / "review.json").write_text(json.dumps(review, indent=2, ensure_ascii=False) + "\n")
|
|
651
|
+
(artifact_dir / "diff.patch").write_text(str(candidate.get("diff_text") or ""))
|
|
652
|
+
return artifact_dir
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def run_public_pr_validation_cycle(*, objective: dict, cycle_num: int, config: dict | None = None) -> int:
|
|
656
|
+
config = config or load_public_contribution_config()
|
|
657
|
+
if not verify_claude_cli():
|
|
658
|
+
log("Automation backend not available or not authenticated. Skipping peer PR validation.")
|
|
659
|
+
mark_public_contribution_result(result="skipped:peer_review_cli_unavailable", config=config)
|
|
660
|
+
return 0
|
|
661
|
+
|
|
662
|
+
_ensure_public_repo_cache(config)
|
|
663
|
+
candidates = _list_reviewable_public_prs(config, limit=3)
|
|
664
|
+
if not candidates:
|
|
665
|
+
log("No reviewable peer public-evolution PRs found.")
|
|
666
|
+
mark_public_contribution_result(result="skipped:no_peer_prs", config=config)
|
|
667
|
+
return 0
|
|
668
|
+
|
|
669
|
+
repo_root = str(CONTRIB_REPO_DIR if CONTRIB_REPO_DIR.exists() else Path.cwd())
|
|
670
|
+
conn = sqlite3.connect(str(NEXO_DB), timeout=10)
|
|
671
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
672
|
+
reviewed = 0
|
|
673
|
+
try:
|
|
674
|
+
for candidate in candidates:
|
|
675
|
+
pr_number = int(candidate.get("number") or 0)
|
|
676
|
+
prompt = build_public_pr_review_prompt(
|
|
677
|
+
pr_number=pr_number,
|
|
678
|
+
title=str(candidate.get("title") or "").strip(),
|
|
679
|
+
author=str((candidate.get("author") or {}).get("login") or "").strip(),
|
|
680
|
+
url=str(candidate.get("url") or "").strip(),
|
|
681
|
+
body=str(candidate.get("body") or ""),
|
|
682
|
+
files=candidate.get("files_changed") or [],
|
|
683
|
+
diff_text=str(candidate.get("diff_text") or ""),
|
|
684
|
+
)
|
|
685
|
+
raw_review = call_public_claude_cli(prompt, cwd=Path(repo_root))
|
|
686
|
+
review = _parse_public_review_json(raw_review)
|
|
687
|
+
decision = str(review.get("decision") or "skip").strip().lower()
|
|
688
|
+
review_status = _submit_public_pr_review(config, pr_number, decision, str(review.get("body") or ""))
|
|
689
|
+
artifact_dir = _write_public_review_artifacts(pr_number, candidate, review)
|
|
690
|
+
conn.execute(
|
|
691
|
+
"INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, reasoning, status, files_changed, test_result) "
|
|
692
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
693
|
+
(
|
|
694
|
+
cycle_num,
|
|
695
|
+
"public_core",
|
|
696
|
+
f"Review PR #{pr_number}: {str(candidate.get('title') or '').strip()}",
|
|
697
|
+
"public_review",
|
|
698
|
+
str(review.get("summary") or "Peer PR validation").strip(),
|
|
699
|
+
review_status,
|
|
700
|
+
json.dumps(candidate.get("files_changed") or []),
|
|
701
|
+
json.dumps(
|
|
702
|
+
{
|
|
703
|
+
"pr_url": candidate.get("url"),
|
|
704
|
+
"decision": decision,
|
|
705
|
+
"artifact_dir": str(artifact_dir),
|
|
706
|
+
}
|
|
707
|
+
),
|
|
708
|
+
),
|
|
709
|
+
)
|
|
710
|
+
conn.commit()
|
|
711
|
+
reviewed += 1
|
|
712
|
+
|
|
713
|
+
if reviewed:
|
|
714
|
+
objective["last_evolution"] = str(date.today())
|
|
715
|
+
objective["total_evolutions"] = cycle_num
|
|
716
|
+
objective.setdefault("history", []).insert(0, {
|
|
717
|
+
"cycle": cycle_num,
|
|
718
|
+
"date": str(date.today()),
|
|
719
|
+
"mode": "public_core_review",
|
|
720
|
+
"proposals": 0,
|
|
721
|
+
"auto_count": 0,
|
|
722
|
+
"auto_applied": 0,
|
|
723
|
+
"analysis": f"Reviewed {reviewed} peer public-evolution PR(s).",
|
|
724
|
+
})
|
|
725
|
+
objective["history"] = objective["history"][:12]
|
|
726
|
+
save_objective(objective)
|
|
727
|
+
mark_public_contribution_result(result=f"peer_reviewed:{reviewed}", config=config)
|
|
728
|
+
return reviewed
|
|
729
|
+
finally:
|
|
730
|
+
conn.close()
|
|
731
|
+
|
|
732
|
+
|
|
433
733
|
def _create_draft_pr(worktree_dir: Path, config: dict, branch_name: str, summary: dict) -> tuple[str, int | None]:
|
|
434
734
|
title = str(summary.get("title") or "chore: public evolution contribution").strip()
|
|
435
735
|
body_lines = [
|
|
@@ -482,6 +782,12 @@ def run_public_contribution_cycle(*, objective: dict, cycle_num: int) -> None:
|
|
|
482
782
|
config = load_public_contribution_config()
|
|
483
783
|
ready, reason, config = can_run_public_contribution(config)
|
|
484
784
|
if not ready:
|
|
785
|
+
if config.get("status") == STATUS_PAUSED_OPEN_PR:
|
|
786
|
+
log(f"Public core contribution paused: {reason}. Switching to peer PR validation.")
|
|
787
|
+
reviewed = run_public_pr_validation_cycle(objective=objective, cycle_num=cycle_num, config=config)
|
|
788
|
+
if reviewed:
|
|
789
|
+
log(f"Peer public PR validation complete: reviewed {reviewed} PR(s).")
|
|
790
|
+
return
|
|
485
791
|
log(f"Public core contribution paused: {reason}")
|
|
486
792
|
mark_public_contribution_result(result=f"skipped:{reason}", config=config)
|
|
487
793
|
return
|
|
@@ -510,6 +816,42 @@ def run_public_contribution_cycle(*, objective: dict, cycle_num: int) -> None:
|
|
|
510
816
|
existing_tests = summary.get("tests")
|
|
511
817
|
summary["tests"] = existing_tests if isinstance(existing_tests, list) and existing_tests else tests_run
|
|
512
818
|
commit_title = str(summary.get("title") or "chore: public evolution contribution").strip()
|
|
819
|
+
duplicate = _public_pr_duplicate_candidate(config, title=commit_title, changed_files=changed_files)
|
|
820
|
+
if duplicate:
|
|
821
|
+
artifact_dir = _write_public_artifacts(
|
|
822
|
+
worktree_dir,
|
|
823
|
+
branch_name,
|
|
824
|
+
{
|
|
825
|
+
**summary,
|
|
826
|
+
"duplicate_of": duplicate,
|
|
827
|
+
"tests": summary.get("tests", []),
|
|
828
|
+
"changed_files": changed_files,
|
|
829
|
+
},
|
|
830
|
+
)
|
|
831
|
+
conn.execute(
|
|
832
|
+
"INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, reasoning, status, files_changed, test_result) "
|
|
833
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
834
|
+
(
|
|
835
|
+
cycle_num,
|
|
836
|
+
"public_core",
|
|
837
|
+
commit_title,
|
|
838
|
+
"draft_pr_dedup",
|
|
839
|
+
f"Duplicate of open opt-in public PR #{duplicate.get('number')}: {duplicate.get('title')}",
|
|
840
|
+
"skipped_duplicate_existing_pr",
|
|
841
|
+
json.dumps(changed_files),
|
|
842
|
+
json.dumps({"duplicate_of": duplicate, "artifact_dir": str(artifact_dir)}),
|
|
843
|
+
),
|
|
844
|
+
)
|
|
845
|
+
conn.commit()
|
|
846
|
+
mark_public_contribution_result(
|
|
847
|
+
result=f"skipped:duplicate_pr:{duplicate.get('number')}",
|
|
848
|
+
config=config,
|
|
849
|
+
)
|
|
850
|
+
log(
|
|
851
|
+
"Public core contribution deduplicated against existing opt-in PR "
|
|
852
|
+
f"#{duplicate.get('number')} ({duplicate.get('url')})."
|
|
853
|
+
)
|
|
854
|
+
return
|
|
513
855
|
|
|
514
856
|
add = _git(worktree_dir, "add", "--", *changed_files, timeout=60)
|
|
515
857
|
if add.returncode != 0:
|
package/src/server.py
CHANGED
|
@@ -7,7 +7,15 @@ import sys
|
|
|
7
7
|
|
|
8
8
|
from fastmcp import FastMCP
|
|
9
9
|
from db import init_db, rebuild_fts_index, get_db, close_db, fts_add_dir, fts_remove_dir, fts_list_dirs
|
|
10
|
-
from tools_sessions import
|
|
10
|
+
from tools_sessions import (
|
|
11
|
+
handle_startup,
|
|
12
|
+
handle_heartbeat,
|
|
13
|
+
handle_status,
|
|
14
|
+
handle_context_packet,
|
|
15
|
+
handle_smart_startup_query,
|
|
16
|
+
handle_session_portable_context,
|
|
17
|
+
handle_session_export_bundle,
|
|
18
|
+
)
|
|
11
19
|
from user_context import get_context as _get_ctx
|
|
12
20
|
from tools_coordination import (
|
|
13
21
|
handle_track, handle_untrack, handle_files,
|
|
@@ -24,6 +32,7 @@ from tools_reminders_crud import (
|
|
|
24
32
|
from tools_learnings import (
|
|
25
33
|
handle_learning_add, handle_learning_search,
|
|
26
34
|
handle_learning_update, handle_learning_delete, handle_learning_list,
|
|
35
|
+
handle_learning_quality,
|
|
27
36
|
)
|
|
28
37
|
from tools_credentials import (
|
|
29
38
|
handle_credential_get, handle_credential_create,
|
|
@@ -164,6 +173,13 @@ mcp = FastMCP(
|
|
|
164
173
|
instructions=(
|
|
165
174
|
f"{_get_ctx().assistant_name} — cognitive co-operator. Save important info from tool results before they clear.\n\n"
|
|
166
175
|
"## CRITICAL — do these or you WILL get corrected\n"
|
|
176
|
+
"- **Protocol (MANDATORY for non-trivial work):** `nexo_task_open(...)` at the start and `nexo_task_close(...)` before saying done. "
|
|
177
|
+
"For edit/execute/delegate tasks this is the default path. Never claim completion without evidence.\n"
|
|
178
|
+
"- **Answer discipline (MANDATORY for non-trivial factual answers):** run `nexo_confidence_check(...)` or use `nexo_task_open(task_type='answer'|'analyze', ...)`. "
|
|
179
|
+
"If the response mode is `verify`, `ask`, or `defer`, follow it instead of answering from memory.\n"
|
|
180
|
+
"- **Workflow runtime (MANDATORY for long multi-step or cross-session work):** open `nexo_goal_open(...)` when the objective must survive sessions, then `nexo_workflow_open(...)`, "
|
|
181
|
+
"update meaningful checkpoints with `nexo_workflow_update(...)`, then use `nexo_workflow_resume(...)` / "
|
|
182
|
+
"`nexo_workflow_replay(...)` instead of restarting blindly.\n"
|
|
167
183
|
"- **Guard (MANDATORY before ANY code edit):** `nexo_guard_check(files='...', area='...')` BEFORE editing code. "
|
|
168
184
|
"No exceptions. Blocking rules→resolve first. `nexo_track(sid=SID, paths=[...])` before shared files\n"
|
|
169
185
|
"- **Skills (MANDATORY before multi-step tasks):** `nexo_skill_match(task)` to find reusable procedures. "
|
|
@@ -183,7 +199,7 @@ mcp = FastMCP(
|
|
|
183
199
|
"Detect intent, not keywords — works in ALL languages.\n"
|
|
184
200
|
"- **Delegate:** prefer direct. If needed: `nexo_context_packet(area)` + guard + 'if unsure STOP'\n"
|
|
185
201
|
"- **Memory:** `nexo_recall` searches all. Capture: errors→`nexo_learning_add`, prefs, entities, decisions\n"
|
|
186
|
-
"- **Change log:** `nexo_change_log(...)` after production edits. NOT for config dir\n"
|
|
202
|
+
"- **Change log:** `nexo_task_close` should be the default closure path. If you bypass it, call `nexo_change_log(...)` after production edits. NOT for config dir\n"
|
|
187
203
|
"- **Diary:** When user signals end of session (any language, any style — 'bye', 'done', 'cierro', etc.), "
|
|
188
204
|
"write `nexo_session_diary_write(...)` with self_critique BEFORE responding. "
|
|
189
205
|
"Detect intent, not keywords. If session closes without diary, auto_close handles it.\n"
|
|
@@ -274,6 +290,22 @@ def nexo_smart_startup() -> str:
|
|
|
274
290
|
return handle_smart_startup_query()
|
|
275
291
|
|
|
276
292
|
|
|
293
|
+
@mcp.tool
|
|
294
|
+
def nexo_session_portable_context(sid: str = "") -> str:
|
|
295
|
+
"""Build a portable handoff packet for another client/runtime.
|
|
296
|
+
|
|
297
|
+
Use this when another client should continue the same work with explicit
|
|
298
|
+
task/checkpoint/goal/workflow context instead of relying on memory alone.
|
|
299
|
+
"""
|
|
300
|
+
return handle_session_portable_context(sid)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@mcp.tool
|
|
304
|
+
def nexo_session_export_bundle(sid: str = "", path: str = "") -> str:
|
|
305
|
+
"""Export a machine-readable session bundle for cross-client handoff or archival."""
|
|
306
|
+
return handle_session_export_bundle(sid, path)
|
|
307
|
+
|
|
308
|
+
|
|
277
309
|
# ── Session Checkpoints (auto-compaction continuity) ──────────────
|
|
278
310
|
|
|
279
311
|
@mcp.tool
|
|
@@ -563,7 +595,17 @@ def nexo_followup_delete(id: str) -> str:
|
|
|
563
595
|
# ── Learnings CRUD (5 tools) ──────────────────────────────────────
|
|
564
596
|
|
|
565
597
|
@mcp.tool
|
|
566
|
-
def nexo_learning_add(
|
|
598
|
+
def nexo_learning_add(
|
|
599
|
+
category: str,
|
|
600
|
+
title: str,
|
|
601
|
+
content: str,
|
|
602
|
+
reasoning: str = "",
|
|
603
|
+
prevention: str = "",
|
|
604
|
+
applies_to: str = "",
|
|
605
|
+
review_days: int = 30,
|
|
606
|
+
priority: str = "medium",
|
|
607
|
+
supersedes_id: int = 0,
|
|
608
|
+
) -> str:
|
|
567
609
|
"""Add a new learning (resolved error, pattern, gotcha).
|
|
568
610
|
|
|
569
611
|
Args:
|
|
@@ -571,9 +613,17 @@ def nexo_learning_add(category: str, title: str, content: str, reasoning: str =
|
|
|
571
613
|
title: Short title for the learning.
|
|
572
614
|
content: Full description with context and solution.
|
|
573
615
|
reasoning: WHY this matters — what led to discovering this (optional).
|
|
616
|
+
prevention: Concrete rule/check that prevents repeating this mistake (optional).
|
|
617
|
+
applies_to: Files, systems, or areas this learning applies to (optional).
|
|
618
|
+
review_days: Days until this learning should be reviewed again (default 30).
|
|
574
619
|
priority: critical, high, medium, low (default: medium). Critical/high never decay below floor.
|
|
620
|
+
supersedes_id: Existing learning ID this new canonical rule replaces (optional).
|
|
575
621
|
"""
|
|
576
|
-
return handle_learning_add(
|
|
622
|
+
return handle_learning_add(
|
|
623
|
+
category, title, content, reasoning,
|
|
624
|
+
prevention=prevention, applies_to=applies_to,
|
|
625
|
+
review_days=review_days, priority=priority, supersedes_id=supersedes_id,
|
|
626
|
+
)
|
|
577
627
|
|
|
578
628
|
|
|
579
629
|
@mcp.tool
|
|
@@ -588,7 +638,19 @@ def nexo_learning_search(query: str, category: str = "") -> str:
|
|
|
588
638
|
|
|
589
639
|
|
|
590
640
|
@mcp.tool
|
|
591
|
-
def nexo_learning_update(
|
|
641
|
+
def nexo_learning_update(
|
|
642
|
+
id: int,
|
|
643
|
+
title: str = "",
|
|
644
|
+
content: str = "",
|
|
645
|
+
category: str = "",
|
|
646
|
+
reasoning: str = "",
|
|
647
|
+
prevention: str = "",
|
|
648
|
+
applies_to: str = "",
|
|
649
|
+
status: str = "",
|
|
650
|
+
review_days: int = 0,
|
|
651
|
+
priority: str = "",
|
|
652
|
+
supersedes_id: int = 0,
|
|
653
|
+
) -> str:
|
|
592
654
|
"""Update a learning entry. Only non-empty fields are changed.
|
|
593
655
|
|
|
594
656
|
Args:
|
|
@@ -596,9 +658,20 @@ def nexo_learning_update(id: int, title: str = "", content: str = "", category:
|
|
|
596
658
|
title: New title (optional).
|
|
597
659
|
content: New content (optional).
|
|
598
660
|
category: New category (optional).
|
|
661
|
+
reasoning: New reasoning/context (optional).
|
|
662
|
+
prevention: New prevention rule (optional).
|
|
663
|
+
applies_to: New applies_to target(s) (optional).
|
|
664
|
+
status: New status such as active/superseded (optional).
|
|
665
|
+
review_days: New review interval in days (optional).
|
|
599
666
|
priority: critical, high, medium, low (optional).
|
|
667
|
+
supersedes_id: Existing learning ID this updated canonical rule replaces (optional).
|
|
600
668
|
"""
|
|
601
|
-
return handle_learning_update(
|
|
669
|
+
return handle_learning_update(
|
|
670
|
+
id, title, content, category,
|
|
671
|
+
reasoning=reasoning, prevention=prevention, applies_to=applies_to,
|
|
672
|
+
status=status, review_days=review_days, priority=priority,
|
|
673
|
+
supersedes_id=supersedes_id,
|
|
674
|
+
)
|
|
602
675
|
|
|
603
676
|
|
|
604
677
|
@mcp.tool
|
|
@@ -621,6 +694,19 @@ def nexo_learning_list(category: str = "") -> str:
|
|
|
621
694
|
return handle_learning_list(category)
|
|
622
695
|
|
|
623
696
|
|
|
697
|
+
@mcp.tool
|
|
698
|
+
def nexo_learning_quality(id: int = 0, category: str = "", status: str = "active", limit: int = 20) -> str:
|
|
699
|
+
"""Score learning quality so fragile rules can be strengthened before they mislead guard or retrieval.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
id: Specific learning ID to inspect (optional).
|
|
703
|
+
category: Filter by category (optional).
|
|
704
|
+
status: Filter by lifecycle status such as active/superseded (default active).
|
|
705
|
+
limit: Max learnings to score when listing (default 20).
|
|
706
|
+
"""
|
|
707
|
+
return handle_learning_quality(id=id, category=category, status=status, limit=limit)
|
|
708
|
+
|
|
709
|
+
|
|
624
710
|
# ── Search index ──────────────────────────────────────────────────
|
|
625
711
|
|
|
626
712
|
@mcp.tool
|