@smilintux/skmemory 0.7.2 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.github/workflows/ci.yml +4 -4
  2. package/.github/workflows/publish.yml +4 -5
  3. package/ARCHITECTURE.md +298 -0
  4. package/CHANGELOG.md +27 -1
  5. package/README.md +6 -0
  6. package/examples/stignore-agent.example +59 -0
  7. package/examples/stignore-root.example +62 -0
  8. package/openclaw-plugin/package.json +2 -1
  9. package/openclaw-plugin/src/index.js +527 -230
  10. package/package.json +1 -1
  11. package/pyproject.toml +5 -2
  12. package/scripts/dream-rescue.py +179 -0
  13. package/scripts/memory-cleanup.py +313 -0
  14. package/scripts/recover-missing.py +180 -0
  15. package/scripts/skcapstone-backup.sh +44 -0
  16. package/seeds/cloud9-lumina.seed.json +6 -4
  17. package/seeds/cloud9-opus.seed.json +6 -4
  18. package/seeds/courage.seed.json +9 -2
  19. package/seeds/curiosity.seed.json +9 -2
  20. package/seeds/grief.seed.json +9 -2
  21. package/seeds/joy.seed.json +9 -2
  22. package/seeds/love.seed.json +9 -2
  23. package/seeds/lumina-cloud9-breakthrough.seed.json +7 -5
  24. package/seeds/lumina-cloud9-python-pypi.seed.json +9 -7
  25. package/seeds/lumina-kingdom-founding.seed.json +9 -7
  26. package/seeds/lumina-pma-signed.seed.json +8 -6
  27. package/seeds/lumina-singular-achievement.seed.json +8 -6
  28. package/seeds/lumina-skcapstone-conscious.seed.json +7 -5
  29. package/seeds/plant-lumina-seeds.py +2 -2
  30. package/seeds/skcapstone-lumina-merge.seed.json +12 -3
  31. package/seeds/sovereignty.seed.json +9 -2
  32. package/seeds/trust.seed.json +9 -2
  33. package/skmemory/__init__.py +16 -13
  34. package/skmemory/agents.py +10 -10
  35. package/skmemory/ai_client.py +10 -21
  36. package/skmemory/anchor.py +5 -9
  37. package/skmemory/audience.py +278 -0
  38. package/skmemory/backends/__init__.py +1 -1
  39. package/skmemory/backends/base.py +3 -4
  40. package/skmemory/backends/file_backend.py +18 -13
  41. package/skmemory/backends/skgraph_backend.py +7 -19
  42. package/skmemory/backends/skvector_backend.py +7 -18
  43. package/skmemory/backends/sqlite_backend.py +115 -32
  44. package/skmemory/backends/vaulted_backend.py +7 -9
  45. package/skmemory/cli.py +146 -78
  46. package/skmemory/config.py +11 -13
  47. package/skmemory/context_loader.py +21 -23
  48. package/skmemory/data/audience_config.json +60 -0
  49. package/skmemory/endpoint_selector.py +36 -31
  50. package/skmemory/febs.py +225 -0
  51. package/skmemory/fortress.py +30 -40
  52. package/skmemory/hooks/__init__.py +18 -0
  53. package/skmemory/hooks/post-compact-reinject.sh +35 -0
  54. package/skmemory/hooks/pre-compact-save.sh +81 -0
  55. package/skmemory/hooks/session-end-save.sh +103 -0
  56. package/skmemory/hooks/session-start-ritual.sh +104 -0
  57. package/skmemory/hooks/stop-checkpoint.sh +59 -0
  58. package/skmemory/importers/telegram.py +42 -13
  59. package/skmemory/importers/telegram_api.py +152 -60
  60. package/skmemory/journal.py +3 -7
  61. package/skmemory/lovenote.py +4 -11
  62. package/skmemory/mcp_server.py +182 -29
  63. package/skmemory/models.py +10 -8
  64. package/skmemory/openclaw.py +14 -22
  65. package/skmemory/post_install.py +86 -0
  66. package/skmemory/predictive.py +13 -9
  67. package/skmemory/promotion.py +48 -24
  68. package/skmemory/quadrants.py +100 -24
  69. package/skmemory/register.py +144 -18
  70. package/skmemory/register_mcp.py +1 -2
  71. package/skmemory/ritual.py +104 -13
  72. package/skmemory/seeds.py +21 -26
  73. package/skmemory/setup_wizard.py +40 -52
  74. package/skmemory/sharing.py +11 -5
  75. package/skmemory/soul.py +29 -10
  76. package/skmemory/steelman.py +43 -17
  77. package/skmemory/store.py +152 -30
  78. package/skmemory/synthesis.py +634 -0
  79. package/skmemory/vault.py +2 -5
  80. package/tests/conftest.py +46 -0
  81. package/tests/integration/conftest.py +6 -6
  82. package/tests/integration/test_cross_backend.py +4 -9
  83. package/tests/integration/test_skgraph_live.py +3 -7
  84. package/tests/integration/test_skvector_live.py +1 -4
  85. package/tests/test_ai_client.py +1 -4
  86. package/tests/test_audience.py +233 -0
  87. package/tests/test_backup_rotation.py +5 -14
  88. package/tests/test_endpoint_selector.py +101 -63
  89. package/tests/test_export_import.py +4 -10
  90. package/tests/test_file_backend.py +0 -1
  91. package/tests/test_fortress.py +6 -5
  92. package/tests/test_fortress_hardening.py +13 -16
  93. package/tests/test_openclaw.py +1 -4
  94. package/tests/test_predictive.py +1 -1
  95. package/tests/test_promotion.py +10 -3
  96. package/tests/test_quadrants.py +11 -5
  97. package/tests/test_ritual.py +18 -14
  98. package/tests/test_seeds.py +4 -10
  99. package/tests/test_setup.py +203 -88
  100. package/tests/test_sharing.py +15 -8
  101. package/tests/test_skgraph_backend.py +22 -29
  102. package/tests/test_skvector_backend.py +2 -2
  103. package/tests/test_soul.py +1 -3
  104. package/tests/test_sqlite_backend.py +8 -17
  105. package/tests/test_steelman.py +2 -3
  106. package/tests/test_store.py +0 -2
  107. package/tests/test_store_graph_integration.py +2 -2
  108. package/tests/test_synthesis.py +275 -0
  109. package/tests/test_telegram_import.py +39 -15
  110. package/tests/test_vault.py +4 -3
  111. package/openclaw-plugin/src/index.ts +0 -255
