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.
- 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 +290 -6
- package/src/cli.py +111 -0
- package/src/client_preferences.py +94 -0
- package/src/client_sync.py +202 -2
- package/src/cognitive/__init__.py +1 -1
- package/src/cognitive/_search.py +39 -19
- package/src/dashboard/app.py +140 -0
- 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/runtime.py +918 -7
- 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/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
package/src/dashboard/app.py
CHANGED
|
@@ -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 %}
|
package/src/db/__init__.py
CHANGED
|
@@ -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,
|
package/src/db/_learnings.py
CHANGED
|
@@ -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
|
|