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
|
@@ -19,6 +19,7 @@ Runs via launchd at 7:00 AM daily.
|
|
|
19
19
|
import json
|
|
20
20
|
import hashlib
|
|
21
21
|
import os
|
|
22
|
+
import re
|
|
22
23
|
import sqlite3
|
|
23
24
|
import subprocess
|
|
24
25
|
import sys
|
|
@@ -37,6 +38,8 @@ from agent_runner import AutomationBackendUnavailableError, run_automation_promp
|
|
|
37
38
|
|
|
38
39
|
LOG_DIR = NEXO_HOME / "logs"
|
|
39
40
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
AUDIT_HISTORY_DIR = LOG_DIR / "self-audit"
|
|
42
|
+
AUDIT_HISTORY_DIR.mkdir(parents=True, exist_ok=True)
|
|
40
43
|
LOG_FILE = LOG_DIR / "self-audit.log"
|
|
41
44
|
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
42
45
|
# Configure your main project repo to check for uncommitted changes (optional)
|
|
@@ -85,6 +88,314 @@ def finding(severity, area, msg):
|
|
|
85
88
|
log(f" [{severity}] {area}: {msg}")
|
|
86
89
|
|
|
87
90
|
|
|
91
|
+
def _parse_iso_dt(value: str | None) -> datetime | None:
|
|
92
|
+
text = str(value or "").strip()
|
|
93
|
+
if not text:
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
return datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
97
|
+
except Exception:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _area_summary_from_daily_summaries(summaries: list[dict]) -> tuple[list[dict], list[str]]:
|
|
102
|
+
per_area: dict[str, dict] = {}
|
|
103
|
+
area_days: dict[str, set[str]] = {}
|
|
104
|
+
for item in summaries:
|
|
105
|
+
day = str(item.get("date_label") or item.get("timestamp") or "")[:10]
|
|
106
|
+
for finding_item in item.get("findings", []):
|
|
107
|
+
area = str(finding_item.get("area") or "unknown").strip() or "unknown"
|
|
108
|
+
severity = str(finding_item.get("severity") or "INFO").strip().upper()
|
|
109
|
+
bucket = per_area.setdefault(area, {"area": area, "count": 0, "error": 0, "warn": 0, "info": 0})
|
|
110
|
+
bucket["count"] += 1
|
|
111
|
+
if severity == "ERROR":
|
|
112
|
+
bucket["error"] += 1
|
|
113
|
+
elif severity == "WARN":
|
|
114
|
+
bucket["warn"] += 1
|
|
115
|
+
else:
|
|
116
|
+
bucket["info"] += 1
|
|
117
|
+
if day:
|
|
118
|
+
area_days.setdefault(area, set()).add(day)
|
|
119
|
+
top_areas = sorted(
|
|
120
|
+
per_area.values(),
|
|
121
|
+
key=lambda item: (-item["count"], -item["error"], item["area"]),
|
|
122
|
+
)[:10]
|
|
123
|
+
repeated = sorted(area for area, days in area_days.items() if len(days) >= 2)
|
|
124
|
+
return top_areas, repeated
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _load_recent_daily_summaries(reference_dt: datetime, window_days: int) -> list[dict]:
|
|
128
|
+
summaries: list[dict] = []
|
|
129
|
+
cutoff = reference_dt - timedelta(days=window_days - 1)
|
|
130
|
+
for path in sorted(AUDIT_HISTORY_DIR.glob("*-daily-summary.json")):
|
|
131
|
+
try:
|
|
132
|
+
payload = json.loads(path.read_text())
|
|
133
|
+
except Exception:
|
|
134
|
+
continue
|
|
135
|
+
ts = _parse_iso_dt(payload.get("timestamp"))
|
|
136
|
+
if not ts:
|
|
137
|
+
continue
|
|
138
|
+
if ts.date() < cutoff.date() or ts.date() > reference_dt.date():
|
|
139
|
+
continue
|
|
140
|
+
summaries.append(payload)
|
|
141
|
+
summaries.sort(key=lambda item: str(item.get("timestamp") or ""))
|
|
142
|
+
return summaries
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def write_horizon_summaries(summary_payload: dict, *, now: datetime | None = None) -> dict:
|
|
146
|
+
now = now or datetime.now()
|
|
147
|
+
daily_payload = dict(summary_payload)
|
|
148
|
+
daily_payload.setdefault("date_label", now.strftime("%Y-%m-%d"))
|
|
149
|
+
daily_file = AUDIT_HISTORY_DIR / f"{daily_payload['date_label']}-daily-summary.json"
|
|
150
|
+
daily_file.write_text(json.dumps(daily_payload, indent=2))
|
|
151
|
+
|
|
152
|
+
outputs = {
|
|
153
|
+
"daily_file": str(daily_file),
|
|
154
|
+
"weekly_file": "",
|
|
155
|
+
"weekly_latest": "",
|
|
156
|
+
"monthly_file": "",
|
|
157
|
+
"monthly_latest": "",
|
|
158
|
+
}
|
|
159
|
+
for kind, window_days in (("weekly", 7), ("monthly", 30)):
|
|
160
|
+
recent = _load_recent_daily_summaries(now, window_days)
|
|
161
|
+
total_counts = {"error": 0, "warn": 0, "info": 0}
|
|
162
|
+
for item in recent:
|
|
163
|
+
counts = item.get("counts") or {}
|
|
164
|
+
for key in total_counts:
|
|
165
|
+
total_counts[key] += int(counts.get(key) or 0)
|
|
166
|
+
top_areas, repeated_areas = _area_summary_from_daily_summaries(recent)
|
|
167
|
+
if kind == "weekly":
|
|
168
|
+
year, week, _ = now.isocalendar()
|
|
169
|
+
label = f"{year}-W{week:02d}"
|
|
170
|
+
else:
|
|
171
|
+
label = now.strftime("%Y-%m")
|
|
172
|
+
rollup = {
|
|
173
|
+
"timestamp": now.isoformat(),
|
|
174
|
+
"label": label,
|
|
175
|
+
"horizon": kind,
|
|
176
|
+
"window_days": window_days,
|
|
177
|
+
"source_daily_summaries": len(recent),
|
|
178
|
+
"days": [item.get("date_label") for item in recent if item.get("date_label")],
|
|
179
|
+
"counts": total_counts,
|
|
180
|
+
"top_areas": top_areas,
|
|
181
|
+
"repeated_areas": repeated_areas,
|
|
182
|
+
}
|
|
183
|
+
dated_file = AUDIT_HISTORY_DIR / f"{label}-{kind}-summary.json"
|
|
184
|
+
latest_file = LOG_DIR / f"self-audit-{kind}-summary.json"
|
|
185
|
+
dated_file.write_text(json.dumps(rollup, indent=2))
|
|
186
|
+
latest_file.write_text(json.dumps(rollup, indent=2))
|
|
187
|
+
outputs[f"{kind}_file"] = str(dated_file)
|
|
188
|
+
outputs[f"{kind}_latest"] = str(latest_file)
|
|
189
|
+
return outputs
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _protocol_debt_table_exists(conn: sqlite3.Connection) -> bool:
|
|
193
|
+
row = conn.execute(
|
|
194
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='protocol_debt'"
|
|
195
|
+
).fetchone()
|
|
196
|
+
return bool(row)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _table_exists(conn: sqlite3.Connection, table_name: str) -> bool:
|
|
200
|
+
row = conn.execute(
|
|
201
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name = ?",
|
|
202
|
+
(table_name,),
|
|
203
|
+
).fetchone()
|
|
204
|
+
return bool(row)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _ensure_protocol_debt(conn: sqlite3.Connection, *, debt_type: str, severity: str, evidence: str) -> bool:
|
|
208
|
+
existing = conn.execute(
|
|
209
|
+
"""SELECT id
|
|
210
|
+
FROM protocol_debt
|
|
211
|
+
WHERE status = 'open' AND debt_type = ? AND evidence = ?
|
|
212
|
+
LIMIT 1""",
|
|
213
|
+
(debt_type, evidence),
|
|
214
|
+
).fetchone()
|
|
215
|
+
if existing:
|
|
216
|
+
return False
|
|
217
|
+
conn.execute(
|
|
218
|
+
"""INSERT INTO protocol_debt (session_id, task_id, debt_type, severity, evidence)
|
|
219
|
+
VALUES ('', '', ?, ?, ?)""",
|
|
220
|
+
(debt_type, severity, evidence),
|
|
221
|
+
)
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _ensure_followup(conn: sqlite3.Connection, *, prefix: str, description: str,
|
|
226
|
+
verification: str, reasoning: str, priority: str = "high") -> str:
|
|
227
|
+
if not _table_exists(conn, "followups"):
|
|
228
|
+
return ""
|
|
229
|
+
existing = conn.execute(
|
|
230
|
+
"""SELECT id FROM followups
|
|
231
|
+
WHERE status NOT LIKE 'COMPLETED%'
|
|
232
|
+
AND status NOT IN ('DELETED','archived','blocked','waiting')
|
|
233
|
+
AND description = ?
|
|
234
|
+
LIMIT 1""",
|
|
235
|
+
(description,),
|
|
236
|
+
).fetchone()
|
|
237
|
+
if existing:
|
|
238
|
+
return str(existing["id"])
|
|
239
|
+
|
|
240
|
+
followup_id = f"NF-{prefix}-{hashlib.sha1(description.encode('utf-8')).hexdigest()[:8].upper()}"
|
|
241
|
+
now_epoch = int(datetime.now().timestamp())
|
|
242
|
+
columns = {row["name"] for row in conn.execute("PRAGMA table_info(followups)").fetchall()}
|
|
243
|
+
values = {
|
|
244
|
+
"id": followup_id,
|
|
245
|
+
"date": "",
|
|
246
|
+
"description": description,
|
|
247
|
+
"verification": verification,
|
|
248
|
+
"status": "PENDING",
|
|
249
|
+
"reasoning": reasoning,
|
|
250
|
+
"recurrence": None,
|
|
251
|
+
"created_at": now_epoch,
|
|
252
|
+
"updated_at": now_epoch,
|
|
253
|
+
}
|
|
254
|
+
if "priority" in columns:
|
|
255
|
+
values["priority"] = priority
|
|
256
|
+
|
|
257
|
+
ordered_columns = [name for name in values.keys() if name in columns]
|
|
258
|
+
placeholders = ", ".join("?" for _ in ordered_columns)
|
|
259
|
+
conn.execute(
|
|
260
|
+
f"INSERT INTO followups ({', '.join(ordered_columns)}) VALUES ({placeholders})",
|
|
261
|
+
[values[name] for name in ordered_columns],
|
|
262
|
+
)
|
|
263
|
+
return followup_id
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
TOPIC_STOPWORDS = {
|
|
267
|
+
"the", "and", "for", "with", "from", "that", "this", "into", "about", "after",
|
|
268
|
+
"before", "again", "need", "needs", "task", "tasks", "work", "working",
|
|
269
|
+
"continue", "continuing", "review", "check", "checks", "make", "making",
|
|
270
|
+
"fix", "fixes", "build", "create", "created", "update", "updates", "ship",
|
|
271
|
+
"prepare", "finish", "open", "another", "around", "must",
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _topic_signature(text: str) -> str:
|
|
276
|
+
tokens = [
|
|
277
|
+
token for token in re.findall(r"[a-z0-9]+", (text or "").lower())
|
|
278
|
+
if len(token) >= 3 and token not in TOPIC_STOPWORDS
|
|
279
|
+
]
|
|
280
|
+
return " ".join(tokens[:2])
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
REPAIR_KEYWORDS = {
|
|
284
|
+
"fix", "fixed", "bug", "bugs", "regression", "regressions", "repair", "repaired",
|
|
285
|
+
"correct", "corrected", "correction", "typo", "hotfix", "patch", "patched",
|
|
286
|
+
"resolve", "resolved", "failure", "error", "issue", "broken", "broke",
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _split_changed_files(raw: str) -> list[str]:
|
|
291
|
+
text = str(raw or "").strip()
|
|
292
|
+
if not text:
|
|
293
|
+
return []
|
|
294
|
+
if text.startswith("["):
|
|
295
|
+
try:
|
|
296
|
+
value = json.loads(text)
|
|
297
|
+
except Exception:
|
|
298
|
+
value = []
|
|
299
|
+
if isinstance(value, list):
|
|
300
|
+
return [str(item).strip() for item in value if str(item).strip()]
|
|
301
|
+
parts = re.split(r"[\n,;]+", text)
|
|
302
|
+
return [part.strip() for part in parts if part.strip()]
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _looks_like_repair_change(text: str) -> bool:
|
|
306
|
+
tokens = {token for token in re.findall(r"[a-z0-9]+", (text or "").lower()) if len(token) >= 3}
|
|
307
|
+
return bool(tokens & REPAIR_KEYWORDS)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _parse_mixed_datetime(value) -> datetime | None:
|
|
311
|
+
if value in (None, ""):
|
|
312
|
+
return None
|
|
313
|
+
if isinstance(value, (int, float)):
|
|
314
|
+
try:
|
|
315
|
+
return datetime.fromtimestamp(float(value))
|
|
316
|
+
except Exception:
|
|
317
|
+
return None
|
|
318
|
+
text = str(value).strip()
|
|
319
|
+
if not text:
|
|
320
|
+
return None
|
|
321
|
+
try:
|
|
322
|
+
return datetime.fromisoformat(text.replace("Z", "+00:00")).replace(tzinfo=None)
|
|
323
|
+
except Exception:
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _learning_matches_change(row: sqlite3.Row, files: list[str], change_text: str, created_at: datetime | None) -> bool:
|
|
328
|
+
learning_text = " ".join(
|
|
329
|
+
str(row[key] or "")
|
|
330
|
+
for key in ("title", "content", "reasoning", "prevention")
|
|
331
|
+
if key in row.keys()
|
|
332
|
+
)
|
|
333
|
+
applies_to = str(row["applies_to"] or "").strip() if "applies_to" in row.keys() else ""
|
|
334
|
+
if files and applies_to:
|
|
335
|
+
applies_tokens = {item for item in _split_changed_files(applies_to)}
|
|
336
|
+
if any(file_path in applies_tokens or Path(file_path).name in applies_to for file_path in files):
|
|
337
|
+
return True
|
|
338
|
+
change_signature = _topic_signature(change_text)
|
|
339
|
+
learning_signature = _topic_signature(learning_text)
|
|
340
|
+
if change_signature and learning_signature and change_signature == learning_signature:
|
|
341
|
+
return True
|
|
342
|
+
if change_signature and change_signature in learning_text.lower():
|
|
343
|
+
return True
|
|
344
|
+
|
|
345
|
+
updated_at = _parse_mixed_datetime(row["updated_at"] if "updated_at" in row.keys() else None)
|
|
346
|
+
if created_at and updated_at:
|
|
347
|
+
delta = updated_at - created_at
|
|
348
|
+
if timedelta(hours=-1) <= delta <= timedelta(days=3):
|
|
349
|
+
return True
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _attempt_repair_learning_auto_capture(row: sqlite3.Row) -> dict:
|
|
354
|
+
try:
|
|
355
|
+
from tools_learnings import find_conflicting_active_learning, handle_learning_add
|
|
356
|
+
except Exception as exc:
|
|
357
|
+
return {"ok": False, "error": f"learning runtime unavailable: {exc}"}
|
|
358
|
+
|
|
359
|
+
files = _split_changed_files(str(row["files"] or ""))
|
|
360
|
+
title_seed = str(row["what_changed"] or row["why"] or "").strip() or f"Repair change #{row['id']}"
|
|
361
|
+
title = title_seed[:120]
|
|
362
|
+
content_parts = [
|
|
363
|
+
str(row["what_changed"] or "").strip(),
|
|
364
|
+
str(row["why"] or "").strip(),
|
|
365
|
+
]
|
|
366
|
+
if files:
|
|
367
|
+
content_parts.append(f"Affected files: {', '.join(files[:5])}")
|
|
368
|
+
content = " ".join(part for part in content_parts if part).strip()
|
|
369
|
+
if not content:
|
|
370
|
+
content = f"Repair-oriented change log entry #{row['id']} required a canonical learning."
|
|
371
|
+
applies_to = ",".join(files)
|
|
372
|
+
conflicting = find_conflicting_active_learning(
|
|
373
|
+
category="nexo-ops",
|
|
374
|
+
title=title,
|
|
375
|
+
content=content,
|
|
376
|
+
applies_to=applies_to,
|
|
377
|
+
)
|
|
378
|
+
supersedes_id = int(conflicting["id"]) if conflicting else 0
|
|
379
|
+
response = handle_learning_add(
|
|
380
|
+
category="nexo-ops",
|
|
381
|
+
title=title,
|
|
382
|
+
content=content,
|
|
383
|
+
reasoning=f"Auto-captured by daily self-audit from repair change #{row['id']}.",
|
|
384
|
+
prevention="Review the canonical repair learning before touching the affected file again." if applies_to else "",
|
|
385
|
+
applies_to=applies_to,
|
|
386
|
+
priority="high",
|
|
387
|
+
supersedes_id=supersedes_id,
|
|
388
|
+
)
|
|
389
|
+
match = re.search(r"Learning #(\d+)", response)
|
|
390
|
+
if match and "ERROR:" not in response:
|
|
391
|
+
return {
|
|
392
|
+
"ok": True,
|
|
393
|
+
"learning_id": int(match.group(1)),
|
|
394
|
+
"response": response,
|
|
395
|
+
}
|
|
396
|
+
return {"ok": False, "error": response}
|
|
397
|
+
|
|
398
|
+
|
|
88
399
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
89
400
|
# Stage A: Mechanical checks (UNCHANGED from v1 — all 18 checks)
|
|
90
401
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -253,6 +564,396 @@ def check_memory_reviews():
|
|
|
253
564
|
finding("INFO", "memory", f"{total} reviews due")
|
|
254
565
|
|
|
255
566
|
|
|
567
|
+
def check_learning_contradictions():
|
|
568
|
+
if not NEXO_DB.exists():
|
|
569
|
+
return
|
|
570
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
571
|
+
conn.row_factory = sqlite3.Row
|
|
572
|
+
if not _table_exists(conn, "learnings"):
|
|
573
|
+
conn.close()
|
|
574
|
+
return
|
|
575
|
+
|
|
576
|
+
from tools_learnings import _applies_overlap, _looks_contradictory
|
|
577
|
+
|
|
578
|
+
rows = conn.execute(
|
|
579
|
+
"""SELECT id, title, content, applies_to
|
|
580
|
+
FROM learnings
|
|
581
|
+
WHERE status = 'active' AND COALESCE(applies_to, '') != ''
|
|
582
|
+
ORDER BY updated_at DESC, id DESC
|
|
583
|
+
LIMIT 200"""
|
|
584
|
+
).fetchall()
|
|
585
|
+
contradictions: list[tuple[sqlite3.Row, sqlite3.Row]] = []
|
|
586
|
+
for index, left in enumerate(rows):
|
|
587
|
+
for right in rows[index + 1:]:
|
|
588
|
+
if not _applies_overlap(left["applies_to"], right["applies_to"]):
|
|
589
|
+
continue
|
|
590
|
+
if not _looks_contradictory(
|
|
591
|
+
f"{left['title']} {left['content']}",
|
|
592
|
+
f"{right['title']} {right['content']}",
|
|
593
|
+
):
|
|
594
|
+
continue
|
|
595
|
+
contradictions.append((left, right))
|
|
596
|
+
|
|
597
|
+
if contradictions:
|
|
598
|
+
finding("ERROR", "contradictions", f"{len(contradictions)} contradictory active learning pair(s)")
|
|
599
|
+
for left, right in contradictions[:5]:
|
|
600
|
+
description = (
|
|
601
|
+
f"Resolve contradictory active learnings #{left['id']} and #{right['id']} "
|
|
602
|
+
f"for {left['applies_to'] or right['applies_to']}"
|
|
603
|
+
)
|
|
604
|
+
reasoning = (
|
|
605
|
+
"Daily self-audit found two active canonical rules that contradict each other. "
|
|
606
|
+
"One rule must be superseded or reconciled before the next edit repeats the error."
|
|
607
|
+
)
|
|
608
|
+
_ensure_followup(
|
|
609
|
+
conn,
|
|
610
|
+
prefix="CONTRADICTION",
|
|
611
|
+
description=description,
|
|
612
|
+
verification="One canonical learning remains active and the conflicting rule is superseded or archived",
|
|
613
|
+
reasoning=reasoning,
|
|
614
|
+
priority="critical",
|
|
615
|
+
)
|
|
616
|
+
conn.commit()
|
|
617
|
+
conn.close()
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def check_error_memory_loop():
|
|
621
|
+
if not NEXO_DB.exists():
|
|
622
|
+
return
|
|
623
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
624
|
+
conn.row_factory = sqlite3.Row
|
|
625
|
+
if not _table_exists(conn, "protocol_tasks"):
|
|
626
|
+
conn.close()
|
|
627
|
+
return
|
|
628
|
+
|
|
629
|
+
rows = conn.execute(
|
|
630
|
+
"""SELECT task_id, goal, area, files, status, learning_id
|
|
631
|
+
FROM protocol_tasks
|
|
632
|
+
WHERE status IN ('failed', 'blocked')
|
|
633
|
+
AND (learning_id IS NULL OR learning_id = 0)
|
|
634
|
+
AND opened_at >= datetime('now', '-30 days')
|
|
635
|
+
ORDER BY opened_at DESC"""
|
|
636
|
+
).fetchall()
|
|
637
|
+
|
|
638
|
+
grouped: dict[str, list[sqlite3.Row]] = {}
|
|
639
|
+
for row in rows:
|
|
640
|
+
files = str(row["files"] or "").strip()
|
|
641
|
+
signature = files if files and files != "[]" else (row["area"] or row["goal"] or "general")
|
|
642
|
+
grouped.setdefault(signature[:220], []).append(row)
|
|
643
|
+
|
|
644
|
+
repeated = {signature: items for signature, items in grouped.items() if len(items) >= 2}
|
|
645
|
+
if repeated:
|
|
646
|
+
finding("WARN", "prevention", f"{len(repeated)} repeated failure cluster(s) still lack canonical prevention learnings")
|
|
647
|
+
for signature, items in list(repeated.items())[:5]:
|
|
648
|
+
description = (
|
|
649
|
+
f"Mine a canonical prevention learning from repeated failed/blocked protocol tasks around {signature}"
|
|
650
|
+
)
|
|
651
|
+
reasoning = (
|
|
652
|
+
f"Daily self-audit found {len(items)} failed/blocked protocol tasks without a linked learning. "
|
|
653
|
+
"Turn the repeated failure into a prevention rule before it repeats again."
|
|
654
|
+
)
|
|
655
|
+
_ensure_followup(
|
|
656
|
+
conn,
|
|
657
|
+
prefix="PREVENTION",
|
|
658
|
+
description=description,
|
|
659
|
+
verification="Canonical prevention learning captured and linked to the repeated failure pattern",
|
|
660
|
+
reasoning=reasoning,
|
|
661
|
+
priority="high",
|
|
662
|
+
)
|
|
663
|
+
conn.commit()
|
|
664
|
+
conn.close()
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def check_repair_changes_missing_learning_capture():
|
|
668
|
+
if not NEXO_DB.exists():
|
|
669
|
+
return
|
|
670
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
671
|
+
conn.row_factory = sqlite3.Row
|
|
672
|
+
if not _table_exists(conn, "change_log") or not _table_exists(conn, "learnings"):
|
|
673
|
+
conn.close()
|
|
674
|
+
return
|
|
675
|
+
|
|
676
|
+
learning_rows = conn.execute(
|
|
677
|
+
"""SELECT *
|
|
678
|
+
FROM learnings
|
|
679
|
+
WHERE COALESCE(status, 'active') != 'deleted'
|
|
680
|
+
ORDER BY updated_at DESC, created_at DESC
|
|
681
|
+
LIMIT 300"""
|
|
682
|
+
).fetchall()
|
|
683
|
+
if not learning_rows:
|
|
684
|
+
learning_rows = []
|
|
685
|
+
|
|
686
|
+
rows = conn.execute(
|
|
687
|
+
"""SELECT id, files, what_changed, why, created_at
|
|
688
|
+
FROM change_log
|
|
689
|
+
WHERE created_at >= datetime('now', '-14 days')
|
|
690
|
+
ORDER BY created_at DESC
|
|
691
|
+
LIMIT 200"""
|
|
692
|
+
).fetchall()
|
|
693
|
+
missing: list[sqlite3.Row] = []
|
|
694
|
+
for row in rows:
|
|
695
|
+
change_text = f"{row['what_changed'] or ''} {row['why'] or ''}".strip()
|
|
696
|
+
if not _looks_like_repair_change(change_text):
|
|
697
|
+
continue
|
|
698
|
+
files = _split_changed_files(str(row["files"] or ""))
|
|
699
|
+
created_at = _parse_mixed_datetime(row["created_at"])
|
|
700
|
+
if any(_learning_matches_change(learning, files, change_text, created_at) for learning in learning_rows):
|
|
701
|
+
continue
|
|
702
|
+
missing.append(row)
|
|
703
|
+
|
|
704
|
+
if missing:
|
|
705
|
+
auto_captured = 0
|
|
706
|
+
unresolved: list[sqlite3.Row] = []
|
|
707
|
+
for row in missing:
|
|
708
|
+
captured = _attempt_repair_learning_auto_capture(row)
|
|
709
|
+
if captured.get("ok"):
|
|
710
|
+
auto_captured += 1
|
|
711
|
+
continue
|
|
712
|
+
unresolved.append(row)
|
|
713
|
+
|
|
714
|
+
if unresolved:
|
|
715
|
+
finding(
|
|
716
|
+
"WARN",
|
|
717
|
+
"learning-capture",
|
|
718
|
+
f"{len(unresolved)} repair/logged fix change(s) still lack linked learnings "
|
|
719
|
+
f"after {auto_captured} self-audit auto-capture(s)",
|
|
720
|
+
)
|
|
721
|
+
else:
|
|
722
|
+
finding(
|
|
723
|
+
"INFO",
|
|
724
|
+
"learning-capture",
|
|
725
|
+
f"Self-audit auto-captured {auto_captured} missing repair learning(s)",
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
for row in unresolved[:5]:
|
|
729
|
+
files = _split_changed_files(str(row["files"] or ""))
|
|
730
|
+
target = files[0] if files else str(row["what_changed"] or "recent repair")[:120]
|
|
731
|
+
evidence = (
|
|
732
|
+
f"Repair-oriented change log entry #{row['id']} on {target} has no nearby linked learning capture."
|
|
733
|
+
)
|
|
734
|
+
_ensure_protocol_debt(
|
|
735
|
+
conn,
|
|
736
|
+
debt_type="repair_change_without_learning_capture",
|
|
737
|
+
severity="warn",
|
|
738
|
+
evidence=evidence,
|
|
739
|
+
)
|
|
740
|
+
_ensure_followup(
|
|
741
|
+
conn,
|
|
742
|
+
prefix="LEARNCAP",
|
|
743
|
+
description=f"Capture canonical learning for repair change touching {target}",
|
|
744
|
+
verification="A learning exists with applies_to/topic linked to the repair change",
|
|
745
|
+
reasoning="Daily self-audit found a repair/fix change log entry with no durable learning attached.",
|
|
746
|
+
priority="high",
|
|
747
|
+
)
|
|
748
|
+
conn.commit()
|
|
749
|
+
conn.close()
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def check_unformalized_mentions():
|
|
753
|
+
if not NEXO_DB.exists():
|
|
754
|
+
return
|
|
755
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
756
|
+
conn.row_factory = sqlite3.Row
|
|
757
|
+
if not _table_exists(conn, "protocol_tasks"):
|
|
758
|
+
conn.close()
|
|
759
|
+
return
|
|
760
|
+
|
|
761
|
+
rows = conn.execute(
|
|
762
|
+
"""SELECT goal, area, learning_id, followup_id
|
|
763
|
+
FROM protocol_tasks
|
|
764
|
+
WHERE opened_at >= datetime('now', '-30 days')
|
|
765
|
+
AND COALESCE(goal, '') != ''
|
|
766
|
+
ORDER BY opened_at DESC"""
|
|
767
|
+
).fetchall()
|
|
768
|
+
if not rows:
|
|
769
|
+
conn.close()
|
|
770
|
+
return
|
|
771
|
+
|
|
772
|
+
formalized_topics: set[str] = set()
|
|
773
|
+
if _table_exists(conn, "workflow_goals"):
|
|
774
|
+
goal_rows = conn.execute(
|
|
775
|
+
"""SELECT title, objective
|
|
776
|
+
FROM workflow_goals
|
|
777
|
+
WHERE status NOT IN ('abandoned', 'cancelled')"""
|
|
778
|
+
).fetchall()
|
|
779
|
+
for row in goal_rows:
|
|
780
|
+
for candidate in (row["title"], row["objective"]):
|
|
781
|
+
signature = _topic_signature(str(candidate or ""))
|
|
782
|
+
if signature:
|
|
783
|
+
formalized_topics.add(signature)
|
|
784
|
+
|
|
785
|
+
repeated: dict[tuple[str, str], list[sqlite3.Row]] = {}
|
|
786
|
+
for row in rows:
|
|
787
|
+
if row["learning_id"] or str(row["followup_id"] or "").strip():
|
|
788
|
+
continue
|
|
789
|
+
signature = _topic_signature(str(row["goal"] or ""))
|
|
790
|
+
if not signature or signature in formalized_topics:
|
|
791
|
+
continue
|
|
792
|
+
area = str(row["area"] or "general").strip() or "general"
|
|
793
|
+
repeated.setdefault((area, signature), []).append(row)
|
|
794
|
+
|
|
795
|
+
loose_topics = {
|
|
796
|
+
key: items
|
|
797
|
+
for key, items in repeated.items()
|
|
798
|
+
if len(items) >= 2
|
|
799
|
+
}
|
|
800
|
+
if loose_topics:
|
|
801
|
+
finding("WARN", "formalization", f"{len(loose_topics)} repeated topic(s) keep being mentioned without durable formalization")
|
|
802
|
+
for (area, signature), items in list(loose_topics.items())[:5]:
|
|
803
|
+
sample_goal = str(items[0]["goal"] or "").strip()[:120]
|
|
804
|
+
description = (
|
|
805
|
+
f"Formalize repeated unresolved theme in {area}: '{sample_goal}' "
|
|
806
|
+
f"appears {len(items)} times without a durable goal, followup, or learning."
|
|
807
|
+
)
|
|
808
|
+
reasoning = (
|
|
809
|
+
"Daily self-audit found the same theme recurring across protocol tasks without being "
|
|
810
|
+
"converted into a workflow goal, followup, or learning. Formalize it before it keeps resurfacing."
|
|
811
|
+
)
|
|
812
|
+
_ensure_followup(
|
|
813
|
+
conn,
|
|
814
|
+
prefix="FORMALIZE",
|
|
815
|
+
description=description,
|
|
816
|
+
verification="Theme converted into a durable goal, followup, or canonical learning",
|
|
817
|
+
reasoning=reasoning,
|
|
818
|
+
priority="high",
|
|
819
|
+
)
|
|
820
|
+
conn.commit()
|
|
821
|
+
conn.close()
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def check_automation_opportunities():
|
|
825
|
+
if not NEXO_DB.exists():
|
|
826
|
+
return
|
|
827
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
828
|
+
conn.row_factory = sqlite3.Row
|
|
829
|
+
if not _table_exists(conn, "protocol_tasks"):
|
|
830
|
+
conn.close()
|
|
831
|
+
return
|
|
832
|
+
|
|
833
|
+
rows = conn.execute(
|
|
834
|
+
"""SELECT goal, area, files
|
|
835
|
+
FROM protocol_tasks
|
|
836
|
+
WHERE status = 'done'
|
|
837
|
+
AND closed_at >= datetime('now', '-30 days')
|
|
838
|
+
ORDER BY closed_at DESC"""
|
|
839
|
+
).fetchall()
|
|
840
|
+
if not rows:
|
|
841
|
+
conn.close()
|
|
842
|
+
return
|
|
843
|
+
|
|
844
|
+
grouped: dict[tuple[str, str], list[sqlite3.Row]] = {}
|
|
845
|
+
for row in rows:
|
|
846
|
+
signature = str(row["files"] or "").strip() or _topic_signature(str(row["goal"] or ""))
|
|
847
|
+
if not signature:
|
|
848
|
+
continue
|
|
849
|
+
area = str(row["area"] or "general").strip() or "general"
|
|
850
|
+
grouped.setdefault((area, signature[:220]), []).append(row)
|
|
851
|
+
|
|
852
|
+
repeated = {
|
|
853
|
+
key: items
|
|
854
|
+
for key, items in grouped.items()
|
|
855
|
+
if len(items) >= 3
|
|
856
|
+
}
|
|
857
|
+
if repeated:
|
|
858
|
+
finding("INFO", "opportunities", f"{len(repeated)} repeated manual pattern(s) are good candidates for skills/scripts")
|
|
859
|
+
for (area, signature), items in list(repeated.items())[:5]:
|
|
860
|
+
sample_goal = str(items[0]["goal"] or "").strip()[:120]
|
|
861
|
+
description = (
|
|
862
|
+
f"Extract a reusable automation for repeated {area} work around '{sample_goal}' "
|
|
863
|
+
f"(seen {len(items)} successful protocol tasks in 30 days)."
|
|
864
|
+
)
|
|
865
|
+
reasoning = (
|
|
866
|
+
"Daily self-audit found repeated successful manual work. Convert it into a skill, script, "
|
|
867
|
+
"or reusable workflow before it keeps consuming operator time."
|
|
868
|
+
)
|
|
869
|
+
_ensure_followup(
|
|
870
|
+
conn,
|
|
871
|
+
prefix="OPPORTUNITY",
|
|
872
|
+
description=description,
|
|
873
|
+
verification="A reusable skill, script, or workflow now covers the repeated manual pattern",
|
|
874
|
+
reasoning=reasoning,
|
|
875
|
+
priority="medium",
|
|
876
|
+
)
|
|
877
|
+
conn.commit()
|
|
878
|
+
conn.close()
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def check_state_watchers():
|
|
882
|
+
try:
|
|
883
|
+
import importlib
|
|
884
|
+
import db as _db
|
|
885
|
+
import state_watchers_runtime as _state_watchers_runtime
|
|
886
|
+
except Exception as exc:
|
|
887
|
+
finding("WARN", "watchers", f"state watchers runtime unavailable: {exc}")
|
|
888
|
+
return
|
|
889
|
+
importlib.reload(_db)
|
|
890
|
+
runtime = importlib.reload(_state_watchers_runtime)
|
|
891
|
+
summary = runtime.run_state_watchers(persist=True)
|
|
892
|
+
counts = summary.get("counts") or {}
|
|
893
|
+
if int(counts.get("critical") or 0) > 0:
|
|
894
|
+
finding("ERROR", "watchers", f"{counts.get('critical')} critical state watcher(s)")
|
|
895
|
+
elif int(counts.get("degraded") or 0) > 0:
|
|
896
|
+
finding("WARN", "watchers", f"{counts.get('degraded')} degraded state watcher(s)")
|
|
897
|
+
elif int(summary.get("watcher_count") or 0) > 0:
|
|
898
|
+
finding("INFO", "watchers", f"{summary.get('watcher_count')} state watcher(s) healthy")
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def check_memory_quality_scores():
|
|
902
|
+
if not NEXO_DB.exists():
|
|
903
|
+
return
|
|
904
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
905
|
+
conn.row_factory = sqlite3.Row
|
|
906
|
+
if not _table_exists(conn, "learnings"):
|
|
907
|
+
conn.close()
|
|
908
|
+
return
|
|
909
|
+
try:
|
|
910
|
+
from tools_learnings import score_learning_quality
|
|
911
|
+
except Exception:
|
|
912
|
+
conn.close()
|
|
913
|
+
return
|
|
914
|
+
|
|
915
|
+
rows = conn.execute(
|
|
916
|
+
"""SELECT *
|
|
917
|
+
FROM learnings
|
|
918
|
+
WHERE status = 'active'
|
|
919
|
+
ORDER BY updated_at DESC, id DESC
|
|
920
|
+
LIMIT 200"""
|
|
921
|
+
).fetchall()
|
|
922
|
+
if not rows:
|
|
923
|
+
conn.close()
|
|
924
|
+
return
|
|
925
|
+
|
|
926
|
+
normalized = [dict(row) for row in rows]
|
|
927
|
+
scored = [(row, score_learning_quality(row, conn)) for row in normalized]
|
|
928
|
+
weak = [(row, quality) for row, quality in scored if quality["score"] < 60]
|
|
929
|
+
fragile_conditioned = [
|
|
930
|
+
(row, quality)
|
|
931
|
+
for row, quality in weak
|
|
932
|
+
if str(row.get("applies_to") or "").strip()
|
|
933
|
+
]
|
|
934
|
+
if weak:
|
|
935
|
+
finding("WARN", "memory-quality", f"{len(weak)} active learning(s) have low quality scores")
|
|
936
|
+
if fragile_conditioned:
|
|
937
|
+
sample = fragile_conditioned[0][0]
|
|
938
|
+
description = (
|
|
939
|
+
f"Refresh low-quality conditioned learnings; first weak rule is #{sample['id']} "
|
|
940
|
+
f"for {sample['applies_to']}"
|
|
941
|
+
)
|
|
942
|
+
else:
|
|
943
|
+
sample = weak[0][0]
|
|
944
|
+
description = f"Refresh low-quality learnings; first weak rule is #{sample['id']} {sample['title']}"
|
|
945
|
+
_ensure_followup(
|
|
946
|
+
conn,
|
|
947
|
+
prefix="MEMQ",
|
|
948
|
+
description=description,
|
|
949
|
+
verification="Weak active learnings refreshed with stronger reasoning/prevention/applies_to coverage",
|
|
950
|
+
reasoning="Daily self-audit found active learnings with weak quality scores that may mislead retrieval or guard.",
|
|
951
|
+
priority="high" if fragile_conditioned else "medium",
|
|
952
|
+
)
|
|
953
|
+
conn.commit()
|
|
954
|
+
conn.close()
|
|
955
|
+
|
|
956
|
+
|
|
256
957
|
def _sha256(path):
|
|
257
958
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
258
959
|
|
|
@@ -435,6 +1136,131 @@ def check_cognitive_health():
|
|
|
435
1136
|
finding("WARN", "cognitive", f"Weekly GC failed: {e}")
|
|
436
1137
|
|
|
437
1138
|
|
|
1139
|
+
def check_codex_conditioned_file_discipline():
|
|
1140
|
+
try:
|
|
1141
|
+
from doctor.providers.runtime import _recent_codex_conditioned_file_discipline_status
|
|
1142
|
+
except Exception as e:
|
|
1143
|
+
finding("WARN", "codex-discipline", f"Codex discipline audit unavailable: {e}")
|
|
1144
|
+
return
|
|
1145
|
+
|
|
1146
|
+
audit = _recent_codex_conditioned_file_discipline_status()
|
|
1147
|
+
if not audit.get("conditioned_rules"):
|
|
1148
|
+
return
|
|
1149
|
+
|
|
1150
|
+
read_violations = int(audit.get("read_without_protocol") or 0)
|
|
1151
|
+
write_without_protocol = int(audit.get("write_without_protocol") or 0)
|
|
1152
|
+
write_without_guard_ack = int(audit.get("write_without_guard_ack") or 0)
|
|
1153
|
+
delete_without_protocol = int(audit.get("delete_without_protocol") or 0)
|
|
1154
|
+
delete_without_guard_ack = int(audit.get("delete_without_guard_ack") or 0)
|
|
1155
|
+
total_violations = (
|
|
1156
|
+
read_violations
|
|
1157
|
+
+ write_without_protocol
|
|
1158
|
+
+ write_without_guard_ack
|
|
1159
|
+
+ delete_without_protocol
|
|
1160
|
+
+ delete_without_guard_ack
|
|
1161
|
+
)
|
|
1162
|
+
if total_violations <= 0:
|
|
1163
|
+
return
|
|
1164
|
+
|
|
1165
|
+
created_debts = 0
|
|
1166
|
+
if NEXO_DB.exists():
|
|
1167
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
1168
|
+
if _protocol_debt_table_exists(conn):
|
|
1169
|
+
debt_type_map = {
|
|
1170
|
+
"read_without_protocol": ("codex_conditioned_read_without_protocol", "warn"),
|
|
1171
|
+
"write_without_protocol": ("codex_conditioned_write_without_protocol", "error"),
|
|
1172
|
+
"write_without_guard_ack": ("codex_conditioned_write_without_guard_ack", "error"),
|
|
1173
|
+
"delete_without_protocol": ("codex_conditioned_delete_without_protocol", "error"),
|
|
1174
|
+
"delete_without_guard_ack": ("codex_conditioned_delete_without_guard_ack", "error"),
|
|
1175
|
+
}
|
|
1176
|
+
for sample in audit.get("samples", []):
|
|
1177
|
+
debt_info = debt_type_map.get(sample.get("kind"))
|
|
1178
|
+
if not debt_info:
|
|
1179
|
+
continue
|
|
1180
|
+
debt_type, severity = debt_info
|
|
1181
|
+
evidence = (
|
|
1182
|
+
"Codex conditioned-file transcript audit: "
|
|
1183
|
+
f"{sample.get('kind')} {sample.get('file')} via {sample.get('tool')} "
|
|
1184
|
+
f"in {sample.get('session_file')}"
|
|
1185
|
+
)
|
|
1186
|
+
if _ensure_protocol_debt(conn, debt_type=debt_type, severity=severity, evidence=evidence):
|
|
1187
|
+
created_debts += 1
|
|
1188
|
+
conn.commit()
|
|
1189
|
+
conn.close()
|
|
1190
|
+
|
|
1191
|
+
severity = "ERROR" if (write_without_protocol or write_without_guard_ack) else "WARN"
|
|
1192
|
+
message = (
|
|
1193
|
+
"Codex conditioned-file discipline drift: "
|
|
1194
|
+
f"{read_violations} read(s) without protocol/guard, "
|
|
1195
|
+
f"{write_without_protocol} write(s) without protocol, "
|
|
1196
|
+
f"{write_without_guard_ack} write(s) without guard ack, "
|
|
1197
|
+
f"{delete_without_protocol} delete(s) without protocol, "
|
|
1198
|
+
f"{delete_without_guard_ack} delete(s) without guard ack"
|
|
1199
|
+
)
|
|
1200
|
+
if created_debts:
|
|
1201
|
+
message += f" | opened {created_debts} protocol debt item(s)"
|
|
1202
|
+
finding(severity, "codex-discipline", message)
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def check_codex_startup_discipline():
|
|
1206
|
+
try:
|
|
1207
|
+
from doctor.providers.runtime import _recent_codex_session_parity_status
|
|
1208
|
+
except Exception as e:
|
|
1209
|
+
finding("WARN", "codex-startup", f"Codex startup audit unavailable: {e}")
|
|
1210
|
+
return
|
|
1211
|
+
|
|
1212
|
+
audit = _recent_codex_session_parity_status()
|
|
1213
|
+
if not audit.get("files"):
|
|
1214
|
+
return
|
|
1215
|
+
|
|
1216
|
+
samples = audit.get("samples", [])
|
|
1217
|
+
missing_startup = [sample for sample in samples if not sample.get("startup")]
|
|
1218
|
+
missing_heartbeat = [sample for sample in samples if sample.get("startup") and not sample.get("heartbeat")]
|
|
1219
|
+
missing_bootstrap = [
|
|
1220
|
+
sample for sample in samples
|
|
1221
|
+
if sample.get("startup") and sample.get("heartbeat") and not sample.get("bootstrap")
|
|
1222
|
+
]
|
|
1223
|
+
if not missing_startup and not missing_heartbeat and not missing_bootstrap:
|
|
1224
|
+
return
|
|
1225
|
+
|
|
1226
|
+
created_debts = 0
|
|
1227
|
+
if NEXO_DB.exists():
|
|
1228
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
1229
|
+
if _protocol_debt_table_exists(conn):
|
|
1230
|
+
for sample in samples:
|
|
1231
|
+
debt_type = ""
|
|
1232
|
+
severity = "warn"
|
|
1233
|
+
if not sample.get("startup"):
|
|
1234
|
+
debt_type = "codex_session_missing_startup"
|
|
1235
|
+
severity = "error"
|
|
1236
|
+
elif not sample.get("heartbeat"):
|
|
1237
|
+
debt_type = "codex_session_missing_heartbeat"
|
|
1238
|
+
elif not sample.get("bootstrap"):
|
|
1239
|
+
debt_type = "codex_session_missing_bootstrap"
|
|
1240
|
+
if not debt_type:
|
|
1241
|
+
continue
|
|
1242
|
+
evidence = (
|
|
1243
|
+
"Codex session parity audit: "
|
|
1244
|
+
f"{debt_type} in {sample.get('file')} "
|
|
1245
|
+
f"(origin={sample.get('origin') or 'unknown'})"
|
|
1246
|
+
)
|
|
1247
|
+
if _ensure_protocol_debt(conn, debt_type=debt_type, severity=severity, evidence=evidence):
|
|
1248
|
+
created_debts += 1
|
|
1249
|
+
conn.commit()
|
|
1250
|
+
conn.close()
|
|
1251
|
+
|
|
1252
|
+
severity = "ERROR" if missing_startup else "WARN"
|
|
1253
|
+
message = (
|
|
1254
|
+
"Codex startup discipline drift: "
|
|
1255
|
+
f"{len(missing_bootstrap)} session(s) missing bootstrap marker, "
|
|
1256
|
+
f"{len(missing_startup)} missing startup, "
|
|
1257
|
+
f"{len(missing_heartbeat)} missing heartbeat"
|
|
1258
|
+
)
|
|
1259
|
+
if created_debts:
|
|
1260
|
+
message += f" | opened {created_debts} protocol debt item(s)"
|
|
1261
|
+
finding(severity, "codex-startup", message)
|
|
1262
|
+
|
|
1263
|
+
|
|
438
1264
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
439
1265
|
# Stage B: Interpretation (automation backend) — NEW in v2
|
|
440
1266
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -533,6 +1359,15 @@ def main():
|
|
|
533
1359
|
check_repetition_rate()
|
|
534
1360
|
check_unused_learnings()
|
|
535
1361
|
check_memory_reviews()
|
|
1362
|
+
check_learning_contradictions()
|
|
1363
|
+
check_error_memory_loop()
|
|
1364
|
+
check_repair_changes_missing_learning_capture()
|
|
1365
|
+
check_unformalized_mentions()
|
|
1366
|
+
check_automation_opportunities()
|
|
1367
|
+
check_state_watchers()
|
|
1368
|
+
check_memory_quality_scores()
|
|
1369
|
+
check_codex_startup_discipline()
|
|
1370
|
+
check_codex_conditioned_file_discipline()
|
|
536
1371
|
check_watchdog_registry()
|
|
537
1372
|
check_snapshot_sync()
|
|
538
1373
|
check_restore_activity()
|
|
@@ -547,13 +1382,16 @@ def main():
|
|
|
547
1382
|
infos = sum(1 for f in findings if f["severity"] == "INFO")
|
|
548
1383
|
log(f"Stage A complete: {errors} errors, {warns} warnings, {infos} info")
|
|
549
1384
|
|
|
550
|
-
# Write raw summary (backward compatible)
|
|
551
|
-
|
|
552
|
-
summary_file.write_text(json.dumps({
|
|
1385
|
+
# Write raw summary (backward compatible) + horizon rollups
|
|
1386
|
+
summary_payload = {
|
|
553
1387
|
"timestamp": datetime.now().isoformat(),
|
|
554
1388
|
"findings": findings,
|
|
555
|
-
"counts": {"error": errors, "warn": warns, "info": infos}
|
|
556
|
-
|
|
1389
|
+
"counts": {"error": errors, "warn": warns, "info": infos},
|
|
1390
|
+
"date_label": datetime.now().strftime("%Y-%m-%d"),
|
|
1391
|
+
}
|
|
1392
|
+
summary_file = LOG_DIR / "self-audit-summary.json"
|
|
1393
|
+
summary_file.write_text(json.dumps(summary_payload, indent=2))
|
|
1394
|
+
write_horizon_summaries(summary_payload)
|
|
557
1395
|
|
|
558
1396
|
# Stage B: CLI interpretation (graceful fallback if CLI unavailable)
|
|
559
1397
|
cli_ok = interpret_findings(findings)
|