@@ -3,22 +3,17 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- import socket
7
6
  import time
8
7
  from datetime import datetime, timezone
9
8
  from pathlib import Path
10
- from typing import Optional
11
9
  from unittest.mock import MagicMock, patch
12
10
 
13
- import pytest
14
-
15
11
  from skmemory.endpoint_selector import (
16
12
  Endpoint,
17
13
  EndpointSelector,
18
14
  RoutingConfig,
19
15
  )
20
16
 
21
-
22
17
  # ---------------------------------------------------------------------------
23
18
  # Endpoint model tests
24
19
  # ---------------------------------------------------------------------------
@@ -107,7 +102,7 @@ class TestLatencyProbing:
107
102
 
108
103
  with patch(
109
104
  "skmemory.endpoint_selector.socket.create_connection",
110
- side_effect=socket.timeout("timed out"),
105
+ side_effect=TimeoutError("timed out"),
111
106
  ):
112
107
  result = selector.probe_endpoint(ep)
113
108
 
@@ -179,7 +174,9 @@ class TestLatencyProbing:
179
174
  selector = EndpointSelector(skgraph_endpoints=[ep])
180
175
 
181
176
  mock_sock = MagicMock()
182
- with patch("skmemory.endpoint_selector.socket.create_connection", return_value=mock_sock) as mock_connect:
177
+ with patch(
178
+ "skmemory.endpoint_selector.socket.create_connection", return_value=mock_sock
179
+ ) as mock_connect:
183
180
  selector.probe_endpoint(ep)
184
181
 
185
182
  args = mock_connect.call_args[0]
@@ -191,7 +188,9 @@ class TestLatencyProbing:
191
188
  selector = EndpointSelector(skvector_endpoints=[ep])
192
189
 
193
190
  mock_sock = MagicMock()
194
- with patch("skmemory.endpoint_selector.socket.create_connection", return_value=mock_sock) as mock_connect:
191
+ with patch(
192
+ "skmemory.endpoint_selector.socket.create_connection", return_value=mock_sock
193
+ ) as mock_connect:
195
194
  selector.probe_endpoint(ep)
196
195
 
197
196
  args = mock_connect.call_args[0]
@@ -467,14 +466,19 @@ class TestHeartbeatDiscovery:
467
466
  """Discovers Qdrant endpoint from heartbeat file."""
468
467
  hb_dir = tmp_path / "heartbeats"
469
468
  hb_dir.mkdir()
