nexo-brain 7.31.12 → 7.32.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 +1 -1
- package/package.json +1 -1
- package/src/auto_close_sessions.py +38 -0
- package/src/cognitive/_search.py +13 -2
- package/src/crons/sync.py +14 -7
- package/src/db/_schema.py +32 -0
- package/src/hooks/auto_capture.py +8 -2
- package/src/hooks/post_tool_use.py +75 -1
- package/src/plugins/protocol.py +143 -0
- package/src/pre_answer_router.py +39 -6
- package/src/requirements.txt +12 -8
- package/src/tools_sessions.py +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.32.0",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.
|
|
21
|
+
Version `7.32.0` is the current packaged-runtime line. Minor release - Cognitive OS Ola 1: the causal/provenance graph now populates from every evidence-backed task close (the connect-the-dots substrate that was previously empty), and abandoned durable workflows are reaped so the cross-session resume surface stays clean. Bundles the 7.31.14 critical fixes (cron-fleet drift guard, deterministic spreading-activation id, real guard_context evidence, auto error->learning capture, fulfillable confidence-check gate, approval-paused workflow resume).
|
|
22
22
|
|
|
23
23
|
Previously in `7.31.9`: patch release over v7.31.8 - UI release closeout now has to prove the original reported symptom was reopened with observable evidence before claiming the release is ready.
|
|
24
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.32.0",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
|
@@ -182,6 +182,43 @@ def auto_close_open_protocol_tasks(conn, sid: str, task: str = "") -> list[str]:
|
|
|
182
182
|
return closed
|
|
183
183
|
|
|
184
184
|
|
|
185
|
+
def auto_close_abandoned_workflow_runs(conn, sid: str) -> dict:
|
|
186
|
+
"""Reap durable workflow_runs / workflow_goals abandoned by a stale session.
|
|
187
|
+
|
|
188
|
+
auto_close only reaped protocol_tasks; a session that opened a durable
|
|
189
|
+
workflow_run / workflow_goal and never closed it left a zombie 'running'
|
|
190
|
+
row forever, polluting the resume surface (M10 gap). Move non-terminal ones
|
|
191
|
+
to a terminal state when their owning session is reaped. closed_at/updated_at
|
|
192
|
+
use datetime('now') to match the workflow tables' timestamp format.
|
|
193
|
+
"""
|
|
194
|
+
note = "auto-close: stale session ended without explicit workflow close"
|
|
195
|
+
runs = conn.execute(
|
|
196
|
+
"SELECT run_id FROM workflow_runs "
|
|
197
|
+
"WHERE session_id = ? AND status IN ('open','running','blocked','waiting_approval')",
|
|
198
|
+
(sid,),
|
|
199
|
+
).fetchall()
|
|
200
|
+
for row in runs:
|
|
201
|
+
conn.execute(
|
|
202
|
+
"UPDATE workflow_runs SET status='cancelled', next_action=?, "
|
|
203
|
+
"closed_at=datetime('now'), updated_at=datetime('now') "
|
|
204
|
+
"WHERE run_id=? AND status IN ('open','running','blocked','waiting_approval')",
|
|
205
|
+
(note, row["run_id"]),
|
|
206
|
+
)
|
|
207
|
+
goals = conn.execute(
|
|
208
|
+
"SELECT goal_id FROM workflow_goals "
|
|
209
|
+
"WHERE session_id = ? AND status IN ('active','blocked')",
|
|
210
|
+
(sid,),
|
|
211
|
+
).fetchall()
|
|
212
|
+
for row in goals:
|
|
213
|
+
conn.execute(
|
|
214
|
+
"UPDATE workflow_goals SET status='abandoned', blocker_reason=?, "
|
|
215
|
+
"closed_at=datetime('now'), updated_at=datetime('now') "
|
|
216
|
+
"WHERE goal_id=? AND status IN ('active','blocked')",
|
|
217
|
+
(note, row["goal_id"]),
|
|
218
|
+
)
|
|
219
|
+
return {"runs": len(runs), "goals": len(goals)}
|
|
220
|
+
|
|
221
|
+
|
|
185
222
|
def main():
|
|
186
223
|
init_db()
|
|
187
224
|
conn = get_db()
|
|
@@ -197,6 +234,7 @@ def main():
|
|
|
197
234
|
draft = get_diary_draft(sid)
|
|
198
235
|
closed_tasks = auto_close_open_protocol_tasks(conn, sid, task=session.get("task", ""))
|
|
199
236
|
closed_task_ids.extend(closed_tasks)
|
|
237
|
+
auto_close_abandoned_workflow_runs(conn, sid)
|
|
200
238
|
|
|
201
239
|
if draft:
|
|
202
240
|
promote_draft_to_diary(sid, draft, task=session.get("task", ""))
|
package/src/cognitive/_search.py
CHANGED
|
@@ -784,8 +784,19 @@ CO_ACTIVATION_MIN_STRENGTH = 0.1
|
|
|
784
784
|
|
|
785
785
|
|
|
786
786
|
def _canonical_co_id(store: str, mid: int) -> int:
|
|
787
|
-
"""Create a canonical hash ID for co-activation tracking.
|
|
788
|
-
|
|
787
|
+
"""Create a canonical, PROCESS-STABLE hash ID for co-activation tracking.
|
|
788
|
+
|
|
789
|
+
MUST be deterministic across processes. Python's builtin hash() is salted
|
|
790
|
+
per process (PYTHONHASHSEED), so co-activation links written in one MCP
|
|
791
|
+
process never matched the same memory's id in the next — fragmenting the
|
|
792
|
+
associative graph (observed ~6x distinct ids per memory) and silently
|
|
793
|
+
degrading spreading activation to within-a-single-process-lifetime. blake2b
|
|
794
|
+
is stable across processes and runs.
|
|
795
|
+
"""
|
|
796
|
+
import hashlib
|
|
797
|
+
|
|
798
|
+
digest = hashlib.blake2b(f"{store}:{mid}".encode("utf-8"), digest_size=8).digest()
|
|
799
|
+
return int.from_bytes(digest, "big") % (2**31)
|
|
789
800
|
|
|
790
801
|
|
|
791
802
|
def record_co_activation(memory_ids: list[tuple[str, int]]):
|
package/src/crons/sync.py
CHANGED
|
@@ -727,13 +727,18 @@ def install_plist(label: str, plist: dict, plist_path: Path, dry_run: bool):
|
|
|
727
727
|
log(f" DRY-RUN: would install {plist_path.name}")
|
|
728
728
|
return
|
|
729
729
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
730
|
+
# Ephemeral/test runtimes (temp NEXO_HOME or HOME, e.g. a pytest run) must
|
|
731
|
+
# NOT touch the operator's real ~/Library/LaunchAgents. The guard is checked
|
|
732
|
+
# BEFORE writing the plist file: otherwise a test run rewrites the real
|
|
733
|
+
# plists with temp-dir ProgramArguments, and one reboot/reload silently
|
|
734
|
+
# kills the whole consolidation cron fleet (cron-fleet-drift incident).
|
|
733
735
|
if not launchctl_side_effects_allowed():
|
|
734
|
-
log(f"
|
|
736
|
+
log(f" Skipped plist write in ephemeral runtime: {plist_path.name}")
|
|
735
737
|
return
|
|
736
738
|
|
|
739
|
+
with open(plist_path, "wb") as f:
|
|
740
|
+
plistlib.dump(plist, f)
|
|
741
|
+
|
|
737
742
|
result = reload_launchagent_plist(plist_path, label=label)
|
|
738
743
|
if result.get("action") == "skipped-ephemeral-runtime":
|
|
739
744
|
log(f" Installed but skipped launchctl in ephemeral runtime: {plist_path.name}")
|
|
@@ -751,8 +756,8 @@ def unload_plist(plist_path: Path, dry_run: bool):
|
|
|
751
756
|
return
|
|
752
757
|
|
|
753
758
|
if not launchctl_side_effects_allowed():
|
|
754
|
-
|
|
755
|
-
log(f"
|
|
759
|
+
# Ephemeral/test runtime: never delete the operator's real plists.
|
|
760
|
+
log(f" Skipped plist removal in ephemeral runtime: {plist_path.name}")
|
|
756
761
|
return
|
|
757
762
|
|
|
758
763
|
result = unload_launchagent_plist(plist_path)
|
|
@@ -830,7 +835,9 @@ def sync(dry_run: bool = False):
|
|
|
830
835
|
return
|
|
831
836
|
|
|
832
837
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
833
|
-
|
|
838
|
+
# In an ephemeral/test runtime, do not even create the real LaunchAgents dir.
|
|
839
|
+
if launchctl_side_effects_allowed():
|
|
840
|
+
LAUNCH_AGENTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
834
841
|
|
|
835
842
|
manifest_crons = load_manifest()
|
|
836
843
|
manifest_ids = {c["id"] for c in manifest_crons}
|
package/src/db/_schema.py
CHANGED
|
@@ -3081,6 +3081,37 @@ def _m81_core_rules_product_metadata(conn):
|
|
|
3081
3081
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_core_rules_protected ON core_rules(protected, is_active)")
|
|
3082
3082
|
|
|
3083
3083
|
|
|
3084
|
+
def _m82_confidence_checks(conn):
|
|
3085
|
+
"""Persist nexo_confidence_check calls so the answer-contract gate works.
|
|
3086
|
+
|
|
3087
|
+
The G1 enforcer (hooks/g1_enforcer.py) treats a verify/ask/defer contract as
|
|
3088
|
+
fulfilled when a confidence_checks row exists for the session created after
|
|
3089
|
+
the task opened. No code ever created or wrote this table, so verify
|
|
3090
|
+
contracts were structurally unfulfillable. handle_confidence_check now writes
|
|
3091
|
+
a row per call; created_at uses datetime('now') to match opened_at format.
|
|
3092
|
+
"""
|
|
3093
|
+
conn.execute(
|
|
3094
|
+
"""
|
|
3095
|
+
CREATE TABLE IF NOT EXISTS confidence_checks (
|
|
3096
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3097
|
+
session_id TEXT,
|
|
3098
|
+
task_id TEXT,
|
|
3099
|
+
goal_hash TEXT,
|
|
3100
|
+
task_type TEXT,
|
|
3101
|
+
area TEXT,
|
|
3102
|
+
response_mode TEXT,
|
|
3103
|
+
confidence INTEGER,
|
|
3104
|
+
high_stakes INTEGER NOT NULL DEFAULT 0,
|
|
3105
|
+
created_at TEXT
|
|
3106
|
+
)
|
|
3107
|
+
"""
|
|
3108
|
+
)
|
|
3109
|
+
conn.execute(
|
|
3110
|
+
"CREATE INDEX IF NOT EXISTS idx_confidence_checks_session "
|
|
3111
|
+
"ON confidence_checks(session_id, created_at)"
|
|
3112
|
+
)
|
|
3113
|
+
|
|
3114
|
+
|
|
3084
3115
|
MIGRATIONS = [
|
|
3085
3116
|
(1, "learnings_columns", _m1_learnings_columns),
|
|
3086
3117
|
(2, "followups_reasoning", _m2_followups_reasoning),
|
|
@@ -3163,6 +3194,7 @@ MIGRATIONS = [
|
|
|
3163
3194
|
(79, "operational_closure_links_readiness", _m79_operational_closure_links_readiness),
|
|
3164
3195
|
(80, "opportunity_orchestrator", _m80_opportunity_orchestrator),
|
|
3165
3196
|
(81, "core_rules_product_metadata", _m81_core_rules_product_metadata),
|
|
3197
|
+
(82, "confidence_checks", _m82_confidence_checks),
|
|
3166
3198
|
]
|
|
3167
3199
|
|
|
3168
3200
|
|
|
@@ -289,7 +289,7 @@ def _content_hash(fact_type: str, content: str) -> str:
|
|
|
289
289
|
|
|
290
290
|
|
|
291
291
|
def _auto_learning_add(title: str, content: str) -> bool:
|
|
292
|
-
"""Best-effort call to tools_learnings.
|
|
292
|
+
"""Best-effort call to tools_learnings.handle_learning_add.
|
|
293
293
|
|
|
294
294
|
Returns True when the learning was stored, False otherwise. Failures
|
|
295
295
|
are silent so the hook itself never breaks the user's prompt flow.
|
|
@@ -300,13 +300,19 @@ def _auto_learning_add(title: str, content: str) -> bool:
|
|
|
300
300
|
return False
|
|
301
301
|
|
|
302
302
|
try:
|
|
303
|
-
|
|
303
|
+
# The public symbol is handle_learning_add. A prior call to a
|
|
304
|
+
# non-existent tools_learnings.add_learning raised AttributeError that
|
|
305
|
+
# was swallowed below, so EVERY auto-captured correction silently
|
|
306
|
+
# failed to persist a learning (error-capture / never-repeat broken).
|
|
307
|
+
result = tools_learnings.handle_learning_add(
|
|
304
308
|
category="auto",
|
|
305
309
|
title=title,
|
|
306
310
|
content=content,
|
|
307
311
|
priority="medium",
|
|
308
312
|
reasoning="auto-captured from correction pattern in UserPromptSubmit/PostToolUse hook",
|
|
309
313
|
)
|
|
314
|
+
if isinstance(result, str):
|
|
315
|
+
return not result.strip().upper().startswith("ERROR")
|
|
310
316
|
if isinstance(result, dict):
|
|
311
317
|
return bool(result.get("ok") or result.get("id") or result.get("learning_id"))
|
|
312
318
|
return bool(result)
|
|
@@ -415,6 +415,73 @@ def check_production_change_log_closeout(payload: dict, sid: str) -> str | None:
|
|
|
415
415
|
return append_operator_language_contract(message)
|
|
416
416
|
|
|
417
417
|
|
|
418
|
+
_SHARED_MUTATION_TOOLS = {
|
|
419
|
+
"Edit",
|
|
420
|
+
"Write",
|
|
421
|
+
"MultiEdit",
|
|
422
|
+
"NotebookEdit",
|
|
423
|
+
"apply_patch",
|
|
424
|
+
"functions.apply_patch",
|
|
425
|
+
}
|
|
426
|
+
_SHARED_PATH_RE = re.compile(
|
|
427
|
+
r"("
|
|
428
|
+
r"/Users/[^ \n\r\t'\"]+/Documents/_PhpstormProjects/|"
|
|
429
|
+
r"/Users/[^ \n\r\t'\"]+/.nexo/core/|"
|
|
430
|
+
r"/home/nexodesk/|"
|
|
431
|
+
r"/var/www/|"
|
|
432
|
+
r"/public_html/|"
|
|
433
|
+
r"/httpdocs/"
|
|
434
|
+
r")",
|
|
435
|
+
re.IGNORECASE,
|
|
436
|
+
)
|
|
437
|
+
_SCOPE_REQUIRED_MARKERS = {
|
|
438
|
+
"conversation": re.compile(r"\b(conversation|conversaci[oó]n|hilo|thread|email|ticket|mensaje|message|n/a)\b", re.IGNORECASE),
|
|
439
|
+
"tenant": re.compile(r"\b(tenant|tienda|shop|cuenta|account|cliente|client|n/a)\b", re.IGNORECASE),
|
|
440
|
+
"language": re.compile(r"\b(idioma|language|lang|locale|es|en|de|fr|pt|it|ca|n/a)\b", re.IGNORECASE),
|
|
441
|
+
"environment": re.compile(r"\b(entorno|environment|local|runtime|producto|producci[oó]n|production|prod|staging)\b", re.IGNORECASE),
|
|
442
|
+
"surface": re.compile(r"\b(superficie|surface|api|ui|dominio|domain|web|public)\b", re.IGNORECASE),
|
|
443
|
+
"deploy": re.compile(r"\b(deploy|despliegue|publicado|published|release|rama|branch|n/a)\b", re.IGNORECASE),
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _payload_scope_text(payload: dict) -> str:
|
|
448
|
+
try:
|
|
449
|
+
return json.dumps(_tool_input(payload), ensure_ascii=False)
|
|
450
|
+
except Exception:
|
|
451
|
+
return str(_tool_input(payload) or "")
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _is_shared_mutation_payload(payload: dict) -> bool:
|
|
455
|
+
tool_name = _tool_name(payload)
|
|
456
|
+
cmd = _extract_command(payload)
|
|
457
|
+
input_text = _payload_scope_text(payload)
|
|
458
|
+
combined = "\n".join(part for part in (tool_name, cmd, input_text) if part)
|
|
459
|
+
if tool_name in _SHARED_MUTATION_TOOLS and _SHARED_PATH_RE.search(combined):
|
|
460
|
+
return True
|
|
461
|
+
if cmd and (_is_production_mutation_command(cmd) or _SHARED_PATH_RE.search(cmd)):
|
|
462
|
+
return True
|
|
463
|
+
return False
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def check_shared_scope_closeout(payload: dict) -> str | None:
|
|
467
|
+
if not _is_shared_mutation_payload(payload):
|
|
468
|
+
return None
|
|
469
|
+
scope_text = _payload_scope_text(payload)
|
|
470
|
+
missing = [
|
|
471
|
+
label
|
|
472
|
+
for label, pattern in _SCOPE_REQUIRED_MARKERS.items()
|
|
473
|
+
if not pattern.search(scope_text)
|
|
474
|
+
]
|
|
475
|
+
if not missing:
|
|
476
|
+
return None
|
|
477
|
+
message = (
|
|
478
|
+
"Antes de seguir con este cambio compartido, deja fijado el alcance operativo: "
|
|
479
|
+
"conversación afectada, tenant/tienda, idiomas, entorno, superficie pública y estado de deploy. "
|
|
480
|
+
"Si algún campo no aplica, márcalo como N/A y continúa con evidencia."
|
|
481
|
+
)
|
|
482
|
+
return append_operator_language_contract(message)
|
|
483
|
+
|
|
484
|
+
|
|
418
485
|
def _run_auto_capture(payload: dict) -> int:
|
|
419
486
|
"""Pipe the tool result into auto_capture for post-output classification."""
|
|
420
487
|
text = _extract_tool_text(payload)
|
|
@@ -484,13 +551,20 @@ def main() -> int:
|
|
|
484
551
|
sid = _resolve_sid_from_payload(payload)
|
|
485
552
|
reminder = check_inbox_and_emit_reminder(sid)
|
|
486
553
|
change_log_message = check_production_change_log_closeout(payload, sid)
|
|
554
|
+
shared_scope_message = check_shared_scope_closeout(payload)
|
|
487
555
|
g1_message: str | None = None
|
|
488
556
|
try:
|
|
489
557
|
from g1_enforcer import check_response_contract_gate # type: ignore
|
|
490
558
|
g1_message = check_response_contract_gate(sid)
|
|
491
559
|
except Exception:
|
|
492
560
|
g1_message = None
|
|
493
|
-
combined = _combine_system_messages(
|
|
561
|
+
combined = _combine_system_messages(
|
|
562
|
+
protocol_message,
|
|
563
|
+
reminder,
|
|
564
|
+
change_log_message,
|
|
565
|
+
shared_scope_message,
|
|
566
|
+
g1_message,
|
|
567
|
+
)
|
|
494
568
|
if combined:
|
|
495
569
|
print(json.dumps({"systemMessage": combined}))
|
|
496
570
|
except Exception:
|
package/src/plugins/protocol.py
CHANGED
|
@@ -1307,6 +1307,58 @@ PUBLIC_RELEASE_WORK_RE = re.compile(
|
|
|
1307
1307
|
re.IGNORECASE,
|
|
1308
1308
|
)
|
|
1309
1309
|
HIGH_STAKES_WORK_TYPES = {"release", "deploy", "deployment", "publish", "publicar", "desplegar"}
|
|
1310
|
+
VISIBLE_RELEASE_SURFACE_PATTERNS = {
|
|
1311
|
+
"api": (
|
|
1312
|
+
re.compile(r"\bapi\b\s*[:=-]", re.IGNORECASE),
|
|
1313
|
+
re.compile(r"\bendpoint(?:s)?\b\s*[:=-]", re.IGNORECASE),
|
|
1314
|
+
re.compile(r"\bruta(?:s)?\b\s*[:=-]", re.IGNORECASE),
|
|
1315
|
+
),
|
|
1316
|
+
"ui": (
|
|
1317
|
+
re.compile(r"\bui\b\s*[:=-]", re.IGNORECASE),
|
|
1318
|
+
re.compile(r"\bux\b\s*[:=-]", re.IGNORECASE),
|
|
1319
|
+
re.compile(r"\binterfaz\b\s*[:=-]", re.IGNORECASE),
|
|
1320
|
+
re.compile(r"\bnavegador\b\s*[:=-]", re.IGNORECASE),
|
|
1321
|
+
re.compile(r"\bbrowser\b\s*[:=-]", re.IGNORECASE),
|
|
1322
|
+
),
|
|
1323
|
+
"dominio_publico": (
|
|
1324
|
+
re.compile(r"\bdominio\s+p[uú]blico\b\s*[:=-]", re.IGNORECASE),
|
|
1325
|
+
re.compile(r"\bpublic\s+domain\b\s*[:=-]", re.IGNORECASE),
|
|
1326
|
+
re.compile(r"\burl\s+p[uú]blica\b\s*[:=-]", re.IGNORECASE),
|
|
1327
|
+
re.compile(r"\bpublic\s+url\b\s*[:=-]", re.IGNORECASE),
|
|
1328
|
+
re.compile(r"https?://[^\s]+", re.IGNORECASE),
|
|
1329
|
+
),
|
|
1330
|
+
"rama_publicacion": (
|
|
1331
|
+
re.compile(r"\brama\b\s*[:=-]", re.IGNORECASE),
|
|
1332
|
+
re.compile(r"\bbranch\b\s*[:=-]", re.IGNORECASE),
|
|
1333
|
+
re.compile(r"\borigin/(?:main|master|stable|release|gh-pages)\b", re.IGNORECASE),
|
|
1334
|
+
re.compile(r"\bgh-pages\b", re.IGNORECASE),
|
|
1335
|
+
),
|
|
1336
|
+
"artefactos": (
|
|
1337
|
+
re.compile(r"\bartefacto(?:s)?\b\s*[:=-]", re.IGNORECASE),
|
|
1338
|
+
re.compile(r"\bartifact(?:s)?\b\s*[:=-]", re.IGNORECASE),
|
|
1339
|
+
re.compile(r"\b(?:dmg|exe|zip|image|imagen|cloud\s+build|github\s+release)\b", re.IGNORECASE),
|
|
1340
|
+
),
|
|
1341
|
+
"manifiestos": (
|
|
1342
|
+
re.compile(r"\bmanifiesto(?:s)?\b\s*[:=-]", re.IGNORECASE),
|
|
1343
|
+
re.compile(r"\bmanifest(?:s)?\b\s*[:=-]", re.IGNORECASE),
|
|
1344
|
+
re.compile(r"\b(?:manifest\.json|update\.json|package\.json|composer\.json)\b", re.IGNORECASE),
|
|
1345
|
+
),
|
|
1346
|
+
"prueba_viva": (
|
|
1347
|
+
re.compile(r"\bprueba\s+viva\b\s*[:=-]", re.IGNORECASE),
|
|
1348
|
+
re.compile(r"\blive\s+test\b\s*[:=-]", re.IGNORECASE),
|
|
1349
|
+
re.compile(r"\bsmoke\b\s*[:=-]", re.IGNORECASE),
|
|
1350
|
+
re.compile(r"\b(?:curl|playwright|logs?\s+vivos?|gcloud\s+logs|http\s*200|200\s+ok)\b", re.IGNORECASE),
|
|
1351
|
+
),
|
|
1352
|
+
}
|
|
1353
|
+
VISIBLE_RELEASE_SURFACE_LABELS = {
|
|
1354
|
+
"api": "API",
|
|
1355
|
+
"ui": "UI",
|
|
1356
|
+
"dominio_publico": "dominio público",
|
|
1357
|
+
"rama_publicacion": "rama de publicación",
|
|
1358
|
+
"artefactos": "artefactos",
|
|
1359
|
+
"manifiestos": "manifiestos",
|
|
1360
|
+
"prueba_viva": "prueba viva",
|
|
1361
|
+
}
|
|
1310
1362
|
|
|
1311
1363
|
|
|
1312
1364
|
def _normalize_artifact_hash(value: str) -> str:
|
|
@@ -1344,6 +1396,15 @@ def _has_public_release_evidence(evidence: str) -> bool:
|
|
|
1344
1396
|
return bool(PUBLIC_RELEASE_EVIDENCE_RE.search(evidence or ""))
|
|
1345
1397
|
|
|
1346
1398
|
|
|
1399
|
+
def _missing_visible_release_surfaces(evidence: str) -> list[str]:
|
|
1400
|
+
text = evidence or ""
|
|
1401
|
+
missing: list[str] = []
|
|
1402
|
+
for key, patterns in VISIBLE_RELEASE_SURFACE_PATTERNS.items():
|
|
1403
|
+
if not any(pattern.search(text) for pattern in patterns):
|
|
1404
|
+
missing.append(VISIBLE_RELEASE_SURFACE_LABELS[key])
|
|
1405
|
+
return missing
|
|
1406
|
+
|
|
1407
|
+
|
|
1347
1408
|
def _active_followup_snapshot(limit: int = 5) -> list[dict]:
|
|
1348
1409
|
try:
|
|
1349
1410
|
followups = get_followups("active")
|
|
@@ -1376,6 +1437,7 @@ def handle_confidence_check(
|
|
|
1376
1437
|
unknowns: str = "[]",
|
|
1377
1438
|
verification_step: str = "",
|
|
1378
1439
|
stakes: str = "",
|
|
1440
|
+
sid: str = "",
|
|
1379
1441
|
) -> str:
|
|
1380
1442
|
"""Return the metacognitive response mode: answer, verify, ask, or defer."""
|
|
1381
1443
|
clean_goal = (goal or "").strip()
|
|
@@ -1404,6 +1466,37 @@ def handle_confidence_check(
|
|
|
1404
1466
|
verification_step=(verification_step or "").strip(),
|
|
1405
1467
|
stakes=(stakes or "").strip(),
|
|
1406
1468
|
)
|
|
1469
|
+
# Persist the check so the G1 answer-contract gate can detect fulfillment of
|
|
1470
|
+
# verify/ask/defer contracts (this table was previously never written, so
|
|
1471
|
+
# verify contracts were structurally unfulfillable). Best-effort: a failure
|
|
1472
|
+
# here must never break the metacognitive answer — g1 simply re-nudges, a
|
|
1473
|
+
# visible signal rather than a silent corruption.
|
|
1474
|
+
try:
|
|
1475
|
+
import hashlib
|
|
1476
|
+
from db import get_db
|
|
1477
|
+
from plugins.guard import _resolve_active_sid
|
|
1478
|
+
conn = get_db()
|
|
1479
|
+
resolved_sid = (sid or "").strip() or _resolve_active_sid(conn)
|
|
1480
|
+
if resolved_sid:
|
|
1481
|
+
conn.execute(
|
|
1482
|
+
"""INSERT INTO confidence_checks
|
|
1483
|
+
(session_id, task_id, goal_hash, task_type, area,
|
|
1484
|
+
response_mode, confidence, high_stakes, created_at)
|
|
1485
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))""",
|
|
1486
|
+
(
|
|
1487
|
+
resolved_sid,
|
|
1488
|
+
"",
|
|
1489
|
+
hashlib.sha256(clean_goal.encode("utf-8")).hexdigest()[:16],
|
|
1490
|
+
clean_type,
|
|
1491
|
+
(area or "").strip(),
|
|
1492
|
+
str(result.get("mode") or ""),
|
|
1493
|
+
int(result.get("confidence") or 0),
|
|
1494
|
+
1 if result.get("high_stakes") else 0,
|
|
1495
|
+
),
|
|
1496
|
+
)
|
|
1497
|
+
conn.commit()
|
|
1498
|
+
except Exception:
|
|
1499
|
+
pass
|
|
1407
1500
|
return json.dumps({"ok": True, **result}, ensure_ascii=False, indent=2)
|
|
1408
1501
|
|
|
1409
1502
|
|
|
@@ -2289,6 +2382,22 @@ def handle_task_close(
|
|
|
2289
2382
|
debt_types=["missing_change_log"],
|
|
2290
2383
|
resolution="Change log created by nexo_task_close",
|
|
2291
2384
|
)
|
|
2385
|
+
# Cognitive OS Ola 1 — materialize causal/provenance edges from the
|
|
2386
|
+
# closed task (task→change_log "ops:produced" + change_log→task
|
|
2387
|
+
# "causal:motivated_by"). record_task_close_edges had NO caller, so
|
|
2388
|
+
# the causal graph stayed empty (0 candidates) and could never feed
|
|
2389
|
+
# connect-the-dots at answer time. Best-effort: graph wiring must
|
|
2390
|
+
# never break a task close.
|
|
2391
|
+
try:
|
|
2392
|
+
import causal_graph
|
|
2393
|
+
causal_graph.record_task_close_edges(
|
|
2394
|
+
task_id=task_id,
|
|
2395
|
+
change_log_id=change_log_id,
|
|
2396
|
+
project_key=str(task.get("project_hint") or task.get("area") or ""),
|
|
2397
|
+
reason_public=(clean_change_summary or task.get("goal") or "")[:200],
|
|
2398
|
+
)
|
|
2399
|
+
except Exception:
|
|
2400
|
+
pass
|
|
2292
2401
|
else:
|
|
2293
2402
|
debt = _ensure_open_debt(
|
|
2294
2403
|
task["session_id"],
|
|
@@ -2476,6 +2585,40 @@ def handle_task_close(
|
|
|
2476
2585
|
indent=2,
|
|
2477
2586
|
)
|
|
2478
2587
|
|
|
2588
|
+
if clean_outcome == "done" and _is_high_stakes_public_work(task, work_type, stakes, closure_text):
|
|
2589
|
+
missing_surfaces = _missing_visible_release_surfaces(live_surface_evidence)
|
|
2590
|
+
if missing_surfaces:
|
|
2591
|
+
debt = _ensure_open_debt(
|
|
2592
|
+
task["session_id"],
|
|
2593
|
+
task_id,
|
|
2594
|
+
"visible_release_surface_matrix_incomplete",
|
|
2595
|
+
severity="error",
|
|
2596
|
+
evidence=(
|
|
2597
|
+
"Visible release/deploy/fix close lacked the full evidence matrix. "
|
|
2598
|
+
f"Missing surfaces: {', '.join(missing_surfaces)}. "
|
|
2599
|
+
f"Evidence provided: {live_surface_evidence[:240]!r}"
|
|
2600
|
+
),
|
|
2601
|
+
debts=debts_created,
|
|
2602
|
+
)
|
|
2603
|
+
return json.dumps(
|
|
2604
|
+
{
|
|
2605
|
+
"ok": False,
|
|
2606
|
+
"error": "Cannot close visible release/deploy/fix as done without the full surface evidence matrix.",
|
|
2607
|
+
"hint": (
|
|
2608
|
+
"Enumerate API, UI, public domain, publication branch, artifacts, manifests, "
|
|
2609
|
+
"and live test evidence. Use N/A only for a surface that truly does not exist."
|
|
2610
|
+
),
|
|
2611
|
+
"task_id": task_id,
|
|
2612
|
+
"blocked_by": "visible_release_surface_matrix",
|
|
2613
|
+
"debt_id": debt.get("id"),
|
|
2614
|
+
"debt_type": "visible_release_surface_matrix_incomplete",
|
|
2615
|
+
"missing_surfaces": missing_surfaces,
|
|
2616
|
+
"response_mode": "verify",
|
|
2617
|
+
},
|
|
2618
|
+
ensure_ascii=False,
|
|
2619
|
+
indent=2,
|
|
2620
|
+
)
|
|
2621
|
+
|
|
2479
2622
|
if task.get("guard_has_blocking") and not files_changed_list:
|
|
2480
2623
|
open_task_debts = list_protocol_debts(status="open", task_id=task_id, limit=200)
|
|
2481
2624
|
has_guard_touch_violation = any(
|
package/src/pre_answer_router.py
CHANGED
|
@@ -1987,15 +1987,48 @@ def _source_filesystem(request: SourceRequest) -> SourceResult:
|
|
|
1987
1987
|
|
|
1988
1988
|
|
|
1989
1989
|
def _source_guard_context(request: SourceRequest) -> SourceResult:
|
|
1990
|
-
#
|
|
1991
|
-
#
|
|
1992
|
-
|
|
1990
|
+
# Real guard verification: surface the file-conditioned blocking learnings
|
|
1991
|
+
# for the requested files. Previously this returned fake evidence
|
|
1992
|
+
# (evidence_refs=["guard_context:requested"], result_count=1) WITHOUT any
|
|
1993
|
+
# check, which silently satisfied the critical-tier required-source / gap
|
|
1994
|
+
# gate for release/server/billing/legal areas. Never fake evidence again.
|
|
1995
|
+
files = [f.strip() for f in (request.files or "").split(",") if f.strip()]
|
|
1996
|
+
if not files:
|
|
1993
1997
|
return SourceResult(source="guard_context")
|
|
1998
|
+
try:
|
|
1999
|
+
from db import get_db
|
|
2000
|
+
from plugins.guard import _load_conditioned_learnings
|
|
2001
|
+
conn = get_db()
|
|
2002
|
+
conditioned = _load_conditioned_learnings(conn, files)
|
|
2003
|
+
except Exception:
|
|
2004
|
+
# Fail-closed: do NOT fake evidence; report that verification could not run.
|
|
2005
|
+
return SourceResult(
|
|
2006
|
+
source="guard_context",
|
|
2007
|
+
rendered="Guard verification could not run for: " + ", ".join(files),
|
|
2008
|
+
result_count=0,
|
|
2009
|
+
)
|
|
2010
|
+
refs: list[str] = []
|
|
2011
|
+
lines: list[str] = []
|
|
2012
|
+
for filepath, entries in conditioned.items():
|
|
2013
|
+
for entry in entries:
|
|
2014
|
+
refs.append(f"learning:{entry.get('id')}")
|
|
2015
|
+
lines.append(
|
|
2016
|
+
f"- [{entry.get('priority', 'medium')}] {entry.get('title', '')} (applies_to {filepath})"
|
|
2017
|
+
)
|
|
2018
|
+
if lines:
|
|
2019
|
+
return SourceResult(
|
|
2020
|
+
source="guard_context",
|
|
2021
|
+
rendered="Blocking/file-conditioned learnings:\n" + "\n".join(lines),
|
|
2022
|
+
evidence_refs=refs,
|
|
2023
|
+
result_count=len(refs),
|
|
2024
|
+
)
|
|
2025
|
+
# Guard ran and found nothing blocking — a real verified-clean result.
|
|
1994
2026
|
return SourceResult(
|
|
1995
2027
|
source="guard_context",
|
|
1996
|
-
rendered=
|
|
1997
|
-
|
|
1998
|
-
|
|
2028
|
+
rendered="Guard verified: no blocking file-conditioned learnings for "
|
|
2029
|
+
+ ", ".join(files),
|
|
2030
|
+
evidence_refs=["guard_context:verified_clean"],
|
|
2031
|
+
result_count=0,
|
|
1999
2032
|
)
|
|
2000
2033
|
|
|
2001
2034
|
|
package/src/requirements.txt
CHANGED
|
@@ -17,15 +17,19 @@ anthropic>=0.80.0
|
|
|
17
17
|
openai>=2.20.0
|
|
18
18
|
|
|
19
19
|
# Embedding model (optional but recommended for cognitive features).
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
20
|
+
# fastembed is hard-pinned (==) so the offline wheel bundle is reproducible; it is
|
|
21
|
+
# pure-python (py3-none-any) so the pin holds on every platform. >=0.8.0 was the
|
|
22
|
+
# floor (older releases need Python <3.12 and pip iterates each obsolete version for
|
|
23
|
+
# ~10 min on Ubuntu 24.04 before finding a compatible one, verified during Win11
|
|
24
|
+
# clean install bootstrap); 0.8.0 is the bundled version.
|
|
25
|
+
#
|
|
26
|
+
# onnxruntime (fastembed's native transitive dep) is deliberately NOT hard-pinned:
|
|
27
|
+
# its wheels are platform/glibc-specific — e.g. 1.26 only ships for manylinux_2_28,
|
|
28
|
+
# while the offline bundle also targets manylinux_2_17/2014 (which cap at 1.19.2).
|
|
29
|
+
# A `==` pin can't be satisfied for every target and breaks BOTH the cross-platform
|
|
30
|
+
# wheel bundle and installs on older Linux. Let fastembed/pip resolve the best
|
|
31
|
+
# onnxruntime wheel per platform.
|
|
27
32
|
fastembed==0.8.0
|
|
28
|
-
onnxruntime==1.26.0
|
|
29
33
|
|
|
30
34
|
# Local Context Layer — document parsers (REQUIRED for the local memory index).
|
|
31
35
|
# extractors.py imports these lazily; without them a clean bundle silently indexes
|
package/src/tools_sessions.py
CHANGED
|
@@ -317,7 +317,7 @@ def _session_portability_bundle(sid: str = "") -> dict:
|
|
|
317
317
|
dict(row) for row in conn.execute(
|
|
318
318
|
"""SELECT run_id, goal_id, goal, workflow_kind, status, priority, next_action, current_step_key, updated_at
|
|
319
319
|
FROM workflow_runs
|
|
320
|
-
WHERE session_id = ? AND status IN ('open', 'running', 'blocked', '
|
|
320
|
+
WHERE session_id = ? AND status IN ('open', 'running', 'blocked', 'waiting_approval')
|
|
321
321
|
ORDER BY updated_at DESC
|
|
322
322
|
LIMIT 10""",
|
|
323
323
|
(session_id,),
|