nexo-brain 2.7.0 → 3.0.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 (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +66 -12
  3. package/hooks/hooks.json +79 -0
  4. package/package.json +1 -1
  5. package/src/agent_runner.py +290 -6
  6. package/src/cli.py +111 -0
  7. package/src/client_preferences.py +94 -0
  8. package/src/client_sync.py +202 -2
  9. package/src/cognitive/__init__.py +1 -1
  10. package/src/cognitive/_search.py +39 -19
  11. package/src/dashboard/app.py +140 -0
  12. package/src/dashboard/templates/base.html +4 -0
  13. package/src/dashboard/templates/protocol.html +199 -0
  14. package/src/db/__init__.py +23 -1
  15. package/src/db/_learnings.py +31 -4
  16. package/src/db/_personal_scripts.py +12 -0
  17. package/src/db/_protocol.py +303 -0
  18. package/src/db/_schema.py +248 -0
  19. package/src/db/_watchers.py +173 -0
  20. package/src/db/_workflow.py +952 -0
  21. package/src/doctor/providers/runtime.py +918 -7
  22. package/src/evolution_cycle.py +62 -0
  23. package/src/hook_guardrails.py +308 -0
  24. package/src/hooks/protocol-guardrail.sh +10 -0
  25. package/src/nexo_sdk.py +103 -0
  26. package/src/plugins/cognitive_memory.py +18 -0
  27. package/src/plugins/cortex.py +55 -35
  28. package/src/plugins/guard.py +132 -16
  29. package/src/plugins/protocol.py +911 -0
  30. package/src/plugins/schedule.py +40 -6
  31. package/src/plugins/simple_api.py +103 -0
  32. package/src/plugins/skills.py +67 -0
  33. package/src/plugins/state_watchers.py +79 -0
  34. package/src/plugins/workflow.py +588 -0
  35. package/src/public_contribution.py +86 -12
  36. package/src/script_registry.py +142 -0
  37. package/src/scripts/deep-sleep/apply_findings.py +204 -0
  38. package/src/scripts/deep-sleep/collect.py +49 -4
  39. package/src/scripts/nexo-agent-run.py +2 -0
  40. package/src/scripts/nexo-daily-self-audit.py +843 -5
  41. package/src/scripts/nexo-evolution-run.py +343 -1
  42. package/src/server.py +92 -6
  43. package/src/skills_runtime.py +151 -0
  44. package/src/state_watchers_runtime.py +334 -0
  45. package/src/tools_learnings.py +345 -7
  46. package/src/tools_sessions.py +183 -0
  47. package/templates/CLAUDE.md.template +9 -1
  48. package/templates/CODEX.AGENTS.md.template +10 -2
@@ -240,6 +240,12 @@ def _summarize_engineering_loop(weekly: dict, monthly: dict) -> dict:
240
240
  protocol_delta = trend.get("protocol_compliance_delta")
241
241
  if isinstance(protocol_delta, (int, float)) and protocol_delta > 0:
242
242
  improving.append({"title": "Protocol", "detail": f"{protocol_delta:+.1f}%", "tone": "healthy", "meta": "vs previous window"})
243
+ duplicate_followup_delta = trend.get("followup_duplicate_open_delta")
244
+ if isinstance(duplicate_followup_delta, int) and duplicate_followup_delta < 0:
245
+ improving.append({"title": "Followup duplication", "detail": f"{duplicate_followup_delta:+d}", "tone": "healthy", "meta": "open duplicates"})
246
+ learning_noise_delta = trend.get("learning_noise_delta")
247
+ if isinstance(learning_noise_delta, int) and learning_noise_delta < 0:
248
+ improving.append({"title": "Learning noise", "detail": f"{learning_noise_delta:+d}", "tone": "healthy", "meta": "active noise pressure"})
243
249
  corrections_delta = trend.get("total_corrections_delta")
244
250
  if isinstance(corrections_delta, int) and corrections_delta < 0:
245
251
  improving.append({"title": "Corrections", "detail": f"{corrections_delta:+d}", "tone": "healthy", "meta": "lower is better"})
@@ -247,6 +253,13 @@ def _summarize_engineering_loop(weekly: dict, monthly: dict) -> dict:
247
253
  if isinstance(mood_delta, (int, float)) and mood_delta > 0:
248
254
  improving.append({"title": "Mood", "detail": f"{mood_delta:+.3f}", "tone": "healthy", "meta": "vs previous window"})
249
255
 
256
+ duplicate_followup_rate_delta = trend.get("followup_duplicate_rate_delta")
257
+ if isinstance(duplicate_followup_rate_delta, (int, float)) and duplicate_followup_rate_delta > 0:
258
+ drifting.append({"title": "followup_duplicates", "detail": f"{duplicate_followup_rate_delta:+.1f}%", "tone": "critical" if duplicate_followup_rate_delta >= 5 else "watch", "meta": "open duplicate rate"})
259
+ learning_noise_rate_delta = trend.get("learning_noise_rate_delta")
260
+ if isinstance(learning_noise_rate_delta, (int, float)) and learning_noise_rate_delta > 0:
261
+ drifting.append({"title": "learning_noise", "detail": f"{learning_noise_rate_delta:+.1f}%", "tone": "critical" if learning_noise_rate_delta >= 5 else "watch", "meta": "active noise rate"})
262
+
250
263
  return {
251
264
  "weekly": weekly,
252
265
  "monthly": monthly,
@@ -256,6 +269,120 @@ def _summarize_engineering_loop(weekly: dict, monthly: dict) -> dict:
256
269
  }
257
270
 
258
271
 
272
+ def _safe_json(value, default):
273
+ if value in (None, ""):
274
+ return default
275
+ if isinstance(value, (list, dict)):
276
+ return value
277
+ try:
278
+ return json.loads(value)
279
+ except Exception:
280
+ return default
281
+
282
+
283
+ def _protocol_explainability_snapshot(limit: int = 20) -> dict:
284
+ db = _db()
285
+ conn = db.get_db()
286
+ max_limit = max(5, min(int(limit or 20), 100))
287
+
288
+ protocol_summary = db.protocol_compliance_summary(7)
289
+ recent_tasks = []
290
+ for row in conn.execute(
291
+ """SELECT * FROM protocol_tasks
292
+ ORDER BY opened_at DESC
293
+ LIMIT ?""",
294
+ (max_limit,),
295
+ ).fetchall():
296
+ item = dict(row)
297
+ for field in (
298
+ "files",
299
+ "plan",
300
+ "known_facts",
301
+ "unknowns",
302
+ "constraints",
303
+ "evidence_refs",
304
+ "response_reasons",
305
+ ):
306
+ item[field] = _safe_json(item.get(field), [])
307
+ item["has_evidence"] = bool(str(item.get("close_evidence") or "").strip())
308
+ item["guarded_open"] = bool(item.get("opened_with_guard") or item.get("opened_with_rules"))
309
+ recent_tasks.append(item)
310
+
311
+ recent_debts = [dict(row) for row in conn.execute(
312
+ """SELECT * FROM protocol_debt
313
+ ORDER BY created_at DESC
314
+ LIMIT ?""",
315
+ (max_limit,),
316
+ ).fetchall()]
317
+
318
+ debt_summary = {"open_total": 0, "by_severity": {}, "by_type": {}}
319
+ for debt in recent_debts:
320
+ if debt.get("status") != "open":
321
+ continue
322
+ debt_summary["open_total"] += 1
323
+ severity = str(debt.get("severity") or "warn")
324
+ debt_type = str(debt.get("debt_type") or "unknown")
325
+ debt_summary["by_severity"][severity] = debt_summary["by_severity"].get(severity, 0) + 1
326
+ debt_summary["by_type"][debt_type] = debt_summary["by_type"].get(debt_type, 0) + 1
327
+
328
+ recent_runs = db.list_workflow_runs(include_closed=True, limit=max_limit)
329
+ workflow_summary = {
330
+ "total": len(recent_runs),
331
+ "open_runs": sum(1 for run in recent_runs if run.get("status") not in {"completed", "failed", "cancelled"}),
332
+ "blocked_runs": sum(1 for run in recent_runs if run.get("status") == "blocked"),
333
+ "waiting_approval": sum(1 for run in recent_runs if run.get("status") == "waiting_approval"),
334
+ }
335
+
336
+ recent_goals = db.list_workflow_goals(include_closed=True, limit=max_limit)
337
+ goal_summary = {
338
+ "total": len(recent_goals),
339
+ "active": sum(1 for goal in recent_goals if goal.get("status") == "active"),
340
+ "blocked": sum(1 for goal in recent_goals if goal.get("status") == "blocked"),
341
+ "closed": sum(1 for goal in recent_goals if goal.get("status") in {"completed", "cancelled", "abandoned"}),
342
+ }
343
+
344
+ guard_checks = [dict(row) for row in conn.execute(
345
+ """SELECT area, files, learnings_returned, blocking_rules_returned, created_at
346
+ FROM guard_checks
347
+ ORDER BY created_at DESC
348
+ LIMIT ?""",
349
+ (max_limit,),
350
+ ).fetchall()]
351
+ areas = {}
352
+ blocking_hits = 0
353
+ for check in guard_checks:
354
+ area = str(check.get("area") or "unknown")
355
+ areas[area] = areas.get(area, 0) + 1
356
+ blocking_hits += int(check.get("blocking_rules_returned") or 0)
357
+
358
+ conditioned_learnings = [dict(row) for row in conn.execute(
359
+ """SELECT id, title, applies_to, priority, status, weight, guard_hits, updated_at
360
+ FROM learnings
361
+ WHERE status = 'active' AND applies_to IS NOT NULL AND TRIM(applies_to) != ''
362
+ ORDER BY COALESCE(guard_hits, 0) DESC, updated_at DESC
363
+ LIMIT ?""",
364
+ (max_limit,),
365
+ ).fetchall()]
366
+
367
+ return {
368
+ "protocol_summary": protocol_summary,
369
+ "debt_summary": debt_summary,
370
+ "recent_tasks": recent_tasks,
371
+ "recent_debts": recent_debts,
372
+ "workflow_summary": workflow_summary,
373
+ "recent_runs": recent_runs,
374
+ "goal_summary": goal_summary,
375
+ "recent_goals": recent_goals,
376
+ "guard_summary": {
377
+ "recent_checks": len(guard_checks),
378
+ "blocking_hits": blocking_hits,
379
+ "areas": areas,
380
+ },
381
+ "guard_checks": guard_checks,
382
+ "conditioned_learnings": conditioned_learnings,
383
+ }
384
+
385
+
259
386
  # ---------------------------------------------------------------------------
260
387
  # HTML page routes — Jinja2 with fallback to plain file
261
388
  # ---------------------------------------------------------------------------
@@ -321,6 +448,10 @@ async def page_trust():
321
448
  async def page_guard():
322
449
  return _render("guard.html")
323
450
 
451
+ @app.get("/protocol", response_class=HTMLResponse)
452
+ async def page_protocol():
453
+ return _render("protocol.html", snapshot=_protocol_explainability_snapshot())
454
+
324
455
  @app.get("/cortex", response_class=HTMLResponse)
325
456
  async def page_cortex():
326
457
  return _render("cortex.html")
@@ -1297,6 +1428,15 @@ async def api_trust_events(limit: int = Query(50, ge=1, le=200)):
1297
1428
  return {"events": [dict(r) for r in rows]}
1298
1429
 
1299
1430
 
1431
+ # ---------------------------------------------------------------------------
1432
+ # Protocol Explainability
1433
+ # ---------------------------------------------------------------------------
1434
+
1435
+ @app.get("/api/protocol")
1436
+ async def api_protocol(limit: int = Query(20, ge=5, le=100)):
1437
+ return _protocol_explainability_snapshot(limit=limit)
1438
+
1439
+
1300
1440
  # ---------------------------------------------------------------------------
1301
1441
  # Guard Heatmap
1302
1442
  # ---------------------------------------------------------------------------
@@ -162,6 +162,10 @@
162
162
  <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
163
163
  Guard Heatmap
164
164
  </a>
165
+ <a href="/protocol" class="nav-item flex items-center gap-2.5 px-3 py-1.5 rounded-md text-xs text-slate-400 transition-colors" data-page="protocol">
166
+ <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6M7 4h10a2 2 0 012 2v12a2 2 0 01-2 2H7a2 2 0 01-2-2V6a2 2 0 012-2z"/></svg>
167
+ Protocol Explainability
168
+ </a>
165
169
  <a href="/cortex" class="nav-item flex items-center gap-2.5 px-3 py-1.5 rounded-md text-xs text-slate-400 transition-colors" data-page="cortex">
166
170
  <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
167
171
  Cortex Monitor
@@ -0,0 +1,199 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Protocol Explainability{% endblock %}
4
+
5
+ {% block page_title %}Protocol Explainability{% endblock %}
6
+ {% block page_subtitle %}
7
+ <span class="text-xs text-slate-500 ml-2">Why the runtime acted, blocked, drifted, or asked for evidence</span>
8
+ {% endblock %}
9
+
10
+ {% block header_actions %}
11
+ <a href="/api/protocol" class="text-xs text-slate-400 hover:text-slate-200 transition-colors px-2 py-1 rounded hover:bg-slate-800 font-mono">
12
+ /api/protocol
13
+ </a>
14
+ {% endblock %}
15
+
16
+ {% block content %}
17
+ {% set compliance = snapshot.protocol_summary or {} %}
18
+ {% set overall = compliance.overall_compliance_pct or 0 %}
19
+ {% set debt = snapshot.debt_summary or {} %}
20
+ {% set workflow = snapshot.workflow_summary or {} %}
21
+ {% set goals = snapshot.goal_summary or {} %}
22
+ {% set conditioned = snapshot.conditioned_learnings or [] %}
23
+
24
+ <div class="grid grid-cols-5 gap-4 mb-6">
25
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
26
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Compliance 7d</p>
27
+ <p class="text-2xl font-display font-bold {{ 'text-emerald-400' if overall >= 80 else 'text-amber-400' if overall >= 60 else 'text-red-400' }}">{{ "%.1f"|format(overall) }}%</p>
28
+ </div>
29
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
30
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Open Debt</p>
31
+ <p class="text-2xl font-display font-bold {{ 'text-emerald-400' if (debt.open_total or 0) == 0 else 'text-red-400' }}">{{ debt.open_total or 0 }}</p>
32
+ </div>
33
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
34
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Open Runs</p>
35
+ <p class="text-2xl font-display font-bold text-slate-100">{{ workflow.open_runs or 0 }}</p>
36
+ </div>
37
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
38
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Blocked Goals</p>
39
+ <p class="text-2xl font-display font-bold {{ 'text-red-400' if (goals.blocked or 0) else 'text-emerald-400' }}">{{ goals.blocked or 0 }}</p>
40
+ </div>
41
+ <div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
42
+ <p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Conditioned Files</p>
43
+ <p class="text-2xl font-display font-bold text-nexo-400">{{ conditioned|length }}</p>
44
+ </div>
45
+ </div>
46
+
47
+ <div class="grid grid-cols-2 gap-5 mb-6">
48
+ <section class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
49
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-4">Protocol Requirements</h2>
50
+ <div class="space-y-3">
51
+ {% for key, label in [('guard_check', 'Guard before action'), ('heartbeat', 'Heartbeat with context'), ('change_log', 'Change log after edits'), ('learning_capture', 'Learning capture after correction'), ('done_evidence', 'Evidence before done')] %}
52
+ {% set item = compliance.get(key, {}) %}
53
+ <div class="flex items-center justify-between gap-4">
54
+ <div>
55
+ <p class="text-sm text-slate-200">{{ label }}</p>
56
+ <p class="text-[11px] text-slate-500 font-mono">
57
+ {% if item.required is defined %}required {{ item.required }}{% endif %}
58
+ {% if item.total is defined %}total {{ item.total }}{% endif %}
59
+ {% if item.executed is defined %} • executed {{ item.executed }}{% endif %}
60
+ {% if item.logged is defined %} • logged {{ item.logged }}{% endif %}
61
+ {% if item.captured is defined %} • captured {{ item.captured }}{% endif %}
62
+ {% if item.done_tasks is defined %} • done {{ item.done_tasks }}{% endif %}
63
+ </p>
64
+ </div>
65
+ <span class="text-sm font-mono {{ 'text-emerald-400' if (item.compliance_pct or 0) >= 80 else 'text-amber-400' if (item.compliance_pct or 0) >= 60 else 'text-red-400' }}">
66
+ {{ "%.1f"|format(item.compliance_pct or 0) }}%
67
+ </span>
68
+ </div>
69
+ {% endfor %}
70
+ </div>
71
+ </section>
72
+
73
+ <section class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
74
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-4">Debt Pressure</h2>
75
+ <div class="grid grid-cols-3 gap-3 mb-4">
76
+ {% for severity in ['error', 'warn', 'info'] %}
77
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
78
+ <p class="text-[10px] uppercase tracking-wider text-slate-500">{{ severity }}</p>
79
+ <p class="text-xl font-display font-bold text-slate-100">{{ debt.by_severity.get(severity, 0) }}</p>
80
+ </div>
81
+ {% endfor %}
82
+ </div>
83
+ <div class="space-y-2 max-h-56 overflow-y-auto pr-1">
84
+ {% for item in snapshot.recent_debts[:10] %}
85
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
86
+ <div class="flex items-center justify-between gap-3 mb-1">
87
+ <span class="text-xs font-semibold {{ 'text-red-400' if item.severity == 'error' else 'text-amber-400' if item.severity == 'warn' else 'text-slate-400' }}">{{ item.debt_type }}</span>
88
+ <span class="text-[10px] text-slate-500 font-mono">{{ item.status }}</span>
89
+ </div>
90
+ <p class="text-xs text-slate-300">{{ item.evidence or 'No evidence summary recorded.' }}</p>
91
+ </div>
92
+ {% else %}
93
+ <p class="text-sm text-slate-500">No protocol debt recorded.</p>
94
+ {% endfor %}
95
+ </div>
96
+ </section>
97
+ </div>
98
+
99
+ <div class="grid grid-cols-2 gap-5 mb-6">
100
+ <section class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
101
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-4">Recent Protocol Tasks</h2>
102
+ <div class="space-y-3 max-h-[420px] overflow-y-auto pr-1">
103
+ {% for item in snapshot.recent_tasks[:12] %}
104
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
105
+ <div class="flex items-center justify-between gap-3 mb-1">
106
+ <p class="text-sm text-slate-100">{{ item.goal }}</p>
107
+ <span class="text-[10px] font-mono px-2 py-0.5 rounded bg-slate-800 text-slate-400">{{ item.status }}</span>
108
+ </div>
109
+ <p class="text-[11px] text-slate-500 font-mono mb-2">{{ item.task_type }} • cortex={{ item.cortex_mode or 'n/a' }} • response={{ item.response_mode or 'n/a' }}</p>
110
+ <div class="flex flex-wrap gap-2 text-[10px]">
111
+ {% if item.guarded_open %}<span class="px-2 py-0.5 rounded bg-nexo-500/15 text-nexo-300">guarded open</span>{% endif %}
112
+ {% if item.must_verify %}<span class="px-2 py-0.5 rounded bg-amber-500/15 text-amber-300">must verify</span>{% endif %}
113
+ {% if item.must_change_log %}<span class="px-2 py-0.5 rounded bg-blue-500/15 text-blue-300">must change-log</span>{% endif %}
114
+ {% if item.has_evidence %}<span class="px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-300">evidence attached</span>{% endif %}
115
+ {% if item.guard_has_blocking %}<span class="px-2 py-0.5 rounded bg-red-500/15 text-red-300">blocking risk</span>{% endif %}
116
+ </div>
117
+ </div>
118
+ {% else %}
119
+ <p class="text-sm text-slate-500">No protocol tasks yet.</p>
120
+ {% endfor %}
121
+ </div>
122
+ </section>
123
+
124
+ <section class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
125
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-4">Durable Workflow State</h2>
126
+ <div class="grid grid-cols-3 gap-3 mb-4">
127
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
128
+ <p class="text-[10px] uppercase tracking-wider text-slate-500">Open Runs</p>
129
+ <p class="text-xl font-display font-bold text-slate-100">{{ workflow.open_runs or 0 }}</p>
130
+ </div>
131
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
132
+ <p class="text-[10px] uppercase tracking-wider text-slate-500">Blocked Runs</p>
133
+ <p class="text-xl font-display font-bold {{ 'text-red-400' if (workflow.blocked_runs or 0) else 'text-slate-100' }}">{{ workflow.blocked_runs or 0 }}</p>
134
+ </div>
135
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
136
+ <p class="text-[10px] uppercase tracking-wider text-slate-500">Waiting Approval</p>
137
+ <p class="text-xl font-display font-bold {{ 'text-amber-400' if (workflow.waiting_approval or 0) else 'text-slate-100' }}">{{ workflow.waiting_approval or 0 }}</p>
138
+ </div>
139
+ </div>
140
+ <div class="space-y-3 max-h-[420px] overflow-y-auto pr-1">
141
+ {% for item in snapshot.recent_runs[:10] %}
142
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
143
+ <div class="flex items-center justify-between gap-3 mb-1">
144
+ <p class="text-sm text-slate-100">{{ item.goal }}</p>
145
+ <span class="text-[10px] font-mono px-2 py-0.5 rounded bg-slate-800 text-slate-400">{{ item.status }}</span>
146
+ </div>
147
+ <p class="text-[11px] text-slate-500 font-mono">{{ item.workflow_kind }}{% if item.current_step_key %} • step={{ item.current_step_key }}{% endif %}{% if item.next_action %} • next={{ item.next_action }}{% endif %}</p>
148
+ </div>
149
+ {% else %}
150
+ <p class="text-sm text-slate-500">No workflow runs recorded.</p>
151
+ {% endfor %}
152
+ </div>
153
+ </section>
154
+ </div>
155
+
156
+ <div class="grid grid-cols-2 gap-5">
157
+ <section class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
158
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-4">Conditioned Learnings</h2>
159
+ <div class="space-y-3 max-h-[420px] overflow-y-auto pr-1">
160
+ {% for item in conditioned[:12] %}
161
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
162
+ <div class="flex items-center justify-between gap-3 mb-1">
163
+ <p class="text-sm text-slate-100">{{ item.title }}</p>
164
+ <span class="text-[10px] font-mono px-2 py-0.5 rounded bg-slate-800 text-slate-400">{{ item.priority or 'normal' }}</span>
165
+ </div>
166
+ <p class="text-xs text-slate-400 break-all">{{ item.applies_to }}</p>
167
+ <p class="text-[11px] text-slate-500 font-mono mt-1">guard_hits={{ item.guard_hits or 0 }} • weight={{ "%.2f"|format(item.weight or 0) }}</p>
168
+ </div>
169
+ {% else %}
170
+ <p class="text-sm text-slate-500">No conditioned learnings active.</p>
171
+ {% endfor %}
172
+ </div>
173
+ </section>
174
+
175
+ <section class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
176
+ <h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-4">Guard Pressure</h2>
177
+ <div class="grid grid-cols-2 gap-3 mb-4">
178
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
179
+ <p class="text-[10px] uppercase tracking-wider text-slate-500">Recent Checks</p>
180
+ <p class="text-xl font-display font-bold text-slate-100">{{ snapshot.guard_summary.recent_checks or 0 }}</p>
181
+ </div>
182
+ <div class="rounded-lg border border-slate-800/60 bg-slate-950/50 p-3">
183
+ <p class="text-[10px] uppercase tracking-wider text-slate-500">Blocking Hits</p>
184
+ <p class="text-xl font-display font-bold {{ 'text-red-400' if (snapshot.guard_summary.blocking_hits or 0) else 'text-slate-100' }}">{{ snapshot.guard_summary.blocking_hits or 0 }}</p>
185
+ </div>
186
+ </div>
187
+ <div class="space-y-2">
188
+ {% for area, count in snapshot.guard_summary.areas.items() %}
189
+ <div class="flex items-center justify-between rounded-lg border border-slate-800/60 bg-slate-950/50 px-3 py-2">
190
+ <span class="text-sm text-slate-300">{{ area }}</span>
191
+ <span class="text-xs font-mono text-slate-500">{{ count }} checks</span>
192
+ </div>
193
+ {% else %}
194
+ <p class="text-sm text-slate-500">No guard pressure recorded.</p>
195
+ {% endfor %}
196
+ </div>
197
+ </section>
198
+ </div>
199
+ {% endblock %}
@@ -44,7 +44,7 @@ from db._reminders import (
44
44
 
45
45
  # Learnings
46
46
  from db._learnings import (
47
- create_learning, update_learning, delete_learning,
47
+ create_learning, update_learning, supersede_learning, delete_learning,
48
48
  search_learnings, list_learnings,
49
49
  extract_keywords, find_similar_learnings,
50
50
  )
@@ -93,6 +93,28 @@ from db._cron_runs import (
93
93
  cron_run_start, cron_run_end, cron_runs_recent, cron_runs_summary,
94
94
  )
95
95
 
96
+ # Protocol discipline runtime
97
+ from db._protocol import (
98
+ create_protocol_task, get_protocol_task, close_protocol_task,
99
+ create_protocol_debt, resolve_protocol_debts, list_protocol_debts,
100
+ protocol_compliance_summary,
101
+ )
102
+
103
+ # Durable workflow runtime
104
+ from db._workflow import (
105
+ create_workflow_run, get_workflow_run, list_workflow_runs,
106
+ list_workflow_steps, record_workflow_transition,
107
+ get_workflow_replay, get_workflow_resume_state,
108
+ create_workflow_goal, get_workflow_goal, list_workflow_goals,
109
+ update_workflow_goal,
110
+ )
111
+
112
+ # State watchers
113
+ from db._watchers import (
114
+ create_state_watcher, get_state_watcher, list_state_watchers,
115
+ update_state_watcher, update_state_watcher_result,
116
+ )
117
+
96
118
  # Personal scripts registry
97
119
  from db._personal_scripts import (
98
120
  upsert_personal_script, list_personal_scripts, get_personal_script,
@@ -13,6 +13,7 @@ def create_learning(
13
13
  reasoning: str = '',
14
14
  prevention: str = '',
15
15
  applies_to: str = '',
16
+ supersedes_id: int | None = None,
16
17
  status: str = 'active',
17
18
  review_due_at: float | None = None,
18
19
  last_reviewed_at: float | None = None,
@@ -22,11 +23,11 @@ def create_learning(
22
23
  now = now_epoch()
23
24
  cursor = conn.execute(
24
25
  "INSERT INTO learnings "
25
- "(category, title, content, reasoning, prevention, applies_to, status, review_due_at, last_reviewed_at, created_at, updated_at) "
26
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
26
+ "(category, title, content, reasoning, prevention, applies_to, supersedes_id, status, review_due_at, last_reviewed_at, created_at, updated_at) "
27
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
27
28
  (
28
29
  category, title, content, reasoning, prevention, applies_to,
29
- status, review_due_at, last_reviewed_at, now, now,
30
+ supersedes_id, status, review_due_at, last_reviewed_at, now, now,
30
31
  )
31
32
  )
32
33
  conn.commit()
@@ -60,6 +61,33 @@ def update_learning(id: int, **kwargs) -> dict:
60
61
  return r
61
62
 
62
63
 
64
+ def supersede_learning(old_id: int, new_id: int, note: str = '') -> dict:
65
+ """Mark an older learning as superseded by a newer canonical learning."""
66
+ conn = get_db()
67
+ old_row = conn.execute("SELECT * FROM learnings WHERE id = ?", (old_id,)).fetchone()
68
+ new_row = conn.execute("SELECT * FROM learnings WHERE id = ?", (new_id,)).fetchone()
69
+ if not old_row:
70
+ return {"error": f"Learning {old_id} not found"}
71
+ if not new_row:
72
+ return {"error": f"Learning {new_id} not found"}
73
+
74
+ old_reasoning = str(old_row["reasoning"] or "")
75
+ suffix = note.strip() if note.strip() else f"Superseded by learning #{new_id}."
76
+ combined_reasoning = f"{old_reasoning}\n{suffix}".strip() if old_reasoning else suffix
77
+ updated_at = now_epoch()
78
+ conn.execute(
79
+ "UPDATE learnings SET status = 'superseded', updated_at = ?, reasoning = ? WHERE id = ?",
80
+ (updated_at, combined_reasoning, old_id),
81
+ )
82
+ conn.execute(
83
+ "UPDATE learnings SET supersedes_id = ?, updated_at = ? WHERE id = ?",
84
+ (old_id, updated_at, new_id),
85
+ )
86
+ conn.commit()
87
+ row = conn.execute("SELECT * FROM learnings WHERE id = ?", (old_id,)).fetchone()
88
+ return dict(row)
89
+
90
+
63
91
  def delete_learning(id: int) -> bool:
64
92
  """Delete a learning entry."""
65
93
  conn = get_db()
@@ -166,4 +194,3 @@ def find_similar_learnings(new_id: int, title: str, content: str, category: str)
166
194
  results.sort(key=lambda x: x[1], reverse=True)
167
195
  return results[:5]
168
196
 
169
-
@@ -517,6 +517,18 @@ def get_personal_script_health_report(*, fix: bool = False) -> dict:
517
517
 
518
518
  for schedule in audit.get("schedules", []):
519
519
  checked += 1
520
+ if schedule.get("schedule_managed") and schedule.get("schedule_type") == "keep_alive":
521
+ runtime_state = str(schedule.get("runtime_state", "") or "")
522
+ runtime_summary = str(schedule.get("runtime_summary", "") or runtime_state or "runtime issue")
523
+ if runtime_state in {"degraded", "stale", "duplicated"}:
524
+ severity = "error" if runtime_state == "duplicated" else "warn"
525
+ issues.append({
526
+ "script_id": schedule.get("script_name") or schedule.get("script_path") or schedule.get("cron_id"),
527
+ "severity": severity,
528
+ "message": (
529
+ f"keep_alive runtime {schedule['cron_id']}: {runtime_summary}"
530
+ ),
531
+ })
520
532
  if schedule.get("schedule_managed"):
521
533
  continue
522
534