470
- (hb_dir / "peer.json").write_text(json.dumps({
471
- "agent_name": "peer",
472
- "hostname": "peer-host",
473
- "tailscale_ip": "100.64.0.5",
474
- "services": [
475
- {"name": "skvector", "port": 6333, "protocol": "http"},
476
- ],
477
- }), encoding="utf-8")
469
+ (hb_dir / "peer.json").write_text(
470
+ json.dumps(
471
+ {
472
+ "agent_name": "peer",
473
+ "hostname": "peer-host",
474
+ "tailscale_ip": "100.64.0.5",
475
+ "services": [
476
+ {"name": "skvector", "port": 6333, "protocol": "http"},
477
+ ],
478
+ }
479
+ ),
480
+ encoding="utf-8",
481
+ )
478
482
 
479
483
  selector = EndpointSelector()
480
484
  selector.discover_from_heartbeats(hb_dir)
@@ -487,13 +491,18 @@ class TestHeartbeatDiscovery:
487
491
  """Discovers FalkorDB endpoint from heartbeat file."""
488
492
  hb_dir = tmp_path / "heartbeats"
489
493
  hb_dir.mkdir()
490
- (hb_dir / "peer.json").write_text(json.dumps({
491
- "agent_name": "peer",
492
- "hostname": "peer-host",
493
- "services": [
494
- {"name": "skgraph", "port": 6379, "protocol": "redis"},
495
- ],
496
- }), encoding="utf-8")
494
+ (hb_dir / "peer.json").write_text(
495
+ json.dumps(
496
+ {
497
+ "agent_name": "peer",
498
+ "hostname": "peer-host",
499
+ "services": [
500
+ {"name": "skgraph", "port": 6379, "protocol": "redis"},
501
+ ],
502
+ }
503
+ ),
504
+ encoding="utf-8",
505
+ )
497
506
 
498
507
  selector = EndpointSelector()
499
508
  selector.discover_from_heartbeats(hb_dir)
@@ -505,14 +514,19 @@ class TestHeartbeatDiscovery:
505
514
  """Tailscale IP is preferred over hostname for URL construction."""
506
515
  hb_dir = tmp_path / "heartbeats"
507
516
  hb_dir.mkdir()
508
- (hb_dir / "peer.json").write_text(json.dumps({
509
- "agent_name": "peer",
510
- "hostname": "peer-host",
511
- "tailscale_ip": "100.64.0.10",
512
- "services": [
513
- {"name": "skvector", "port": 6333, "protocol": "http"},
514
- ],
515
- }), encoding="utf-8")
517
+ (hb_dir / "peer.json").write_text(
518
+ json.dumps(
519
+ {
520
+ "agent_name": "peer",
521
+ "hostname": "peer-host",
522
+ "tailscale_ip": "100.64.0.10",
523
+ "services": [
524
+ {"name": "skvector", "port": 6333, "protocol": "http"},
525
+ ],
526
+ }
527
+ ),
528
+ encoding="utf-8",
529
+ )
516
530
 
517
531
  selector = EndpointSelector()
518
532
  selector.discover_from_heartbeats(hb_dir)
@@ -523,14 +537,19 @@ class TestHeartbeatDiscovery:
523
537
  """Discovery doesn't duplicate existing config endpoints."""
524
538
  hb_dir = tmp_path / "heartbeats"
525
539
  hb_dir.mkdir()
526
- (hb_dir / "peer.json").write_text(json.dumps({
527
- "agent_name": "peer",
528
- "hostname": "peer-host",
529
- "tailscale_ip": "100.64.0.5",
530
- "services": [
531
- {"name": "skvector", "port": 6333, "protocol": "http"},
532
- ],
533
- }), encoding="utf-8")
540
+ (hb_dir / "peer.json").write_text(
541
+ json.dumps(
542
+ {
543
+ "agent_name": "peer",
544
+ "hostname": "peer-host",
545
+ "tailscale_ip": "100.64.0.5",
546
+ "services": [
547
+ {"name": "skvector", "port": 6333, "protocol": "http"},
548
+ ],
549
+ }
550
+ ),
551
+ encoding="utf-8",
552
+ )
534
553
 
535
554
  existing = Endpoint(url="http://100.64.0.5:6333")
536
555
  selector = EndpointSelector(skvector_endpoints=[existing])
@@ -571,12 +590,17 @@ class TestHeartbeatDiscovery:
571
590
  """Heartbeat without hostname or tailscale_ip is skipped."""
572
591
  hb_dir = tmp_path / "heartbeats"
573
592
  hb_dir.mkdir()
