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.
Files changed (98) hide show
  1. package/README.md +4 -4
  2. package/package.json +1 -1
  3. package/scripts/migrate-v1.7-to-v1.8.py +2 -2
  4. package/scripts/nexo-preflight.sh +236 -0
  5. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  6. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  7. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  8. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  9. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  10. package/src/auto_update.py +25 -0
  11. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  12. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  13. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  14. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  15. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  16. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  17. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  18. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  19. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  20. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  21. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  22. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  23. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  24. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  25. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  26. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  27. package/src/crons/manifest.json +6 -13
  28. package/src/crons/sync.py +151 -6
  29. package/src/db/__init__.py +13 -0
  30. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  31. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  32. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  33. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  34. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  35. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  36. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  37. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  38. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  39. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  40. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  41. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  42. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  43. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  44. package/src/db/_cron_runs.py +74 -0
  45. package/src/db/_episodic.py +40 -6
  46. package/src/db/_schema.py +64 -0
  47. package/src/db/_skills.py +514 -0
  48. package/src/hooks/session-stop.sh +13 -101
  49. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  50. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  51. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  52. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  53. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  54. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  55. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  56. package/src/plugins/episodic_memory.py +5 -3
  57. package/src/plugins/schedule.py +212 -0
  58. package/src/plugins/skills.py +264 -0
  59. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  60. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  61. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  62. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  63. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  64. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  65. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  66. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  67. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  68. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  69. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  70. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  71. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  72. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  73. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  74. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  75. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  76. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  77. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  78. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  79. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  80. package/src/scripts/deep-sleep/apply_findings.py +110 -8
  81. package/src/scripts/deep-sleep/collect.py +33 -11
  82. package/src/scripts/deep-sleep/extract-prompt.md +38 -0
  83. package/src/scripts/deep-sleep/extract.py +80 -8
  84. package/src/scripts/deep-sleep/synthesize-prompt.md +29 -1
  85. package/src/scripts/deep-sleep/synthesize.py +3 -1
  86. package/src/scripts/nexo-catchup.py +65 -29
  87. package/src/scripts/nexo-cron-wrapper.sh +53 -0
  88. package/src/scripts/nexo-daily-self-audit.py +4 -2
  89. package/src/scripts/nexo-deep-sleep.sh +66 -77
  90. package/src/scripts/nexo-evolution-run.py +13 -0
  91. package/src/scripts/nexo-learning-housekeep.py +156 -1
  92. package/src/scripts/nexo-learning-validator.py +19 -0
  93. package/src/scripts/nexo-postmortem-consolidator.py +3 -2
  94. package/src/scripts/nexo-sleep.py +16 -11
  95. package/src/scripts/nexo-synthesis.py +46 -3
  96. package/src/scripts/nexo-watchdog.sh +72 -19
  97. package/src/server.py +5 -1
  98. 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 — Add new monitors here
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
- # TYPE field: "core" = part of NEXO (goes to public repo), "personal" = user-specific
45
- # Format: NAME|PLIST_ID|LOG_STDOUT|LOG_STDERR|MAX_STALE_SECS|PROCESS_GREP|SCHEDULE_DESC|TYPE
46
- # Add your own monitors below. Core NEXO services are listed as examples.
47
- MONITORS=(
48
- "Catchup|com.nexo.catchup|$NEXO_HOME/logs/catchup-stdout.log|$NEXO_HOME/logs/catchup-stderr.log|0||RunAtLoad once|core"
49
- "Cognitive Decay|com.nexo.cognitive-decay|$NEXO_HOME/logs/cognitive-decay-stdout.log|$NEXO_HOME/logs/cognitive-decay-stderr.log|90000||Daily 3:00 AM|core"
50
- "Evolution|com.nexo.evolution|$NEXO_HOME/logs/evolution-stdout.log|$NEXO_HOME/logs/evolution-stderr.log|0||Weekly Sun 3:00 AM|core"
51
- "GitHub Monitor|com.nexo.github-monitor|$NEXO_HOME/logs/github-monitor-stdout.log|$NEXO_HOME/logs/github-monitor-stderr.log|90000||Daily 8:00 AM|core"
52
- "Immune|com.nexo.immune|$NEXO_HOME/logs/immune-stdout.log|$NEXO_HOME/logs/immune-stderr.log|3600||Every 30 min|core"
53
- "Postmortem|com.nexo.postmortem|$NEXO_HOME/logs/postmortem-stdout.log|$NEXO_HOME/logs/postmortem-stderr.log|90000||Daily 23:30|core"
54
- "Prevent Sleep|com.nexo.prevent-sleep|||0|caffeinate|KeepAlive|core"
55
- "Self Audit|com.nexo.self-audit|$NEXO_HOME/logs/self-audit-stdout.log|$NEXO_HOME/logs/self-audit-stderr.log|90000||Daily 7:00 AM|core"
56
- "Sleep|com.nexo.sleep|$NEXO_HOME/logs/sleep-stdout.log|$NEXO_HOME/logs/sleep-stderr.log|90000||Daily 4:00 AM|core"
57
- "Synthesis|com.nexo.synthesis|$NEXO_HOME/logs/synthesis-stdout.log|$NEXO_HOME/logs/synthesis-stderr.log|10800||Every 2 hours|core"
58
- "Deep Sleep|com.nexo.deep-sleep|$NEXO_HOME/logs/deep-sleep-stdout.log|$NEXO_HOME/logs/deep-sleep-stderr.log|90000||Daily 4:30 AM|core"
59
- "Followup Hygiene|com.nexo.followup-hygiene|$NEXO_HOME/logs/followup-hygiene-stdout.log|$NEXO_HOME/logs/followup-hygiene-stderr.log|604800||Weekly Sun 5:00 AM|core"
60
- # Add your own personal monitors below (type "personal"):
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:** `nexo_session_diary_write(...)` mandatory on close. Include self_critique\n"
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()