nexo-brain 2.2.0 → 2.3.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/README.md +4 -4
- package/package.json +1 -1
- package/scripts/migrate-v1.7-to-v1.8.py +2 -2
- package/scripts/nexo-preflight.sh +236 -0
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/auto_update.py +25 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest.json +6 -13
- package/src/crons/sync.py +151 -6
- package/src/db/__init__.py +13 -0
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
- package/src/db/_cron_runs.py +74 -0
- package/src/db/_episodic.py +40 -6
- package/src/db/_schema.py +64 -0
- package/src/db/_skills.py +514 -0
- package/src/hooks/session-stop.sh +13 -101
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
- package/src/plugins/episodic_memory.py +5 -3
- package/src/plugins/schedule.py +212 -0
- package/src/plugins/skills.py +264 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/apply_findings.py +110 -8
- package/src/scripts/deep-sleep/collect.py +33 -11
- package/src/scripts/deep-sleep/extract-prompt.md +38 -0
- package/src/scripts/deep-sleep/extract.py +80 -8
- package/src/scripts/deep-sleep/synthesize-prompt.md +29 -1
- package/src/scripts/deep-sleep/synthesize.py +3 -1
- package/src/scripts/nexo-catchup.py +65 -29
- package/src/scripts/nexo-cron-wrapper.sh +53 -0
- package/src/scripts/nexo-daily-self-audit.py +4 -2
- package/src/scripts/nexo-deep-sleep.sh +66 -77
- package/src/scripts/nexo-evolution-run.py +13 -0
- package/src/scripts/nexo-learning-housekeep.py +156 -1
- package/src/scripts/nexo-learning-validator.py +19 -0
- package/src/scripts/nexo-postmortem-consolidator.py +3 -2
- package/src/scripts/nexo-sleep.py +16 -11
- package/src/scripts/nexo-synthesis.py +46 -3
- package/src/scripts/nexo-watchdog.sh +72 -19
- package/src/server.py +5 -1
- package/src/scripts/nexo-github-monitor.py +0 -256
|
@@ -32,34 +32,87 @@ TS_EPOCH=$(date +%s)
|
|
|
32
32
|
log() { echo "[$TS] $1" >> "$LOG"; }
|
|
33
33
|
|
|
34
34
|
# ============================================================================
|
|
35
|
-
# MONITOR REGISTRY —
|
|
35
|
+
# MONITOR REGISTRY — generated dynamically from manifest.json
|
|
36
36
|
# ============================================================================
|
|
37
|
-
# Format: NAME|PLIST_ID|LOG_STDOUT|LOG_STDERR|MAX_STALE_SECS|PROCESS_GREP|SCHEDULE_DESC
|
|
37
|
+
# Format: NAME|PLIST_ID|LOG_STDOUT|LOG_STDERR|MAX_STALE_SECS|PROCESS_GREP|SCHEDULE_DESC|TYPE
|
|
38
38
|
#
|
|
39
39
|
# MAX_STALE_SECS: how old stdout log can be before WARN.
|
|
40
40
|
# 0 = skip staleness check (for one-shot or infrequent tasks)
|
|
41
41
|
# WARN at MAX_STALE_SECS, FAIL at 3x MAX_STALE_SECS
|
|
42
42
|
# PROCESS_GREP: pattern to grep in ps (empty = skip process check)
|
|
43
43
|
# ============================================================================
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# Add
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
44
|
+
# Core monitors are built from crons/manifest.json (single source of truth).
|
|
45
|
+
# The NEXO_CODE env var must point to the repo src/ directory.
|
|
46
|
+
# Add personal (non-manifest) monitors to PERSONAL_MONITORS below.
|
|
47
|
+
NEXO_CODE="${NEXO_CODE:-$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)}"
|
|
48
|
+
MANIFEST_FILE="$NEXO_CODE/crons/manifest.json"
|
|
49
|
+
|
|
50
|
+
_build_monitors_from_manifest() {
|
|
51
|
+
if [ ! -f "$MANIFEST_FILE" ]; then
|
|
52
|
+
log "WARNING: manifest.json not found at $MANIFEST_FILE — no core monitors loaded"
|
|
53
|
+
return
|
|
54
|
+
fi
|
|
55
|
+
python3 -c "
|
|
56
|
+
import json, sys
|
|
57
|
+
|
|
58
|
+
nexo_home = '$NEXO_HOME'
|
|
59
|
+
|
|
60
|
+
with open('$MANIFEST_FILE') as f:
|
|
61
|
+
data = json.load(f)
|
|
62
|
+
|
|
63
|
+
for c in data.get('crons', []):
|
|
64
|
+
cid = c['id']
|
|
65
|
+
# Derive human-readable name from id
|
|
66
|
+
name = cid.replace('-', ' ').title()
|
|
67
|
+
plist_id = 'com.nexo.' + cid
|
|
68
|
+
stdout_log = nexo_home + '/logs/' + cid + '-stdout.log'
|
|
69
|
+
stderr_log = nexo_home + '/logs/' + cid + '-stderr.log'
|
|
70
|
+
|
|
71
|
+
# Derive max_stale_secs and schedule_desc from schedule config
|
|
72
|
+
if c.get('run_at_load'):
|
|
73
|
+
max_stale = 0
|
|
74
|
+
schedule_desc = 'RunAtLoad once'
|
|
75
|
+
elif 'interval_seconds' in c:
|
|
76
|
+
iv = c['interval_seconds']
|
|
77
|
+
# Allow 2x the interval before WARN
|
|
78
|
+
max_stale = iv * 2
|
|
79
|
+
if iv >= 3600:
|
|
80
|
+
schedule_desc = f'Every {iv // 3600}h'
|
|
81
|
+
else:
|
|
82
|
+
schedule_desc = f'Every {iv // 60} min'
|
|
83
|
+
elif 'schedule' in c:
|
|
84
|
+
s = c['schedule']
|
|
85
|
+
h = s.get('hour', 0)
|
|
86
|
+
m = s.get('minute', 0)
|
|
87
|
+
if 'weekday' in s:
|
|
88
|
+
days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
|
|
89
|
+
schedule_desc = f'Weekly {days[s[\"weekday\"]]} {h}:{m:02d}'
|
|
90
|
+
max_stale = 0 # weekly tasks: skip staleness
|
|
91
|
+
else:
|
|
92
|
+
schedule_desc = f'Daily {h}:{m:02d}'
|
|
93
|
+
max_stale = 90000 # ~25h
|
|
94
|
+
else:
|
|
95
|
+
max_stale = 0
|
|
96
|
+
schedule_desc = 'unknown'
|
|
97
|
+
|
|
98
|
+
mon_type = 'core' if c.get('core') else 'personal'
|
|
99
|
+
proc_grep = '' # manifest crons are one-shot, no persistent process
|
|
100
|
+
|
|
101
|
+
print(f'{name}|{plist_id}|{stdout_log}|{stderr_log}|{max_stale}|{proc_grep}|{schedule_desc}|{mon_type}')
|
|
102
|
+
" 2>/dev/null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
MONITORS=()
|
|
106
|
+
while IFS= read -r line; do
|
|
107
|
+
[ -n "$line" ] && MONITORS+=("$line")
|
|
108
|
+
done < <(_build_monitors_from_manifest)
|
|
109
|
+
|
|
110
|
+
# Personal (non-manifest) monitors — add yours below.
|
|
111
|
+
# These are NOT in manifest.json and won't be synced by cron-sync.
|
|
112
|
+
PERSONAL_MONITORS=(
|
|
61
113
|
# "My Service|com.nexo.my-service|$NEXO_HOME/logs/my-service.log||3600||Every 30 min|personal"
|
|
62
114
|
)
|
|
115
|
+
MONITORS+=("${PERSONAL_MONITORS[@]+"${PERSONAL_MONITORS[@]}"}")
|
|
63
116
|
|
|
64
117
|
# Cron jobs to check (NAME|SCRIPT|CHECK_PATH|MAX_STALE_SECS|SCHEDULE)
|
|
65
118
|
CRON_MONITORS=(
|
package/src/server.py
CHANGED
|
@@ -106,8 +106,12 @@ mcp = FastMCP(
|
|
|
106
106
|
"- **Delegate:** prefer direct. If needed: `nexo_context_packet(area)` + guard + 'if unsure STOP'\n"
|
|
107
107
|
"- **Memory:** `nexo_recall` searches all. Capture: errors→`nexo_learning_add`, prefs, entities, decisions\n"
|
|
108
108
|
"- **Change log:** `nexo_change_log(...)` after production edits. NOT for config dir\n"
|
|
109
|
-
"- **Diary:**
|
|
109
|
+
"- **Diary:** When user signals end of session (any language, any style — 'bye', 'done', 'cierro', etc.), "
|
|
110
|
+
"write `nexo_session_diary_write(...)` with self_critique BEFORE responding. "
|
|
111
|
+
"Detect intent, not keywords. If session closes without diary, auto_close handles it.\n"
|
|
110
112
|
"- **Cortex:** `nexo_cortex_check` before budget/campaign/architecture changes\n"
|
|
113
|
+
"- **Skills:** before multi-step tasks, `nexo_skill_match(task)` to find reusable procedures. "
|
|
114
|
+
"If match found, read it and follow the steps. After completion, `nexo_skill_result(id, success, context)` to record outcome.\n"
|
|
111
115
|
"- **Dissonance:** user contradicts memory→`nexo_cognitive_dissonance`. Frustrated→force=True\n"
|
|
112
116
|
"- **Trust:** <40=paranoid verify twice, >80=fluid. Check: `nexo_cognitive_trust`"
|
|
113
117
|
),
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
NEXO GitHub Monitor — Wrapper + CLI pattern.
|
|
4
|
-
Python: gh CLI API calls, data collection.
|
|
5
|
-
CLI: Generates rich analysis and suggested responses for issues/PRs.
|
|
6
|
-
|
|
7
|
-
Runs at 08:00 via LaunchAgent.
|
|
8
|
-
Results saved to ~/.nexo/github-status.json.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import os
|
|
13
|
-
import subprocess
|
|
14
|
-
import sys
|
|
15
|
-
from datetime import datetime, timedelta
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
NEXO_HOME = Path(os.environ.get("NEXO_HOME", Path.home() / ".nexo"))
|
|
19
|
-
STATUS_FILE = NEXO_HOME / "github-status.json"
|
|
20
|
-
LOG_FILE = NEXO_HOME / "logs" / "github-monitor.log"
|
|
21
|
-
REPO = "wazionapps/nexo"
|
|
22
|
-
CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def log(msg: str):
|
|
26
|
-
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
27
|
-
line = f"[{ts}] {msg}"
|
|
28
|
-
print(line, flush=True)
|
|
29
|
-
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
-
with open(LOG_FILE, "a") as f:
|
|
31
|
-
f.write(line + "\n")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def gh_api(endpoint: str) -> dict | list | None:
|
|
35
|
-
"""Call GitHub API via gh."""
|
|
36
|
-
try:
|
|
37
|
-
result = subprocess.run(
|
|
38
|
-
["gh", "api", endpoint],
|
|
39
|
-
capture_output=True, text=True, timeout=21600
|
|
40
|
-
)
|
|
41
|
-
if result.returncode == 0:
|
|
42
|
-
return json.loads(result.stdout)
|
|
43
|
-
except Exception:
|
|
44
|
-
pass
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def collect_data():
|
|
49
|
-
"""Collect all GitHub data — mechanical work."""
|
|
50
|
-
data = {
|
|
51
|
-
"timestamp": datetime.now().isoformat(),
|
|
52
|
-
"repo": REPO,
|
|
53
|
-
"issues": [],
|
|
54
|
-
"prs": [],
|
|
55
|
-
"latest_release": None,
|
|
56
|
-
"unreleased_commits": 0,
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
# Issues
|
|
60
|
-
log("Fetching issues...")
|
|
61
|
-
issues = gh_api(f"repos/{REPO}/issues?state=open&per_page=50")
|
|
62
|
-
if issues:
|
|
63
|
-
for issue in issues:
|
|
64
|
-
if "pull_request" in issue:
|
|
65
|
-
continue
|
|
66
|
-
item = {
|
|
67
|
-
"number": issue["number"],
|
|
68
|
-
"title": issue["title"][:80],
|
|
69
|
-
"body": (issue.get("body") or "")[:500],
|
|
70
|
-
"created": issue["created_at"][:10],
|
|
71
|
-
"comments": issue["comments"],
|
|
72
|
-
"labels": [l["name"] for l in issue.get("labels", [])],
|
|
73
|
-
"author": issue.get("user", {}).get("login", ""),
|
|
74
|
-
}
|
|
75
|
-
# Get comment bodies for context
|
|
76
|
-
if issue["comments"] > 0:
|
|
77
|
-
comments = gh_api(f"repos/{REPO}/issues/{issue['number']}/comments?per_page=5")
|
|
78
|
-
if comments:
|
|
79
|
-
item["comment_bodies"] = [
|
|
80
|
-
{"author": c.get("user", {}).get("login", ""), "body": c.get("body", "")[:300]}
|
|
81
|
-
for c in comments[:5]
|
|
82
|
-
]
|
|
83
|
-
data["issues"].append(item)
|
|
84
|
-
|
|
85
|
-
# PRs
|
|
86
|
-
log("Fetching PRs...")
|
|
87
|
-
prs = gh_api(f"repos/{REPO}/pulls?state=open&per_page=50")
|
|
88
|
-
if prs:
|
|
89
|
-
for pr in prs:
|
|
90
|
-
reviews = gh_api(f"repos/{REPO}/pulls/{pr['number']}/reviews") or []
|
|
91
|
-
item = {
|
|
92
|
-
"number": pr["number"],
|
|
93
|
-
"title": pr["title"][:80],
|
|
94
|
-
"body": (pr.get("body") or "")[:500],
|
|
95
|
-
"author": pr["user"]["login"],
|
|
96
|
-
"created": pr["created_at"][:10],
|
|
97
|
-
"reviews": len(reviews),
|
|
98
|
-
"changed_files": pr.get("changed_files", 0),
|
|
99
|
-
}
|
|
100
|
-
data["prs"].append(item)
|
|
101
|
-
|
|
102
|
-
# Releases
|
|
103
|
-
log("Fetching releases...")
|
|
104
|
-
releases = gh_api(f"repos/{REPO}/releases?per_page=1")
|
|
105
|
-
if releases and len(releases) > 0:
|
|
106
|
-
data["latest_release"] = releases[0].get("tag_name", "none")
|
|
107
|
-
tag = releases[0].get("tag_name", "")
|
|
108
|
-
if tag:
|
|
109
|
-
try:
|
|
110
|
-
result = subprocess.run(
|
|
111
|
-
["gh", "api", f"repos/{REPO}/compare/{tag}...main"],
|
|
112
|
-
capture_output=True, text=True, timeout=21600
|
|
113
|
-
)
|
|
114
|
-
if result.returncode == 0:
|
|
115
|
-
compare = json.loads(result.stdout)
|
|
116
|
-
data["unreleased_commits"] = compare.get("ahead_by", 0)
|
|
117
|
-
except Exception:
|
|
118
|
-
pass
|
|
119
|
-
|
|
120
|
-
return data
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def analyze_via_cli(data):
|
|
124
|
-
"""Pass collected data to CLI for analysis and suggested responses."""
|
|
125
|
-
data_json = json.dumps(data, ensure_ascii=False)
|
|
126
|
-
|
|
127
|
-
prompt = f"""Analyze this GitHub repository status for NEXO Brain (wazionapps/nexo).
|
|
128
|
-
|
|
129
|
-
DATA:
|
|
130
|
-
{data_json}
|
|
131
|
-
|
|
132
|
-
Generate a status report with:
|
|
133
|
-
1. SUMMARY: counts of open issues, PRs, unresponded items
|
|
134
|
-
2. For each UNRESPONDED ISSUE (comments=0): suggest a response in English (technical, helpful, friendly)
|
|
135
|
-
3. For each PR: brief assessment (looks good / needs changes / needs review)
|
|
136
|
-
4. RELEASE STATUS: if >10 unreleased commits, recommend a release
|
|
137
|
-
5. ALERTS: anything needing immediate attention (stale issues >7d, etc.)
|
|
138
|
-
|
|
139
|
-
Return as JSON:
|
|
140
|
-
{{
|
|
141
|
-
"summary": {{
|
|
142
|
-
"open_issues": N,
|
|
143
|
-
"unresponded_issues": N,
|
|
144
|
-
"stale_issues": N,
|
|
145
|
-
"open_prs": N,
|
|
146
|
-
"unreviewed_prs": N,
|
|
147
|
-
"unreleased_commits": N
|
|
148
|
-
}},
|
|
149
|
-
"issue_responses": [
|
|
150
|
-
{{"number": N, "suggested_response": "text"}},
|
|
151
|
-
...
|
|
152
|
-
],
|
|
153
|
-
"pr_assessments": [
|
|
154
|
-
{{"number": N, "assessment": "text"}},
|
|
155
|
-
...
|
|
156
|
-
],
|
|
157
|
-
"alerts": ["alert1", ...],
|
|
158
|
-
"release_recommendation": "text or null"
|
|
159
|
-
}}"""
|
|
160
|
-
)
|
|
161
|
-
if auth_check.returncode != 0:
|
|
162
|
-
# CLI not authenticated, skip gracefully
|
|
163
|
-
return ""
|
|
164
|
-
|
|
165
|
-
env = os.environ.copy()
|
|
166
|
-
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
167
|
-
env.pop("CLAUDECODE", None)
|
|
168
|
-
env.pop("CLAUDE_CODE", None)
|
|
169
|
-
|
|
170
|
-
result = subprocess.run(
|
|
171
|
-
[str(CLAUDE_CLI), "-p", prompt,
|
|
172
|
-
"--model", "opus", "--output-format", "text",
|
|
173
|
-
"--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
|
|
174
|
-
capture_output=True, text=True, timeout=21600, env=env
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
if result.returncode != 0:
|
|
178
|
-
log(f"CLI analysis failed: {result.stderr[:200]}")
|
|
179
|
-
return None
|
|
180
|
-
|
|
181
|
-
output = result.stdout.strip()
|
|
182
|
-
start = output.find("{")
|
|
183
|
-
end = output.rfind("}") + 1
|
|
184
|
-
if start >= 0 and end > start:
|
|
185
|
-
return json.loads(output[start:end])
|
|
186
|
-
return None
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def main():
|
|
190
|
-
log("=== NEXO GitHub Monitor ===")
|
|
191
|
-
|
|
192
|
-
# Step 1: Collect data (mechanical)
|
|
193
|
-
data = collect_data()
|
|
194
|
-
|
|
195
|
-
# Step 2: Analyze via CLI (intelligent)
|
|
196
|
-
log("Analyzing via CLI...")
|
|
197
|
-
analysis = analyze_via_cli(data)
|
|
198
|
-
|
|
199
|
-
# Build status file
|
|
200
|
-
status = {
|
|
201
|
-
"timestamp": data["timestamp"],
|
|
202
|
-
"repo": REPO,
|
|
203
|
-
"issues": {
|
|
204
|
-
"open": len(data["issues"]),
|
|
205
|
-
"unresponded": sum(1 for i in data["issues"] if i["comments"] == 0),
|
|
206
|
-
"stale": sum(1 for i in data["issues"]
|
|
207
|
-
if i["comments"] == 0 and i["created"] < (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')),
|
|
208
|
-
"items": [{"number": i["number"], "title": i["title"], "created": i["created"],
|
|
209
|
-
"comments": i["comments"], "labels": i["labels"]} for i in data["issues"]],
|
|
210
|
-
},
|
|
211
|
-
"prs": {
|
|
212
|
-
"open": len(data["prs"]),
|
|
213
|
-
"unreviewed": sum(1 for p in data["prs"] if p["reviews"] == 0),
|
|
214
|
-
"items": [{"number": p["number"], "title": p["title"], "author": p["author"],
|
|
215
|
-
"created": p["created"], "reviews": p["reviews"]} for p in data["prs"]],
|
|
216
|
-
},
|
|
217
|
-
"releases": {
|
|
218
|
-
"latest": data["latest_release"] or "none",
|
|
219
|
-
"unreleased_commits": data["unreleased_commits"],
|
|
220
|
-
},
|
|
221
|
-
"alerts": [],
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
# Merge CLI analysis
|
|
225
|
-
if analysis:
|
|
226
|
-
status["alerts"] = analysis.get("alerts", [])
|
|
227
|
-
status["issue_responses"] = analysis.get("issue_responses", [])
|
|
228
|
-
status["pr_assessments"] = analysis.get("pr_assessments", [])
|
|
229
|
-
status["release_recommendation"] = analysis.get("release_recommendation")
|
|
230
|
-
else:
|
|
231
|
-
# Fallback alerts without CLI
|
|
232
|
-
if status["issues"]["unresponded"] > 0:
|
|
233
|
-
status["alerts"].append(f"{status['issues']['unresponded']} issues without response")
|
|
234
|
-
if status["issues"]["stale"] > 0:
|
|
235
|
-
status["alerts"].append(f"{status['issues']['stale']} stale issues (>7d)")
|
|
236
|
-
if status["prs"]["unreviewed"] > 0:
|
|
237
|
-
status["alerts"].append(f"{status['prs']['unreviewed']} PRs awaiting review")
|
|
238
|
-
if data["unreleased_commits"] > 10:
|
|
239
|
-
status["alerts"].append(f"{data['unreleased_commits']} unreleased commits")
|
|
240
|
-
|
|
241
|
-
# Log summary
|
|
242
|
-
log(f"Issues: {status['issues']['open']} open ({status['issues']['unresponded']} unresponded)")
|
|
243
|
-
log(f"PRs: {status['prs']['open']} open ({status['prs']['unreviewed']} unreviewed)")
|
|
244
|
-
log(f"Latest release: {status['releases']['latest']}")
|
|
245
|
-
if status["alerts"]:
|
|
246
|
-
log(f"ALERTS: {'; '.join(status['alerts'])}")
|
|
247
|
-
|
|
248
|
-
# Save
|
|
249
|
-
STATUS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
250
|
-
STATUS_FILE.write_text(json.dumps(status, indent=2))
|
|
251
|
-
log(f"Status saved to {STATUS_FILE}")
|
|
252
|
-
log("=== Done ===")
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if __name__ == "__main__":
|
|
256
|
-
main()
|