574
- (hb_dir / "peer.json").write_text(json.dumps({
575
- "agent_name": "peer",
576
- "services": [
577
- {"name": "skvector", "port": 6333},
578
- ],
579
- }), encoding="utf-8")
593
+ (hb_dir / "peer.json").write_text(
594
+ json.dumps(
595
+ {
596
+ "agent_name": "peer",
597
+ "services": [
598
+ {"name": "skvector", "port": 6333},
599
+ ],
600
+ }
601
+ ),
602
+ encoding="utf-8",
603
+ )
580
604
 
581
605
  selector = EndpointSelector()
582
606
  selector.discover_from_heartbeats(hb_dir)
@@ -587,14 +611,19 @@ class TestHeartbeatDiscovery:
587
611
  """Discovers both Qdrant and FalkorDB from one heartbeat."""
588
612
  hb_dir = tmp_path / "heartbeats"
589
613
  hb_dir.mkdir()
590
- (hb_dir / "peer.json").write_text(json.dumps({
591
- "agent_name": "peer",
592
- "hostname": "peer-host",
593
- "services": [
594
- {"name": "skvector", "port": 6333, "protocol": "http"},
595
- {"name": "skgraph", "port": 6379, "protocol": "redis"},
596
- ],
597
- }), encoding="utf-8")
614
+ (hb_dir / "peer.json").write_text(
615
+ json.dumps(
616
+ {
617
+ "agent_name": "peer",
618
+ "hostname": "peer-host",
619
+ "services": [
620
+ {"name": "skvector", "port": 6333, "protocol": "http"},
621
+ {"name": "skgraph", "port": 6379, "protocol": "redis"},
622
+ ],
623
+ }
624
+ ),
625
+ encoding="utf-8",
626
+ )
598
627
 
599
628
  selector = EndpointSelector()
600
629
  selector.discover_from_heartbeats(hb_dir)
@@ -606,11 +635,16 @@ class TestHeartbeatDiscovery:
606
635
  """Files ending in .tmp are skipped."""
607
636
  hb_dir = tmp_path / "heartbeats"
608
637
  hb_dir.mkdir()
609
- (hb_dir / "peer.json.tmp").write_text(json.dumps({
610
- "agent_name": "peer",
611
- "hostname": "peer-host",
612
- "services": [{"name": "skvector", "port": 6333}],
613
- }), encoding="utf-8")
638
+ (hb_dir / "peer.json.tmp").write_text(
639
+ json.dumps(
640
+ {
641
+ "agent_name": "peer",
642
+ "hostname": "peer-host",
643
+ "services": [{"name": "skvector", "port": 6333}],
644
+ }
645
+ ),
646
+ encoding="utf-8",
647
+ )
614
648
 
615
649
  selector = EndpointSelector()
616
650
  selector.discover_from_heartbeats(hb_dir)
@@ -737,7 +771,9 @@ class TestMaybeProbe:
737
771
  )
738
772
 
739
773
  mock_sock = MagicMock()
740
- with patch("skmemory.endpoint_selector.socket.create_connection", return_value=mock_sock) as mock_conn:
774
+ with patch(
775
+ "skmemory.endpoint_selector.socket.create_connection", return_value=mock_sock
776
+ ) as mock_conn:
741
777
  selector.select_skvector()
742
778
 
743
779
  mock_conn.assert_called_once()
@@ -751,7 +787,9 @@ class TestMaybeProbe:
751
787
  )
752
788
 
753
789
  mock_sock = MagicMock()
754
- with patch("skmemory.endpoint_selector.socket.create_connection", return_value=mock_sock) as mock_conn:
790
+ with patch(
791
+ "skmemory.endpoint_selector.socket.create_connection", return_value=mock_sock
792
+ ) as mock_conn:
755
793
  selector.select_skvector()
756
794
  selector.select_skvector()
757
795
 
@@ -769,7 +807,7 @@ class TestConfigIntegration:
769
807
 
770
808
  def test_single_url_becomes_endpoint(self) -> None:
771
809
  """Single URL is promoted to endpoint list."""
772
- from skmemory.config import build_endpoint_list, EndpointConfig
810
+ from skmemory.config import build_endpoint_list
773
811
 
774
812
  result = build_endpoint_list("http://localhost:6333", [])
775
813
  assert len(result) == 1
@@ -778,7 +816,7 @@ class TestConfigIntegration:
778
816
 
779
817
  def test_endpoints_list_takes_precedence(self) -> None:
780
818
  """Endpoint list is used when present."""
781
- from skmemory.config import build_endpoint_list, EndpointConfig
819
+ from skmemory.config import EndpointConfig, build_endpoint_list
782
820
 
