nexo-brain 2.6.21 → 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 +72 -20
- package/hooks/hooks.json +79 -0
- package/package.json +1 -1
- package/src/agent_runner.py +296 -8
- package/src/cli.py +209 -4
- package/src/client_preferences.py +115 -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 +264 -0
- package/src/dashboard/templates/base.html +4 -0
- package/src/dashboard/templates/dashboard.html +59 -1
- 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 +1095 -3
- 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 +482 -2
- 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/client_sync.py
CHANGED
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
import argparse
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
9
|
+
import shlex
|
|
8
10
|
import shutil
|
|
9
11
|
import subprocess
|
|
10
12
|
import sys
|
|
@@ -308,6 +310,179 @@ def _write_json_object(path: Path, payload: dict) -> None:
|
|
|
308
310
|
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
309
311
|
|
|
310
312
|
|
|
313
|
+
CORE_HOOK_SPECS = [
|
|
314
|
+
{
|
|
315
|
+
"event": "SessionStart",
|
|
316
|
+
"identity": "session-start-ts",
|
|
317
|
+
"timeout": 2,
|
|
318
|
+
"command_template": lambda nexo_home, _runtime_root, _hooks_dir: (
|
|
319
|
+
f"mkdir -p {shlex.quote(str(nexo_home / 'operations'))} && "
|
|
320
|
+
f"date +%s > {shlex.quote(str(nexo_home / 'operations' / '.session-start-ts'))}"
|
|
321
|
+
),
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"event": "SessionStart",
|
|
325
|
+
"identity": "daily-briefing-check.sh",
|
|
326
|
+
"timeout": 5,
|
|
327
|
+
"script": "daily-briefing-check.sh",
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"event": "SessionStart",
|
|
331
|
+
"identity": "session-start.sh",
|
|
332
|
+
"timeout": 35,
|
|
333
|
+
"script": "session-start.sh",
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"event": "Stop",
|
|
337
|
+
"identity": "session-stop.sh",
|
|
338
|
+
"timeout": 10,
|
|
339
|
+
"script": "session-stop.sh",
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"event": "PostToolUse",
|
|
343
|
+
"identity": "capture-tool-logs.sh",
|
|
344
|
+
"timeout": 5,
|
|
345
|
+
"script": "capture-tool-logs.sh",
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"event": "PostToolUse",
|
|
349
|
+
"identity": "capture-session.sh",
|
|
350
|
+
"timeout": 3,
|
|
351
|
+
"script": "capture-session.sh",
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"event": "PostToolUse",
|
|
355
|
+
"identity": "inbox-hook.sh",
|
|
356
|
+
"timeout": 5,
|
|
357
|
+
"script": "inbox-hook.sh",
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"event": "PostToolUse",
|
|
361
|
+
"identity": "protocol-guardrail.sh",
|
|
362
|
+
"timeout": 5,
|
|
363
|
+
"script": "protocol-guardrail.sh",
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"event": "PreCompact",
|
|
367
|
+
"identity": "pre-compact.sh",
|
|
368
|
+
"timeout": 10,
|
|
369
|
+
"script": "pre-compact.sh",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"event": "PostCompact",
|
|
373
|
+
"identity": "post-compact.sh",
|
|
374
|
+
"timeout": 10,
|
|
375
|
+
"script": "post-compact.sh",
|
|
376
|
+
},
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _resolve_hook_source_dir(runtime_root: Path) -> Path:
|
|
381
|
+
direct = runtime_root / "hooks"
|
|
382
|
+
if direct.is_dir():
|
|
383
|
+
return direct
|
|
384
|
+
sibling = runtime_root.parent / "src" / "hooks"
|
|
385
|
+
if sibling.is_dir():
|
|
386
|
+
return sibling
|
|
387
|
+
fallback = runtime_root.parent / "hooks"
|
|
388
|
+
if fallback.is_dir():
|
|
389
|
+
return fallback
|
|
390
|
+
return direct
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _render_hook_command(spec: dict, *, nexo_home: Path, runtime_root: Path, hooks_dir: Path) -> str:
|
|
394
|
+
command_template = spec.get("command_template")
|
|
395
|
+
if callable(command_template):
|
|
396
|
+
return command_template(nexo_home, runtime_root, hooks_dir)
|
|
397
|
+
script_name = spec.get("script", "").strip()
|
|
398
|
+
script_path = hooks_dir / script_name
|
|
399
|
+
return (
|
|
400
|
+
f"NEXO_HOME={shlex.quote(str(nexo_home))} "
|
|
401
|
+
f"NEXO_CODE={shlex.quote(str(runtime_root))} "
|
|
402
|
+
f"bash {shlex.quote(str(script_path))}"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _hook_identity(command: str) -> str:
|
|
407
|
+
text = str(command or "")
|
|
408
|
+
if ".session-start-ts" in text:
|
|
409
|
+
return "session-start-ts"
|
|
410
|
+
match = re.search(r"([A-Za-z0-9._-]+\.sh)\b", text)
|
|
411
|
+
if match:
|
|
412
|
+
return match.group(1)
|
|
413
|
+
return text.strip()
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _normalize_hook_sections(entries) -> list[dict]:
|
|
417
|
+
normalized: list[dict] = []
|
|
418
|
+
for entry in entries or []:
|
|
419
|
+
if not isinstance(entry, dict):
|
|
420
|
+
continue
|
|
421
|
+
hooks = entry.get("hooks")
|
|
422
|
+
if isinstance(hooks, list):
|
|
423
|
+
normalized.append(
|
|
424
|
+
{
|
|
425
|
+
"matcher": entry.get("matcher", "*") or "*",
|
|
426
|
+
"hooks": [dict(hook) for hook in hooks if isinstance(hook, dict)],
|
|
427
|
+
}
|
|
428
|
+
)
|
|
429
|
+
continue
|
|
430
|
+
if entry.get("command"):
|
|
431
|
+
hook = {"type": entry.get("type", "command"), "command": entry["command"]}
|
|
432
|
+
if entry.get("timeout"):
|
|
433
|
+
hook["timeout"] = entry["timeout"]
|
|
434
|
+
normalized.append({"matcher": entry.get("matcher", "*") or "*", "hooks": [hook]})
|
|
435
|
+
return normalized
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _merge_core_hooks(existing_hooks, *, runtime_root: Path, nexo_home: Path) -> tuple[dict, int]:
|
|
439
|
+
hooks_payload = dict(existing_hooks) if isinstance(existing_hooks, dict) else {}
|
|
440
|
+
hooks_dir = _resolve_hook_source_dir(runtime_root)
|
|
441
|
+
managed_count = 0
|
|
442
|
+
|
|
443
|
+
for spec in CORE_HOOK_SPECS:
|
|
444
|
+
event = spec["event"]
|
|
445
|
+
sections = _normalize_hook_sections(hooks_payload.get(event))
|
|
446
|
+
hooks_payload[event] = sections
|
|
447
|
+
command = _render_hook_command(spec, nexo_home=nexo_home, runtime_root=runtime_root, hooks_dir=hooks_dir)
|
|
448
|
+
identity = spec["identity"]
|
|
449
|
+
|
|
450
|
+
found = False
|
|
451
|
+
for section in sections:
|
|
452
|
+
for hook in section["hooks"]:
|
|
453
|
+
if _hook_identity(hook.get("command", "")) != identity:
|
|
454
|
+
continue
|
|
455
|
+
hook["type"] = "command"
|
|
456
|
+
hook["command"] = command
|
|
457
|
+
if spec.get("timeout"):
|
|
458
|
+
hook["timeout"] = spec["timeout"]
|
|
459
|
+
found = True
|
|
460
|
+
managed_count += 1
|
|
461
|
+
break
|
|
462
|
+
if found:
|
|
463
|
+
break
|
|
464
|
+
|
|
465
|
+
if found:
|
|
466
|
+
continue
|
|
467
|
+
|
|
468
|
+
target = None
|
|
469
|
+
for section in sections:
|
|
470
|
+
if section.get("matcher", "*") == "*":
|
|
471
|
+
target = section
|
|
472
|
+
break
|
|
473
|
+
if target is None:
|
|
474
|
+
target = {"matcher": "*", "hooks": []}
|
|
475
|
+
sections.append(target)
|
|
476
|
+
|
|
477
|
+
new_hook = {"type": "command", "command": command}
|
|
478
|
+
if spec.get("timeout"):
|
|
479
|
+
new_hook["timeout"] = spec["timeout"]
|
|
480
|
+
target["hooks"].append(new_hook)
|
|
481
|
+
managed_count += 1
|
|
482
|
+
|
|
483
|
+
return hooks_payload, managed_count
|
|
484
|
+
|
|
485
|
+
|
|
311
486
|
def _sync_json_client(path: Path, server_config: dict, label: str, *, managed_metadata: dict | None = None) -> dict:
|
|
312
487
|
payload = _load_json_object(path)
|
|
313
488
|
mcp_servers = payload.setdefault("mcpServers", {})
|
|
@@ -343,6 +518,32 @@ def _claude_desktop_managed_metadata(server_config: dict, *, operator_name: str)
|
|
|
343
518
|
}
|
|
344
519
|
|
|
345
520
|
|
|
521
|
+
def _sync_claude_code_settings(path: Path, server_config: dict) -> dict:
|
|
522
|
+
payload = _load_json_object(path)
|
|
523
|
+
mcp_servers = payload.setdefault("mcpServers", {})
|
|
524
|
+
if not isinstance(mcp_servers, dict):
|
|
525
|
+
mcp_servers = {}
|
|
526
|
+
payload["mcpServers"] = mcp_servers
|
|
527
|
+
action = "updated" if "nexo" in mcp_servers else "created"
|
|
528
|
+
mcp_servers["nexo"] = server_config
|
|
529
|
+
|
|
530
|
+
runtime_root = Path(server_config.get("env", {}).get("NEXO_CODE", "")).expanduser()
|
|
531
|
+
nexo_home = Path(server_config.get("env", {}).get("NEXO_HOME", "")).expanduser()
|
|
532
|
+
payload["hooks"], managed_hook_count = _merge_core_hooks(
|
|
533
|
+
payload.get("hooks", {}),
|
|
534
|
+
runtime_root=runtime_root,
|
|
535
|
+
nexo_home=nexo_home,
|
|
536
|
+
)
|
|
537
|
+
_write_json_object(path, payload)
|
|
538
|
+
return {
|
|
539
|
+
"ok": True,
|
|
540
|
+
"client": "claude_code",
|
|
541
|
+
"action": action,
|
|
542
|
+
"path": str(path),
|
|
543
|
+
"managed_hook_count": managed_hook_count,
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
|
|
346
547
|
def sync_claude_code(
|
|
347
548
|
*,
|
|
348
549
|
nexo_home: str | os.PathLike[str] | None = None,
|
|
@@ -358,10 +559,9 @@ def sync_claude_code(
|
|
|
358
559
|
python_path=python_path,
|
|
359
560
|
operator_name=operator_name,
|
|
360
561
|
)
|
|
361
|
-
result =
|
|
562
|
+
result = _sync_claude_code_settings(
|
|
362
563
|
_claude_code_settings_path(Path(user_home).expanduser() if user_home else None),
|
|
363
564
|
server_config,
|
|
364
|
-
"claude_code",
|
|
365
565
|
)
|
|
366
566
|
bootstrap_result = sync_client_bootstrap(
|
|
367
567
|
"claude_code",
|
|
@@ -29,7 +29,7 @@ from cognitive._search import (
|
|
|
29
29
|
search, bm25_search, hyde_expand_query,
|
|
30
30
|
record_co_activation,
|
|
31
31
|
_kg_boost_results, _apply_temporal_boost,
|
|
32
|
-
create_trigger, check_triggers, list_triggers, delete_trigger, rearm_trigger,
|
|
32
|
+
create_trigger, preview_triggers, check_triggers, list_triggers, delete_trigger, rearm_trigger,
|
|
33
33
|
# Constants
|
|
34
34
|
CO_ACTIVATION_DECAY, CO_ACTIVATION_BOOST, CO_ACTIVATION_MIN_STRENGTH,
|
|
35
35
|
)
|
package/src/cognitive/_search.py
CHANGED
|
@@ -542,18 +542,17 @@ def create_trigger(pattern: str, action: str, context: str = "") -> int:
|
|
|
542
542
|
return cur.lastrowid
|
|
543
543
|
|
|
544
544
|
|
|
545
|
-
def
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
545
|
+
def _match_triggers(
|
|
546
|
+
text: str,
|
|
547
|
+
*,
|
|
548
|
+
use_semantic: bool = False,
|
|
549
|
+
semantic_threshold: float = 0.7,
|
|
550
|
+
fire: bool = False,
|
|
551
|
+
) -> list[dict]:
|
|
552
|
+
"""Match armed prospective triggers against text.
|
|
550
553
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
use_semantic: Also do embedding similarity matching
|
|
554
|
-
semantic_threshold: Min cosine similarity for semantic match
|
|
555
|
-
Returns:
|
|
556
|
-
List of fired triggers with actions
|
|
554
|
+
When ``fire`` is False, matches are previewed without mutating trigger state.
|
|
555
|
+
When ``fire`` is True, matching armed triggers transition to fired.
|
|
557
556
|
"""
|
|
558
557
|
if not text or not text.strip():
|
|
559
558
|
return []
|
|
@@ -571,7 +570,7 @@ def check_triggers(text: str, use_semantic: bool = False, semantic_threshold: fl
|
|
|
571
570
|
if use_semantic:
|
|
572
571
|
text_vec = embed(text)
|
|
573
572
|
|
|
574
|
-
|
|
573
|
+
matched_triggers = []
|
|
575
574
|
now = datetime.utcnow().isoformat()
|
|
576
575
|
|
|
577
576
|
for trigger in armed:
|
|
@@ -594,11 +593,12 @@ def check_triggers(text: str, use_semantic: bool = False, semantic_threshold: fl
|
|
|
594
593
|
match_type = f"semantic({sim:.3f})"
|
|
595
594
|
|
|
596
595
|
if matched:
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
596
|
+
if fire:
|
|
597
|
+
db.execute(
|
|
598
|
+
"UPDATE prospective_triggers SET status = 'fired', fired_at = ? WHERE id = ?",
|
|
599
|
+
(now, trigger["id"])
|
|
600
|
+
)
|
|
601
|
+
matched_triggers.append({
|
|
602
602
|
"id": trigger["id"],
|
|
603
603
|
"pattern": trigger["trigger_pattern"],
|
|
604
604
|
"action": trigger["action"],
|
|
@@ -607,10 +607,30 @@ def check_triggers(text: str, use_semantic: bool = False, semantic_threshold: fl
|
|
|
607
607
|
"created_at": trigger["created_at"],
|
|
608
608
|
})
|
|
609
609
|
|
|
610
|
-
if
|
|
610
|
+
if fire and matched_triggers:
|
|
611
611
|
db.commit()
|
|
612
612
|
|
|
613
|
-
return
|
|
613
|
+
return matched_triggers
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def preview_triggers(text: str, use_semantic: bool = False, semantic_threshold: float = 0.7) -> list[dict]:
|
|
617
|
+
"""Preview trigger matches without consuming or firing them."""
|
|
618
|
+
return _match_triggers(
|
|
619
|
+
text,
|
|
620
|
+
use_semantic=use_semantic,
|
|
621
|
+
semantic_threshold=semantic_threshold,
|
|
622
|
+
fire=False,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def check_triggers(text: str, use_semantic: bool = False, semantic_threshold: float = 0.7) -> list[dict]:
|
|
627
|
+
"""Check text against all armed triggers and fire matching ones."""
|
|
628
|
+
return _match_triggers(
|
|
629
|
+
text,
|
|
630
|
+
use_semantic=use_semantic,
|
|
631
|
+
semantic_threshold=semantic_threshold,
|
|
632
|
+
fire=True,
|
|
633
|
+
)
|
|
614
634
|
|
|
615
635
|
|
|
616
636
|
def list_triggers(status: str = "armed") -> list[dict]:
|
package/src/dashboard/app.py
CHANGED
|
@@ -156,6 +156,233 @@ def _email_db():
|
|
|
156
156
|
return conn
|
|
157
157
|
|
|
158
158
|
|
|
159
|
+
def _deep_sleep_dir() -> Path:
|
|
160
|
+
nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
161
|
+
return nexo_home / "operations" / "deep-sleep"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _latest_periodic_summary(kind: str) -> dict:
|
|
165
|
+
root = _deep_sleep_dir()
|
|
166
|
+
pattern = f"*-{kind}-summary.json"
|
|
167
|
+
candidates = []
|
|
168
|
+
for path in root.glob(pattern):
|
|
169
|
+
try:
|
|
170
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
171
|
+
except Exception:
|
|
172
|
+
continue
|
|
173
|
+
label = str(payload.get("label", "") or "")
|
|
174
|
+
if label:
|
|
175
|
+
candidates.append((label, payload))
|
|
176
|
+
if not candidates:
|
|
177
|
+
return {}
|
|
178
|
+
return sorted(candidates, key=lambda item: item[0])[-1][1]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _summarize_engineering_loop(weekly: dict, monthly: dict) -> dict:
|
|
182
|
+
matters_now = []
|
|
183
|
+
for item in (weekly.get("project_pulse") or weekly.get("top_projects") or [])[:4]:
|
|
184
|
+
matters_now.append(
|
|
185
|
+
{
|
|
186
|
+
"title": str(item.get("project", "") or "unknown"),
|
|
187
|
+
"detail": f"score {item.get('score', 0)}",
|
|
188
|
+
"tone": str(item.get("status", "watch") or "watch"),
|
|
189
|
+
"meta": ", ".join(item.get("reasons", [])[:2]) if isinstance(item.get("reasons"), list) else "",
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
drifting = []
|
|
194
|
+
protocol = weekly.get("protocol_summary") or {}
|
|
195
|
+
for key, label in (
|
|
196
|
+
("guard_check", "guard_check"),
|
|
197
|
+
("heartbeat", "heartbeat"),
|
|
198
|
+
("change_log", "change_log"),
|
|
199
|
+
):
|
|
200
|
+
item = protocol.get(key) or {}
|
|
201
|
+
pct = item.get("compliance_pct")
|
|
202
|
+
if isinstance(pct, (int, float)) and pct < 70:
|
|
203
|
+
drifting.append(
|
|
204
|
+
{
|
|
205
|
+
"title": label,
|
|
206
|
+
"detail": f"{pct:.1f}% compliance",
|
|
207
|
+
"tone": "critical" if pct < 45 else "elevated",
|
|
208
|
+
"meta": "",
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
for item in (weekly.get("top_patterns") or [])[:3]:
|
|
212
|
+
pattern = str(item.get("pattern", "") or "")
|
|
213
|
+
if pattern:
|
|
214
|
+
drifting.append(
|
|
215
|
+
{
|
|
216
|
+
"title": pattern,
|
|
217
|
+
"detail": f"{item.get('count', 0)}x this period",
|
|
218
|
+
"tone": "watch",
|
|
219
|
+
"meta": "recurring pattern",
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
if len(drifting) >= 4:
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
improving = []
|
|
226
|
+
trend = weekly.get("trend") or {}
|
|
227
|
+
trust_delta = trend.get("avg_trust_delta")
|
|
228
|
+
if isinstance(trust_delta, (int, float)) and trust_delta > 0:
|
|
229
|
+
improving.append({"title": "Trust", "detail": f"{trust_delta:+.1f}", "tone": "healthy", "meta": "vs previous window"})
|
|
230
|
+
delivery = weekly.get("delivery_metrics") or {}
|
|
231
|
+
if int(delivery.get("engineering_followups", 0) or 0) > 0:
|
|
232
|
+
improving.append(
|
|
233
|
+
{
|
|
234
|
+
"title": "Engineering followups",
|
|
235
|
+
"detail": str(delivery.get("engineering_followups", 0)),
|
|
236
|
+
"tone": "healthy",
|
|
237
|
+
"meta": "guardrails created from recurring patterns",
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
protocol_delta = trend.get("protocol_compliance_delta")
|
|
241
|
+
if isinstance(protocol_delta, (int, float)) and protocol_delta > 0:
|
|
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"})
|
|
249
|
+
corrections_delta = trend.get("total_corrections_delta")
|
|
250
|
+
if isinstance(corrections_delta, int) and corrections_delta < 0:
|
|
251
|
+
improving.append({"title": "Corrections", "detail": f"{corrections_delta:+d}", "tone": "healthy", "meta": "lower is better"})
|
|
252
|
+
mood_delta = trend.get("avg_mood_delta")
|
|
253
|
+
if isinstance(mood_delta, (int, float)) and mood_delta > 0:
|
|
254
|
+
improving.append({"title": "Mood", "detail": f"{mood_delta:+.3f}", "tone": "healthy", "meta": "vs previous window"})
|
|
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
|
+
|
|
263
|
+
return {
|
|
264
|
+
"weekly": weekly,
|
|
265
|
+
"monthly": monthly,
|
|
266
|
+
"matters_now": matters_now[:4],
|
|
267
|
+
"drifting": drifting[:4],
|
|
268
|
+
"improving": improving[:4],
|
|
269
|
+
}
|
|
270
|
+
|
|
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
|
+
|
|
159
386
|
# ---------------------------------------------------------------------------
|
|
160
387
|
# HTML page routes — Jinja2 with fallback to plain file
|
|
161
388
|
# ---------------------------------------------------------------------------
|
|
@@ -221,6 +448,10 @@ async def page_trust():
|
|
|
221
448
|
async def page_guard():
|
|
222
449
|
return _render("guard.html")
|
|
223
450
|
|
|
451
|
+
@app.get("/protocol", response_class=HTMLResponse)
|
|
452
|
+
async def page_protocol():
|
|
453
|
+
return _render("protocol.html", snapshot=_protocol_explainability_snapshot())
|
|
454
|
+
|
|
224
455
|
@app.get("/cortex", response_class=HTMLResponse)
|
|
225
456
|
async def page_cortex():
|
|
226
457
|
return _render("cortex.html")
|
|
@@ -396,6 +627,30 @@ async def api_trust():
|
|
|
396
627
|
}
|
|
397
628
|
|
|
398
629
|
|
|
630
|
+
@app.get("/api/project-pulse")
|
|
631
|
+
async def api_project_pulse(kind: str = Query("weekly", pattern="^(weekly|monthly)$")):
|
|
632
|
+
"""Latest project pressure snapshot from Deep Sleep summaries."""
|
|
633
|
+
summary = _latest_periodic_summary(kind)
|
|
634
|
+
if not summary:
|
|
635
|
+
return JSONResponse({"error": f"No {kind} summary found"}, status_code=404)
|
|
636
|
+
return {
|
|
637
|
+
"kind": kind,
|
|
638
|
+
"label": summary.get("label"),
|
|
639
|
+
"project_pulse": summary.get("project_pulse", []),
|
|
640
|
+
"top_projects": summary.get("top_projects", []),
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
@app.get("/api/engineering-loop")
|
|
645
|
+
async def api_engineering_loop():
|
|
646
|
+
"""Dashboard narrative: what matters now, what is drifting, what is improving."""
|
|
647
|
+
weekly = _latest_periodic_summary("weekly")
|
|
648
|
+
monthly = _latest_periodic_summary("monthly")
|
|
649
|
+
if not weekly and not monthly:
|
|
650
|
+
return JSONResponse({"error": "No periodic Deep Sleep summaries found"}, status_code=404)
|
|
651
|
+
return _summarize_engineering_loop(weekly or {}, monthly or {})
|
|
652
|
+
|
|
653
|
+
|
|
399
654
|
@app.get("/api/adaptive")
|
|
400
655
|
async def api_adaptive():
|
|
401
656
|
"""Adaptive personality: current weight state + mode history."""
|
|
@@ -1173,6 +1428,15 @@ async def api_trust_events(limit: int = Query(50, ge=1, le=200)):
|
|
|
1173
1428
|
return {"events": [dict(r) for r in rows]}
|
|
1174
1429
|
|
|
1175
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
|
+
|
|
1176
1440
|
# ---------------------------------------------------------------------------
|
|
1177
1441
|
# Guard Heatmap
|
|
1178
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
|