superlocalmemory 3.8.13 → 4.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/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- package/src/superlocalmemory/storage/access_control.py +0 -182
|
@@ -54,7 +54,9 @@ from superlocalmemory.evolution.model_selection import (
|
|
|
54
54
|
|
|
55
55
|
logger = logging.getLogger(__name__)
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
# Resolve the quarantine location under the selected runtime data root so a
|
|
58
|
+
# custom SLM_DATA_DIR is honored rather than always writing under the home dir.
|
|
59
|
+
EVOLVED_SKILLS_DIR = canonical_data_root() / "quarantine" / "skills"
|
|
58
60
|
|
|
59
61
|
# Model aliasing + per-step selection live in ``model_selection`` — a single
|
|
60
62
|
# source of truth shared with the config layer. ``_MODEL_ALIASES`` and
|
|
@@ -117,6 +119,8 @@ class SkillEvolver:
|
|
|
117
119
|
*,
|
|
118
120
|
profile_id: str = "default",
|
|
119
121
|
budget: EvolutionBudget | None = None,
|
|
122
|
+
audit_chain=None,
|
|
123
|
+
activator=None,
|
|
120
124
|
):
|
|
121
125
|
self._db_path = str(db_path)
|
|
122
126
|
self._store = EvolutionStore(db_path)
|
|
@@ -128,6 +132,8 @@ class SkillEvolver:
|
|
|
128
132
|
self._models = None
|
|
129
133
|
self._profile_id = profile_id
|
|
130
134
|
self._current_cycle_id: str | None = None
|
|
135
|
+
# Injectable activator for testing (CRIT-2).
|
|
136
|
+
self._activator = activator
|
|
131
137
|
|
|
132
138
|
# SB-3: SkillEvolver always holds a budget. Default one is rooted
|
|
133
139
|
# at ~/.superlocalmemory so production callers pick it up
|
|
@@ -141,12 +147,43 @@ class SkillEvolver:
|
|
|
141
147
|
)
|
|
142
148
|
self._budget = budget
|
|
143
149
|
|
|
150
|
+
# Phase 2 (Module 8): tamper-evident audit chain for evolution events.
|
|
151
|
+
# Never use ":memory:" in production — use file path next to db_path.
|
|
152
|
+
if audit_chain is None:
|
|
153
|
+
from superlocalmemory.compliance.audit import AuditChain
|
|
154
|
+
if self._db_path == ":memory:":
|
|
155
|
+
# In-memory DB is test-only; use in-memory audit too.
|
|
156
|
+
audit_chain = AuditChain(":memory:")
|
|
157
|
+
else:
|
|
158
|
+
audit_db = Path(self._db_path).parent / "audit.db"
|
|
159
|
+
audit_chain = AuditChain(audit_db)
|
|
160
|
+
self._audit = audit_chain
|
|
161
|
+
|
|
144
162
|
def _is_enabled(self) -> bool:
|
|
145
163
|
"""Check if evolution is enabled in config."""
|
|
146
164
|
if self._config and hasattr(self._config, "evolution"):
|
|
147
165
|
return self._config.evolution.enabled
|
|
148
166
|
return False
|
|
149
167
|
|
|
168
|
+
def _is_auto_approve(self) -> bool:
|
|
169
|
+
"""Check evolution.auto_approve config key (LLD Decision A2, default False).
|
|
170
|
+
|
|
171
|
+
Config key wins. Falls back to SLM_EVO_AUTO_APPROVE env var for CI.
|
|
172
|
+
"""
|
|
173
|
+
evo_cfg = getattr(self._config, "evolution", None)
|
|
174
|
+
if evo_cfg is not None and hasattr(evo_cfg, "auto_approve"):
|
|
175
|
+
return bool(evo_cfg.auto_approve)
|
|
176
|
+
return os.environ.get("SLM_EVO_AUTO_APPROVE", "").strip() in (
|
|
177
|
+
"1", "true", "True",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def _get_activator(self):
|
|
181
|
+
"""Return the SkillActivator instance (injectable for tests)."""
|
|
182
|
+
if self._activator is not None:
|
|
183
|
+
return self._activator
|
|
184
|
+
from superlocalmemory.evolution.skill_activator import SkillActivator
|
|
185
|
+
return SkillActivator()
|
|
186
|
+
|
|
150
187
|
def _get_backend(self) -> str:
|
|
151
188
|
"""Get or detect the LLM backend."""
|
|
152
189
|
if self._backend:
|
|
@@ -225,7 +262,10 @@ class SkillEvolver:
|
|
|
225
262
|
) -> dict:
|
|
226
263
|
"""Inner consolidation loop — runs under an open budget cycle."""
|
|
227
264
|
self._store.reset_cycle(profile_id)
|
|
228
|
-
results = {
|
|
265
|
+
results = {
|
|
266
|
+
"candidates": 0, "evolved": 0, "rejected": 0,
|
|
267
|
+
"skipped": 0, "quarantined": 0, "backend": backend,
|
|
268
|
+
}
|
|
229
269
|
|
|
230
270
|
# Prune recovered skills from anti-loop tracking
|
|
231
271
|
active_degraded = self._degradation.get_active_degraded(profile_id)
|
|
@@ -248,6 +288,8 @@ class SkillEvolver:
|
|
|
248
288
|
results["evolved"] += 1
|
|
249
289
|
elif outcome == "rejected":
|
|
250
290
|
results["rejected"] += 1
|
|
291
|
+
elif outcome == "quarantined":
|
|
292
|
+
results["quarantined"] += 1
|
|
251
293
|
else:
|
|
252
294
|
results["skipped"] += 1
|
|
253
295
|
|
|
@@ -332,10 +374,32 @@ class SkillEvolver:
|
|
|
332
374
|
def _process_candidate(
|
|
333
375
|
self, candidate: EvolutionCandidate, profile_id: str,
|
|
334
376
|
) -> str:
|
|
335
|
-
"""Process a single evolution candidate through the
|
|
336
|
-
|
|
337
|
-
|
|
377
|
+
"""Process a single evolution candidate through the governed state machine.
|
|
378
|
+
|
|
379
|
+
State machine (Phase 2):
|
|
380
|
+
CANDIDATE (insert_record once)
|
|
381
|
+
→ LLM confirm gate
|
|
382
|
+
fail → append_transition(CANDIDATE, REJECTED) → return "rejected"
|
|
383
|
+
→ Generate mutation
|
|
384
|
+
fail → append_transition(CANDIDATE, FAILED) → return "rejected"
|
|
385
|
+
→ Blind verify
|
|
386
|
+
fail → append_transition(CANDIDATE, REJECTED) → return "rejected"
|
|
387
|
+
pass → append_transition(CANDIDATE, VERIFIED_QUARANTINED)
|
|
388
|
+
→ emit audit "skill_promotion"
|
|
389
|
+
VERIFIED_QUARANTINED
|
|
390
|
+
→ no auto_approve → return "quarantined"
|
|
391
|
+
→ auto_approve → append_transition(VQ, APPROVED)
|
|
392
|
+
APPROVED
|
|
393
|
+
→ activate()
|
|
394
|
+
success → append_transition(APPROVED, ACTIVE) → emit "skill_activation"
|
|
395
|
+
→ return "evolved"
|
|
396
|
+
fail → append_transition(APPROVED, ROLLED_BACK)
|
|
397
|
+
→ emit "skill_activation_failed" → return "rejected"
|
|
398
|
+
|
|
399
|
+
Returns one of: "evolved", "rejected", "skipped", "quarantined"
|
|
338
400
|
"""
|
|
401
|
+
from superlocalmemory.evolution.skill_activator import SkillActivationError
|
|
402
|
+
|
|
339
403
|
now = datetime.now(timezone.utc).isoformat()
|
|
340
404
|
record_id = hashlib.sha256(
|
|
341
405
|
f"{candidate.skill_name}:{candidate.trigger.value}:{now}".encode(),
|
|
@@ -350,16 +414,20 @@ class SkillEvolver:
|
|
|
350
414
|
return "skipped"
|
|
351
415
|
|
|
352
416
|
if self._store.has_exceeded_attempts(candidate.skill_name, profile_id):
|
|
353
|
-
logger.info(
|
|
417
|
+
logger.info(
|
|
418
|
+
"Skill %s exceeded max attempts, flagging for review",
|
|
419
|
+
candidate.skill_name,
|
|
420
|
+
)
|
|
354
421
|
return "skipped"
|
|
355
422
|
|
|
356
|
-
# Mark as addressed (even
|
|
423
|
+
# Mark as addressed (even on reject — prevents repeated scans)
|
|
357
424
|
self._store.mark_addressed(candidate.skill_name, context_hash)
|
|
358
425
|
|
|
359
426
|
# Step 1: Read original skill content
|
|
360
427
|
original_content = self._read_skill_content(candidate.skill_name)
|
|
361
428
|
|
|
362
|
-
#
|
|
429
|
+
# CRIT-1: insert_record is called EXACTLY ONCE per candidate.
|
|
430
|
+
# All subsequent state changes use append_transition with this record_id.
|
|
363
431
|
record = EvolutionRecord(
|
|
364
432
|
id=record_id,
|
|
365
433
|
skill_name=candidate.skill_name,
|
|
@@ -371,56 +439,50 @@ class SkillEvolver:
|
|
|
371
439
|
original_content=original_content[:2000],
|
|
372
440
|
created_at=now,
|
|
373
441
|
)
|
|
374
|
-
self._store.
|
|
442
|
+
self._store.insert_record(record, profile_id)
|
|
375
443
|
|
|
376
|
-
# Step 2: LLM confirmation gate (
|
|
444
|
+
# Step 2: LLM confirmation gate (strict JSON parse)
|
|
377
445
|
confirmed = self._llm_confirm(candidate, original_content)
|
|
378
446
|
if not confirmed:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
completed_at=datetime.now(timezone.utc).isoformat(),
|
|
447
|
+
self._store.append_transition(
|
|
448
|
+
record_id, profile_id,
|
|
449
|
+
EvolutionStatus.CANDIDATE, EvolutionStatus.REJECTED,
|
|
450
|
+
reason="llm_confirmation_rejected",
|
|
384
451
|
)
|
|
385
|
-
self._store.save_record(record, profile_id)
|
|
386
452
|
return "rejected"
|
|
387
453
|
|
|
388
|
-
# Step 3: Generate mutation
|
|
454
|
+
# Step 3: Generate mutation
|
|
389
455
|
prompt = mutgen.build_mutation_prompt(candidate, original_content)
|
|
390
456
|
evolved_content = self._generate_mutation(prompt)
|
|
391
457
|
if not evolved_content:
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
completed_at=datetime.now(timezone.utc).isoformat(),
|
|
458
|
+
self._store.append_transition(
|
|
459
|
+
record_id, profile_id,
|
|
460
|
+
EvolutionStatus.CANDIDATE, EvolutionStatus.FAILED,
|
|
461
|
+
reason="mutation_generation_failed",
|
|
397
462
|
)
|
|
398
|
-
self._store.save_record(record, profile_id)
|
|
399
463
|
return "rejected"
|
|
400
464
|
|
|
401
|
-
# Step 4: Blind verification (
|
|
465
|
+
# Step 4: Blind verification (independent model from generator)
|
|
402
466
|
description = self._extract_description(evolved_content)
|
|
403
467
|
v_prompt = verifier.build_verification_prompt(
|
|
404
468
|
candidate.skill_name, description, evolved_content,
|
|
405
469
|
)
|
|
406
470
|
v_result = self._blind_verify(v_prompt)
|
|
407
471
|
if not v_result.passed:
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
evolved_content=evolved_content[:2000],
|
|
413
|
-
blind_verified=False,
|
|
414
|
-
completed_at=datetime.now(timezone.utc).isoformat(),
|
|
472
|
+
self._store.append_transition(
|
|
473
|
+
record_id, profile_id,
|
|
474
|
+
EvolutionStatus.CANDIDATE, EvolutionStatus.REJECTED,
|
|
475
|
+
reason=f"blind_verification: {v_result.reasoning}",
|
|
415
476
|
)
|
|
416
|
-
self._store.save_record(record, profile_id)
|
|
417
477
|
return "rejected"
|
|
418
478
|
|
|
419
|
-
# Step 5:
|
|
479
|
+
# Step 5: Write to quarantine (returns path + dir_name — CRIT-2)
|
|
420
480
|
diff = self._compute_diff(original_content, evolved_content)
|
|
421
|
-
skill_path = self._write_evolved_skill(
|
|
481
|
+
skill_path, dir_name = self._write_evolved_skill(
|
|
482
|
+
candidate, evolved_content, record_id,
|
|
483
|
+
)
|
|
422
484
|
|
|
423
|
-
#
|
|
485
|
+
# Compute generation from parent history
|
|
424
486
|
parent_history = self._store.get_skill_history(
|
|
425
487
|
candidate.skill_name, profile_id, limit=1,
|
|
426
488
|
)
|
|
@@ -430,25 +492,122 @@ class SkillEvolver:
|
|
|
430
492
|
else 0
|
|
431
493
|
)
|
|
432
494
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
495
|
+
# CANDIDATE → VERIFIED_QUARANTINED
|
|
496
|
+
self._store.append_transition(
|
|
497
|
+
record_id, profile_id,
|
|
498
|
+
EvolutionStatus.CANDIDATE, EvolutionStatus.VERIFIED_QUARANTINED,
|
|
499
|
+
reason="blind_verified",
|
|
500
|
+
metadata={
|
|
501
|
+
"quarantine_dir_name": dir_name,
|
|
502
|
+
"diff_summary": self._summarize_diff(diff),
|
|
503
|
+
"generation": parent_gen + 1,
|
|
504
|
+
},
|
|
442
505
|
)
|
|
443
|
-
self._store.save_record(record, profile_id)
|
|
444
506
|
self._store.record_evolution_attempt(profile_id)
|
|
445
507
|
|
|
508
|
+
# Phase 2 (Module 8): emit "skill_promotion" audit event
|
|
509
|
+
evolved_hash = hashlib.sha256(evolved_content.encode()).hexdigest()
|
|
510
|
+
self._audit.log(
|
|
511
|
+
operation="skill_promotion",
|
|
512
|
+
agent_id=self._profile_id,
|
|
513
|
+
profile_id=self._profile_id,
|
|
514
|
+
content_hash=evolved_hash,
|
|
515
|
+
metadata={
|
|
516
|
+
"record_id": record_id,
|
|
517
|
+
"skill_name": candidate.skill_name,
|
|
518
|
+
"evolution_type": candidate.evolution_type.value,
|
|
519
|
+
"trigger": candidate.trigger.value,
|
|
520
|
+
"quarantine_dir": dir_name,
|
|
521
|
+
},
|
|
522
|
+
)
|
|
523
|
+
|
|
446
524
|
logger.info(
|
|
447
|
-
"Evolved skill: %s (%s via %s) → %s",
|
|
525
|
+
"Evolved skill: %s (%s via %s) → quarantine %s",
|
|
448
526
|
candidate.skill_name, candidate.evolution_type.value,
|
|
449
527
|
candidate.trigger.value, skill_path,
|
|
450
528
|
)
|
|
451
|
-
|
|
529
|
+
|
|
530
|
+
# Check auto-approve — if off, stop here for human review
|
|
531
|
+
if not self._is_auto_approve():
|
|
532
|
+
return "quarantined"
|
|
533
|
+
|
|
534
|
+
# VERIFIED_QUARANTINED → APPROVED (auto)
|
|
535
|
+
actor_id = "auto"
|
|
536
|
+
self._store.append_transition(
|
|
537
|
+
record_id, profile_id,
|
|
538
|
+
EvolutionStatus.VERIFIED_QUARANTINED, EvolutionStatus.APPROVED,
|
|
539
|
+
actor_id=actor_id, reason="auto_approved",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# APPROVED → ACTIVE or ROLLED_BACK
|
|
543
|
+
activator = self._get_activator()
|
|
544
|
+
try:
|
|
545
|
+
activation_result = activator.activate(
|
|
546
|
+
candidate.skill_name, dir_name, actor_id=actor_id,
|
|
547
|
+
)
|
|
548
|
+
self._store.append_transition(
|
|
549
|
+
record_id, profile_id,
|
|
550
|
+
EvolutionStatus.APPROVED, EvolutionStatus.ACTIVE,
|
|
551
|
+
actor_id=actor_id,
|
|
552
|
+
metadata={"live_path": activation_result.get("live_path", "")},
|
|
553
|
+
)
|
|
554
|
+
self._audit.log(
|
|
555
|
+
operation="skill_activation",
|
|
556
|
+
agent_id=actor_id,
|
|
557
|
+
profile_id=self._profile_id,
|
|
558
|
+
content_hash=activation_result["content_hash"],
|
|
559
|
+
metadata={
|
|
560
|
+
"record_id": record_id,
|
|
561
|
+
"skill_name": candidate.skill_name,
|
|
562
|
+
"live_path": activation_result["live_path"],
|
|
563
|
+
"backup_path": activation_result.get("backup_path"),
|
|
564
|
+
},
|
|
565
|
+
)
|
|
566
|
+
return "evolved"
|
|
567
|
+
except (SkillActivationError, FileNotFoundError, ValueError, OSError) as exc:
|
|
568
|
+
# Audit P1-1: broaden the catch. A missing quarantine artifact raises
|
|
569
|
+
# FileNotFoundError and a path-traversal guard raises ValueError; both
|
|
570
|
+
# previously escaped after APPROVED was recorded, leaving the machine
|
|
571
|
+
# stuck at APPROVED with no terminal transition.
|
|
572
|
+
# Audit P1-2: actually PERFORM the rollback instead of only logging
|
|
573
|
+
# one. The terminal status is honest — ROLLED_BACK only if a restore
|
|
574
|
+
# happened, otherwise FAILED (nothing was written to live).
|
|
575
|
+
try:
|
|
576
|
+
rb = activator.rollback(candidate.skill_name)
|
|
577
|
+
rolled_back = bool(rb.get("rolled_back"))
|
|
578
|
+
except Exception as rb_exc: # rollback must never mask the original error
|
|
579
|
+
logger.error(
|
|
580
|
+
"skill rollback errored for %s: %s",
|
|
581
|
+
candidate.skill_name, rb_exc,
|
|
582
|
+
)
|
|
583
|
+
rolled_back = False
|
|
584
|
+
end_status = (
|
|
585
|
+
EvolutionStatus.ROLLED_BACK if rolled_back
|
|
586
|
+
else EvolutionStatus.FAILED
|
|
587
|
+
)
|
|
588
|
+
self._store.append_transition(
|
|
589
|
+
record_id, profile_id,
|
|
590
|
+
EvolutionStatus.APPROVED, end_status,
|
|
591
|
+
actor_id=actor_id, reason=str(exc),
|
|
592
|
+
)
|
|
593
|
+
self._audit.log(
|
|
594
|
+
operation="skill_activation_failed",
|
|
595
|
+
agent_id=actor_id,
|
|
596
|
+
profile_id=self._profile_id,
|
|
597
|
+
content_hash="",
|
|
598
|
+
metadata={
|
|
599
|
+
"record_id": record_id,
|
|
600
|
+
"skill_name": candidate.skill_name,
|
|
601
|
+
"error": str(exc),
|
|
602
|
+
"rolled_back": rolled_back,
|
|
603
|
+
},
|
|
604
|
+
)
|
|
605
|
+
logger.error(
|
|
606
|
+
"skill activation failed for %s: %s (%s)",
|
|
607
|
+
candidate.skill_name, exc,
|
|
608
|
+
"rolled back" if rolled_back else "failed, nothing to roll back",
|
|
609
|
+
)
|
|
610
|
+
return "rejected"
|
|
452
611
|
|
|
453
612
|
# ------------------------------------------------------------------
|
|
454
613
|
# LLM calls — single-line funnel through evolution.llm_dispatch
|
|
@@ -520,22 +679,76 @@ class SkillEvolver:
|
|
|
520
679
|
logger.debug("evolution dispatch failed: %s", exc)
|
|
521
680
|
return ""
|
|
522
681
|
|
|
523
|
-
def
|
|
524
|
-
"""LLM
|
|
525
|
-
|
|
682
|
+
def _parse_approval_decision(self, response: str) -> bool:
|
|
683
|
+
"""Parse LLM approval decision from structured JSON.
|
|
684
|
+
|
|
685
|
+
Accepts ONLY {"decision": "approve"} (case-insensitive value).
|
|
686
|
+
Rejects: "yes", "maybe", "approve" as free-text, partial JSON, or any
|
|
687
|
+
other value including {"decision": "yes"} or {"decision": "approved"}.
|
|
688
|
+
|
|
689
|
+
LLMs sometimes emit leading text before the JSON object — the parser
|
|
690
|
+
finds the first '{' and last '}' to extract the JSON fragment.
|
|
691
|
+
|
|
692
|
+
Returns True only if decision is exactly "approve" (case-insensitive).
|
|
693
|
+
Returns False for all other inputs including empty string, malformed
|
|
694
|
+
JSON, or any decision value other than "approve".
|
|
695
|
+
"""
|
|
696
|
+
if not response:
|
|
697
|
+
return False
|
|
698
|
+
stripped = response.strip()
|
|
699
|
+
# Find first JSON object — LLM may emit leading explanation text.
|
|
700
|
+
start = stripped.find("{")
|
|
701
|
+
end = stripped.rfind("}")
|
|
702
|
+
if start == -1 or end == -1 or end <= start:
|
|
703
|
+
return False
|
|
704
|
+
try:
|
|
705
|
+
obj = json.loads(stripped[start:end + 1])
|
|
706
|
+
except (json.JSONDecodeError, ValueError):
|
|
707
|
+
return False
|
|
708
|
+
if not isinstance(obj, dict):
|
|
709
|
+
return False
|
|
710
|
+
decision = obj.get("decision", "")
|
|
711
|
+
return isinstance(decision, str) and decision.lower() == "approve"
|
|
712
|
+
|
|
713
|
+
def _build_confirm_prompt(
|
|
714
|
+
self, candidate: EvolutionCandidate, original: str,
|
|
715
|
+
) -> str:
|
|
716
|
+
"""Build the strict-JSON LLM confirmation prompt."""
|
|
717
|
+
return (
|
|
526
718
|
f"A skill '{candidate.skill_name}' has effective score "
|
|
527
|
-
f"{candidate.effective_score:.0%} over
|
|
719
|
+
f"{candidate.effective_score:.0%} over "
|
|
720
|
+
f"{candidate.invocation_count} invocations.\n"
|
|
528
721
|
f"Evidence: {'; '.join(candidate.evidence)}\n\n"
|
|
529
|
-
f"Should this skill be evolved ({candidate.evolution_type.value})
|
|
530
|
-
f"
|
|
722
|
+
f"Should this skill be evolved ({candidate.evolution_type.value})?\n"
|
|
723
|
+
f"Respond ONLY with a JSON object. No other text.\n"
|
|
724
|
+
f'Approve: {{"decision": "approve"}}\n'
|
|
725
|
+
f'Reject: {{"decision": "reject", "reason": "one sentence"}}'
|
|
531
726
|
)
|
|
727
|
+
|
|
728
|
+
def _llm_confirm(self, candidate: EvolutionCandidate, original: str) -> bool:
|
|
729
|
+
"""LLM confirmation gate — strict structured decision parse (Module 3).
|
|
730
|
+
|
|
731
|
+
Replaces the legacy 'yes' in response.lower() check with strict JSON
|
|
732
|
+
parsing. Only {"decision": "approve"} returns True.
|
|
733
|
+
"""
|
|
734
|
+
prompt = self._build_confirm_prompt(candidate, original)
|
|
532
735
|
response = self._llm_call(
|
|
533
736
|
prompt, max_tokens=100, model=self._get_models().confirm,
|
|
534
737
|
)
|
|
535
738
|
if not response:
|
|
536
|
-
logger.warning(
|
|
739
|
+
logger.warning(
|
|
740
|
+
"LLM confirmation gate: empty response, "
|
|
741
|
+
"skipping evolution for %s",
|
|
742
|
+
candidate.skill_name,
|
|
743
|
+
)
|
|
537
744
|
return False # Fail-closed: no LLM = no evolution
|
|
538
|
-
|
|
745
|
+
approved = self._parse_approval_decision(response)
|
|
746
|
+
if not approved:
|
|
747
|
+
logger.info(
|
|
748
|
+
"LLM confirmation gate rejected %s (response: %.80r)",
|
|
749
|
+
candidate.skill_name, response,
|
|
750
|
+
)
|
|
751
|
+
return approved
|
|
539
752
|
|
|
540
753
|
def _generate_mutation(self, prompt: str) -> Optional[str]:
|
|
541
754
|
"""Generate evolved SKILL.md via the configured mutation model.
|
|
@@ -619,8 +832,16 @@ class SkillEvolver:
|
|
|
619
832
|
candidate: EvolutionCandidate,
|
|
620
833
|
content: str,
|
|
621
834
|
record_id: str,
|
|
622
|
-
) -> Path:
|
|
623
|
-
"""Write evolved SKILL.md to
|
|
835
|
+
) -> tuple[Path, str]:
|
|
836
|
+
"""Write evolved SKILL.md to the quarantine directory for review.
|
|
837
|
+
|
|
838
|
+
Returns (skill_path, dir_name) — both are needed by _process_candidate:
|
|
839
|
+
- skill_path: the Path to the written SKILL.md (for logging)
|
|
840
|
+
- dir_name: the quarantine subdirectory name (CRIT-2; passed to SkillActivator)
|
|
841
|
+
|
|
842
|
+
The dir_name is DISTINCT from candidate.skill_name. It is the sanitized,
|
|
843
|
+
versioned subdirectory inside the quarantine root, e.g. "brainstorming-vabc12".
|
|
844
|
+
"""
|
|
624
845
|
EVOLVED_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
625
846
|
|
|
626
847
|
# Build directory name. skill_name derives from a behavioral-assertion
|
|
@@ -648,20 +869,21 @@ class SkillEvolver:
|
|
|
648
869
|
skill_path = skill_dir / "SKILL.md"
|
|
649
870
|
skill_path.write_text(content, encoding="utf-8")
|
|
650
871
|
|
|
651
|
-
# Write metadata sidecar
|
|
872
|
+
# Write metadata sidecar (CRIT-2: includes quarantine_dir_name for recovery)
|
|
652
873
|
meta = {
|
|
653
874
|
"skill_id": dir_name,
|
|
654
875
|
"parent_skill_id": candidate.skill_name,
|
|
655
876
|
"evolution_type": candidate.evolution_type.value,
|
|
656
877
|
"trigger": candidate.trigger.value,
|
|
657
878
|
"record_id": record_id,
|
|
879
|
+
"quarantine_dir_name": dir_name,
|
|
658
880
|
"evidence": list(candidate.evidence),
|
|
659
881
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
660
882
|
}
|
|
661
883
|
meta_path = skill_dir / ".skill_meta.json"
|
|
662
884
|
meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8")
|
|
663
885
|
|
|
664
|
-
return skill_path
|
|
886
|
+
return skill_path, dir_name
|
|
665
887
|
|
|
666
888
|
# ------------------------------------------------------------------
|
|
667
889
|
# Utilities
|
|
@@ -32,14 +32,30 @@ class TriggerType(str, Enum):
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
class EvolutionStatus(str, Enum):
|
|
35
|
-
"""Pipeline status.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
"""Pipeline status.
|
|
36
|
+
|
|
37
|
+
State machine (Phase 2):
|
|
38
|
+
CANDIDATE → VERIFIED_QUARANTINED → APPROVED → ACTIVE
|
|
39
|
+
↓ ↓
|
|
40
|
+
REJECTED ROLLED_BACK
|
|
41
|
+
|
|
42
|
+
Legacy values (CONFIRMED, MUTATED, VERIFIED, PROMOTED) are retained for
|
|
43
|
+
DB-row compatibility with pre-Phase-2 records. New code uses the states
|
|
44
|
+
above. PROMOTED is an alias for what is now VERIFIED_QUARANTINED in the
|
|
45
|
+
live-in-quarantine sense; new records use VERIFIED_QUARANTINED explicitly.
|
|
46
|
+
"""
|
|
47
|
+
CANDIDATE = "candidate" # Detected, not yet processed
|
|
48
|
+
CONFIRMED = "confirmed" # Legacy: LLM gate passed
|
|
49
|
+
MUTATED = "mutated" # Legacy: new SKILL.md generated
|
|
50
|
+
VERIFIED = "verified" # Legacy: blind verify passed
|
|
51
|
+
PROMOTED = "promoted" # Legacy alias for quarantined
|
|
52
|
+
REJECTED = "rejected" # Failed gate or verification
|
|
53
|
+
FAILED = "failed" # Error during evolution
|
|
54
|
+
# --- Phase 2 states ---
|
|
55
|
+
VERIFIED_QUARANTINED = "verified_quarantined" # Passed blind verify, in quarantine
|
|
56
|
+
APPROVED = "approved" # Human/policy approved; ready to activate
|
|
57
|
+
ACTIVE = "active" # Live in skill directory
|
|
58
|
+
ROLLED_BACK = "rolled_back" # Activation reverted; prior artifact restored
|
|
43
59
|
|
|
44
60
|
|
|
45
61
|
@dataclass(frozen=True)
|
|
@@ -74,6 +90,12 @@ class EvolutionRecord:
|
|
|
74
90
|
rejection_reason: str = ""
|
|
75
91
|
created_at: str = ""
|
|
76
92
|
completed_at: str = ""
|
|
93
|
+
# Phase 2 (CRIT-2): the sanitized directory name inside the quarantine root,
|
|
94
|
+
# e.g. "brainstorming-vabc12". Distinct from skill_name ("brainstorming").
|
|
95
|
+
# Stored in-memory; not persisted to skill_evolution_log (no schema column).
|
|
96
|
+
# The activator reads this to locate the artifact; it is also stored in the
|
|
97
|
+
# VERIFIED_QUARANTINED transition metadata for auditability.
|
|
98
|
+
quarantine_dir_name: str = ""
|
|
77
99
|
|
|
78
100
|
|
|
79
101
|
@dataclass(frozen=True)
|
|
@@ -17,6 +17,7 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
import importlib.util
|
|
20
21
|
import json
|
|
21
22
|
import logging
|
|
22
23
|
import sqlite3
|
|
@@ -28,13 +29,17 @@ from superlocalmemory.storage.logical_edges import iter_logical_edges
|
|
|
28
29
|
|
|
29
30
|
logger = logging.getLogger(__name__)
|
|
30
31
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
# Availability via find_spec only — never import pycozo at module load.
|
|
33
|
+
# Native bindings belong to the same risk class as lancedb (background
|
|
34
|
+
# runtimes / GC races). Real import is deferred to CozoDBGraphBackend.__init__.
|
|
35
|
+
def _pycozo_spec_present() -> bool:
|
|
36
|
+
try:
|
|
37
|
+
return importlib.util.find_spec("pycozo") is not None
|
|
38
|
+
except Exception:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_COZO_AVAILABLE: bool = _pycozo_spec_present()
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
class CozoDBError(Exception):
|
|
@@ -121,14 +126,17 @@ class CozoDBGraphBackend:
|
|
|
121
126
|
"""
|
|
122
127
|
|
|
123
128
|
def __init__(self, db_path: str) -> None:
|
|
124
|
-
if not
|
|
129
|
+
if not _pycozo_spec_present():
|
|
125
130
|
raise CozoDBNotAvailable(
|
|
126
131
|
"CozoDB not installed. Run: pip install superlocalmemory[cozo]"
|
|
127
132
|
)
|
|
133
|
+
# Lazy import: only construct-time loads the native binding.
|
|
134
|
+
from pycozo.client import Client as _CozoClient # noqa: PLC0415
|
|
135
|
+
|
|
128
136
|
path = Path(db_path)
|
|
129
137
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
130
138
|
self._db_path = str(path)
|
|
131
|
-
client = _CozoClient("rocksdb", self._db_path, dataframe=False)
|
|
139
|
+
client = _CozoClient("rocksdb", self._db_path, dataframe=False)
|
|
132
140
|
self._db = _CozoClientAdapter(client)
|
|
133
141
|
self._shadow_checks = 0
|
|
134
142
|
self._shadow_mismatches = 0
|
|
@@ -516,5 +516,6 @@ class AutoInvoker:
|
|
|
516
516
|
self._config.profile_id,
|
|
517
517
|
)
|
|
518
518
|
except Exception as exc:
|
|
519
|
-
|
|
519
|
+
# warning — not debug — so unexpected failures surface in prod logs
|
|
520
|
+
logger.warning("Soft prompt injection failed: %s", exc)
|
|
520
521
|
return ""
|