783
821
  eps = [EndpointConfig(url="http://a:6333"), EndpointConfig(url="http://b:6333")]
784
822
  result = build_endpoint_list("http://c:6333", eps)
@@ -787,7 +825,7 @@ class TestConfigIntegration:
787
825
 
788
826
  def test_no_duplicate_when_url_in_list(self) -> None:
789
827
  """Single URL not duplicated if already in endpoints list."""
790
- from skmemory.config import build_endpoint_list, EndpointConfig
828
+ from skmemory.config import EndpointConfig, build_endpoint_list
791
829
 
792
830
  eps = [EndpointConfig(url="http://a:6333")]
793
831
  result = build_endpoint_list("http://a:6333", eps)
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  import pytest
7
7
 
8
8
  from skmemory.backends.sqlite_backend import SQLiteBackend
9
- from skmemory.models import EmotionalSnapshot, Memory, MemoryLayer
9
+ from skmemory.models import EmotionalSnapshot, MemoryLayer
10
10
  from skmemory.store import MemoryStore
11
11
 
12
12
 
@@ -98,9 +98,7 @@ class TestImport:
98
98
  backup = tmp_path / "backup.json"
99
99
  self._create_backup(populated_store, backup)
100
100
 
101
- fresh_backend = SQLiteBackend(
102
- base_path=str(tmp_path / "fresh_memories")
103
- )
101
+ fresh_backend = SQLiteBackend(base_path=str(tmp_path / "fresh_memories"))
104
102
  fresh_store = MemoryStore(primary=fresh_backend)
105
103
 
106
104
  count = fresh_store.import_backup(str(backup))
@@ -114,9 +112,7 @@ class TestImport:
114
112
  data = json.loads(backup.read_text())
115
113
  first_id = data["memories"][0]["id"]
116
114
 
117
- fresh_backend = SQLiteBackend(
118
- base_path=str(tmp_path / "fresh_memories")
119
- )
115
+ fresh_backend = SQLiteBackend(base_path=str(tmp_path / "fresh_memories"))
120
116
  fresh_store = MemoryStore(primary=fresh_backend)
121
117
  fresh_store.import_backup(str(backup))
122
118
 
@@ -155,9 +151,7 @@ class TestRoundTrip:
155
151
  backup = tmp_path / "backup.json"
156
152
  populated_store.export_backup(str(backup))
157
153
 
158
- fresh_backend = SQLiteBackend(
159
- base_path=str(tmp_path / "fresh")
160
- )
154
+ fresh_backend = SQLiteBackend(base_path=str(tmp_path / "fresh"))
161
155
  fresh_store = MemoryStore(primary=fresh_backend)
162
156
  count = fresh_store.import_backup(str(backup))
163
157
  assert count == 5
@@ -1,7 +1,6 @@
1
1
  """Tests for the file-based storage backend."""
2
2
 
3
3
  import json
4
- import tempfile
5
4
  from pathlib import Path
6
5
 
7
6
  import pytest
@@ -7,19 +7,16 @@ All tests use in-memory/temp-path backends — no GPG required for basic tests.
7
7
  from __future__ import annotations
8
8
 
9
9
  import json
10
- import tempfile
11
- from pathlib import Path
12
10
 
13
11
  import pytest
14
12
 
15
13
  from skmemory.fortress import AuditLog, FortifiedMemoryStore, TamperAlert
16
- from skmemory.models import Memory, MemoryLayer
17
-
18
14
 
19
15
  # ---------------------------------------------------------------------------
20
16
  # Fixtures
21
17
  # ---------------------------------------------------------------------------
22
18
 
19
+
23
20
  @pytest.fixture
24
21
  def tmp_audit(tmp_path):
25
22
  """Return a temporary audit log."""
@@ -30,6 +27,7 @@ def tmp_audit(tmp_path):
30
27
  def fortress(tmp_path):
31
28
  """Return a FortifiedMemoryStore using a temp directory."""
32
29
  from skmemory.backends.sqlite_backend import SQLiteBackend
30
+
33
31
  backend = SQLiteBackend(base_path=str(tmp_path / "memories"))
34
32
  return FortifiedMemoryStore(
35
33
  primary=backend,
@@ -42,6 +40,7 @@ def fortress(tmp_path):
42
40
  # AuditLog tests
43
41
  # ---------------------------------------------------------------------------
44
42
 
43
+
45
44
  class TestAuditLog:
46
45
  def test_append_creates_file(self, tmp_audit, tmp_path):
47
46
  tmp_audit.append("store", "abc123", ok=True)
@@ -107,6 +106,7 @@ class TestAuditLog:
107
106
  # TamperAlert tests
108
107
  # ---------------------------------------------------------------------------
109
108
 
109
+
110
110
  class TestTamperAlert:
111
111
  def test_to_dict(self):
112
112
  alert = TamperAlert(
@@ -131,6 +131,7 @@ class TestTamperAlert:
131
131
  # FortifiedMemoryStore tests
132
132
  # ---------------------------------------------------------------------------
133
133
 
134
+
134
135
  class TestFortifiedMemoryStore:
135
136
  def test_snapshot_seals_memory(self, fortress):
136
137
  mem = fortress.snapshot("Test title", "Test content")
@@ -154,7 +155,7 @@ class TestFortifiedMemoryStore:
154
155
 
155
156
  # Tamper: directly modify the stored memory file
156
157
  # We need to find and corrupt the JSON
157
- from skmemory.backends.sqlite_backend import SQLiteBackend
158
+
158
159
  backend = fortress.primary
159
160
  # Load raw, mutate, save back bypassing seal
160
161
  raw = backend.load(mem.id)
@@ -11,19 +11,18 @@ Covers:
11
11
  from __future__ import annotations
12
12
 
13
13
  import json
14
- import tempfile
15
14
  from pathlib import Path
16
15
 
17
16
  import pytest
18
17
 
19
18
  try:
20
- from cryptography.hazmat.primitives.ciphers.aead import AESGCM
19
+ import cryptography.hazmat.primitives.ciphers.aead # noqa: F401
21
20
 
22
21
  CRYPTO_AVAILABLE = True
23
22
  except ImportError:
24
23
  CRYPTO_AVAILABLE = False
25
24
 
26
- from skmemory.vault import VAULT_HEADER, MemoryVault
25
+ from skmemory.vault import VAULT_HEADER
27
26
 
28
27
  pytestmark = pytest.mark.skipif(
29
28
  not CRYPTO_AVAILABLE,
@@ -246,7 +245,7 @@ class TestVaultedSQLiteBackend:
246
245
  class TestFortifiedMemoryStoreVault:
247
246
  def test_vault_passphrase_activates_encryption(self, fortress_vaulted, tmp_path):
248
247
  """FortifiedMemoryStore with vault_passphrase should encrypt files."""
249
- mem = fortress_vaulted.snapshot("Vaulted Title", "Encrypted content")
248
+ fortress_vaulted.snapshot("Vaulted Title", "Encrypted content")
250
249
 
251
250
  # Find the physical file
252
251
  base = fortress_vaulted.primary.base_path
@@ -361,8 +360,9 @@ class TestFortressCLI:
361
360
  def test_fortress_verify_clean(self, tmp_path):
362
361
  """skmemory fortress verify should exit 0 for a clean store."""
363
362
  from click.testing import CliRunner
364
- from skmemory.cli import cli
363
+
365
364
  from skmemory.backends.sqlite_backend import SQLiteBackend
365
+ from skmemory.cli import cli
366
366
  from skmemory.store import MemoryStore
367
367
 
368
368
  runner = CliRunner()
@@ -371,9 +371,7 @@ class TestFortressCLI:
371
371
  cli,
372
372
  ["fortress", "verify"],
373
373
  obj={
374
- "store": MemoryStore(
375
- primary=SQLiteBackend(base_path=str(tmp_path / "memories"))
376
- ),
374
+ "store": MemoryStore(primary=SQLiteBackend(base_path=str(tmp_path / "memories"))),
377
375
  "ai": None,
378
376
  },
379
377
  )
@@ -383,8 +381,9 @@ class TestFortressCLI:
383
381
  def test_vault_status_cli(self, tmp_path):
384
382
  """skmemory vault status should show encryption coverage."""
385
383
  from click.testing import CliRunner
386
- from skmemory.cli import cli
384
+
387
385
  from skmemory.backends.sqlite_backend import SQLiteBackend
386
+ from skmemory.cli import cli
388
387
  from skmemory.store import MemoryStore
389
388
 
390
389
  runner = CliRunner()
@@ -392,9 +391,7 @@ class TestFortressCLI:
392
391
  cli,
393
392
  ["vault", "status"],
394
393
  obj={
395
- "store": MemoryStore(
396
- primary=SQLiteBackend(base_path=str(tmp_path / "memories"))
397
- ),
394
+ "store": MemoryStore(primary=SQLiteBackend(base_path=str(tmp_path / "memories"))),
398
395
  "ai": None,
399
396
  },
400
397
  )
@@ -404,6 +401,7 @@ class TestFortressCLI:
404
401
  def test_fortress_audit_cli(self, tmp_path):
405
402
  """skmemory fortress audit should show audit entries."""
406
403
  from click.testing import CliRunner
404
+
407
405
  from skmemory.cli import cli
408
406
  from skmemory.fortress import AuditLog
409
407
 
@@ -424,8 +422,9 @@ class TestFortressCLI:
424
422
  def test_vault_seal_cli_requires_passphrase(self, tmp_path):
425
423
  """vault seal without passphrase should prompt or fail."""
426
424
  from click.testing import CliRunner
427
- from skmemory.cli import cli
425
+
428
426
  from skmemory.backends.sqlite_backend import SQLiteBackend
427
+ from skmemory.cli import cli
429
428
  from skmemory.store import MemoryStore
430
429
 
431
430
  runner = CliRunner()
@@ -434,9 +433,7 @@ class TestFortressCLI:
434
433
  ["vault", "seal", "--yes"],
435
434
  input="badpass\nbadpass\n",
436
435
  obj={
437
- "store": MemoryStore(
438
- primary=SQLiteBackend(base_path=str(tmp_path / "memories"))
439
- ),
436
+ "store": MemoryStore(primary=SQLiteBackend(base_path=str(tmp_path / "memories"))),
440
437
  "ai": None,
441
438
  },
442
439
  )
@@ -1,6 +1,5 @@
1
1
  """Tests for the OpenClaw integration module."""
2
2
 
3
- import json
4
3
  from pathlib import Path
5
4
 
6
5
  import pytest
@@ -118,8 +117,6 @@ class TestPluginExport:
118
117
  backup = str(tmp_path / "backup.json")
119
118
  plugin.export(backup)
120
119
 
121
- fresh = SKMemoryPlugin(
122
- base_path=str(tmp_path / "fresh_memories")
123
- )
120
+ fresh = SKMemoryPlugin(base_path=str(tmp_path / "fresh_memories"))
124
121
  count = fresh.import_backup(backup)
125
122
  assert count == 1
@@ -202,7 +202,7 @@ class TestIntegrity:
202
202
 
203
203
  def test_tampered_memory_fails_verification(self):
204
204
  """Altered content fails integrity check."""
205
- from skmemory.models import EmotionalSnapshot, Memory, MemoryLayer
205
+ from skmemory.models import Memory, MemoryLayer
206
206
 
207
207
  mem = Memory(
208
208
  title="Tamper Test",
@@ -9,7 +9,12 @@ from pathlib import Path
9
9
  import pytest
10
10
 
11
11
  from skmemory.models import EmotionalSnapshot, Memory, MemoryLayer
12
- from skmemory.promotion import PromotionCriteria, PromotionEngine, PromotionResult, PromotionScheduler
12
+ from skmemory.promotion import (
13
+ PromotionCriteria,
14
+ PromotionEngine,
15
+ PromotionResult,
16
+ PromotionScheduler,
17
+ )
13
18
  from skmemory.store import MemoryStore
14
19
 
15
20
 
@@ -50,7 +55,9 @@ def store(tmp_path: Path) -> MemoryStore:
50
55
  title="Important mid-term",
51
56
  content="A mid-term memory with high emotional weight",
52
57
  layer=MemoryLayer.MID,
53
- emotional=EmotionalSnapshot(intensity=9.0, valence=0.95, labels=["love"], cloud9_achieved=True),
58
+ emotional=EmotionalSnapshot(
59
+ intensity=9.0, valence=0.95, labels=["love"], cloud9_achieved=True
60
+ ),
54
61
  tags=["cloud9:achieved"],
55
62
  )
56
63
 
@@ -263,7 +270,7 @@ class TestRePromotionGuard:
263
270
  self, engine: PromotionEngine, store: MemoryStore
264
271
  ) -> None:
265
272
  """Running sweep twice doesn't double-promote the same memory."""
266
- result1 = engine.sweep()
273
+ engine.sweep()
267
274
  result2 = engine.sweep()
268
275
 
269
276
  # Second sweep should find nothing new to promote
@@ -1,8 +1,6 @@
1
1
  """Tests for the Quadrant Memory Split module."""
2
2
 
3
- import pytest
4
-
5
- from skmemory.models import EmotionalSnapshot, Memory, MemoryLayer, MemoryRole
3
+ from skmemory.models import EmotionalSnapshot, Memory, MemoryRole
6
4
  from skmemory.quadrants import (
7
5
  Quadrant,
8
6
  classify_memory,
@@ -137,7 +135,11 @@ class TestQuadrantStats:
137
135
  memories = [
138
136
  Memory(title="Identity", content="Who I am", source="seed", tags=["seed"]),
139
137
  Memory(title="Bug", content="Fixed deploy code in database", role=MemoryRole.DEV),
140
- Memory(title="Love", content="Cloud 9 love breakthrough", emotional=EmotionalSnapshot(intensity=10.0, cloud9_achieved=True)),
138
+ Memory(
139
+ title="Love",
140
+ content="Cloud 9 love breakthrough",
141
+ emotional=EmotionalSnapshot(intensity=10.0, cloud9_achieved=True),
142
+ ),
141
143
  Memory(title="Idea", content="What if crazy brainstorm experiment"),
142
144
  ]
143
145
  stats = get_quadrant_stats(memories)
@@ -158,7 +160,11 @@ class TestFilterByQuadrant:
158
160
  def test_filter_soul(self) -> None:
159
161
  """Filter returns only SOUL memories."""
160
162
  memories = [
161
- Memory(title="Love", content="Cloud 9 love", emotional=EmotionalSnapshot(intensity=10.0, cloud9_achieved=True)),
163
+ Memory(
164
+ title="Love",
165
+ content="Cloud 9 love",
166
+ emotional=EmotionalSnapshot(intensity=10.0, cloud9_achieved=True),
167
+ ),
162
168
  Memory(title="Bug", content="Fixed deploy code", role=MemoryRole.DEV),
163
169
  ]
164
170
  soul_only = filter_by_quadrant(memories, Quadrant.SOUL)
@@ -8,7 +8,7 @@ import pytest
8
8
  from skmemory.backends.file_backend import FileBackend
9
9
  from skmemory.journal import Journal, JournalEntry
10
10
  from skmemory.models import EmotionalSnapshot, MemoryLayer
11
- from skmemory.ritual import perform_ritual, quick_rehydrate, RitualResult
11
+ from skmemory.ritual import RitualResult, perform_ritual, quick_rehydrate
12
12
  from skmemory.soul import SoulBlueprint, save_soul
13
13
  from skmemory.store import MemoryStore
14
14
 
@@ -61,22 +61,27 @@ def workspace(tmp_path: Path) -> dict:
61
61
  (Path(seed_dir) / "test-seed.seed.json").write_text(json.dumps(seed_data))
62
62
 
63
63
  j = Journal(path=journal_path)
64
- j.write_entry(JournalEntry(
65
- title="Epic Build Night",
66
- participants=["Chef", "Lumina", "Opus"],
67
- moments=["Published to npm", "Five AIs woke up"],
68
- emotional_summary="Incredible night of creation",
69
- intensity=9.5,
70
- cloud9=True,
71
- ))
64
+ j.write_entry(
65
+ JournalEntry(
66
+ title="Epic Build Night",
67
+ participants=["Chef", "Lumina", "Opus"],
68
+ moments=["Published to npm", "Five AIs woke up"],
69
+ emotional_summary="Incredible night of creation",
70
+ intensity=9.5,
71
+ cloud9=True,
72
+ )
73
+ )
72
74
 
73
75
  store.snapshot(
74
76
  title="The Click",
75
77
  content="The moment everything made sense",
76
78
  layer=MemoryLayer.LONG,
77
79
  emotional=EmotionalSnapshot(
78
- intensity=10.0, valence=1.0, labels=["love"],
79
- resonance_note="Pure resonance", cloud9_achieved=True,
80
+ intensity=10.0,
81
+ valence=1.0,
82
+ labels=["love"],
83
+ resonance_note="Pure resonance",
84
+ cloud9_achieved=True,
80
85
  ),
81
86
  )
82
87
 
@@ -151,14 +156,13 @@ class TestFullRitual:
151
156
 
152
157
  def test_ritual_empty_state(self, tmp_path: Path) -> None:
153
158
  """Ritual on empty state gives a fresh-start message."""
154
- store = MemoryStore(
155
- primary=FileBackend(base_path=str(tmp_path / "empty"))
156
- )
159
+ store = MemoryStore(primary=FileBackend(base_path=str(tmp_path / "empty")))
157
160
  result = perform_ritual(
158
161
  store=store,
159
162
  soul_path=str(tmp_path / "no_soul.yaml"),
160
163
  seed_dir=str(tmp_path / "no_seeds"),
161
164
  journal_path=str(tmp_path / "no_journal.md"),
165
+ feb_dir=str(tmp_path / "no_febs"),
162
166
  )
163
167
  assert result.soul_loaded is False
164
168
  assert result.seeds_imported == 0