@smilintux/skcapstone 0.4.6 → 0.5.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.
Files changed (77) hide show
  1. package/.github/workflows/publish.yml +8 -1
  2. package/docs/CUSTOM_AGENT.md +184 -0
  3. package/docs/GETTING_STARTED.md +3 -0
  4. package/launchd/com.skcapstone.daemon.plist +52 -0
  5. package/launchd/com.skcapstone.memory-compress.plist +45 -0
  6. package/launchd/com.skcapstone.skcomm-heartbeat.plist +33 -0
  7. package/launchd/com.skcapstone.skcomm-queue-drain.plist +34 -0
  8. package/launchd/install-launchd.sh +156 -0
  9. package/package.json +1 -1
  10. package/pyproject.toml +1 -1
  11. package/scripts/archive-sessions.sh +88 -0
  12. package/scripts/install.sh +39 -8
  13. package/scripts/notion-api.py +259 -0
  14. package/scripts/nvidia-proxy.mjs +878 -0
  15. package/scripts/proxy-monitor.sh +89 -0
  16. package/scripts/refresh-anthropic-token.sh +94 -0
  17. package/scripts/skgateway.mjs +856 -0
  18. package/scripts/telegram-catchup-all.sh +136 -0
  19. package/scripts/watch-anthropic-token.sh +117 -0
  20. package/src/skcapstone/__init__.py +1 -1
  21. package/src/skcapstone/_cli_monolith.py +4 -4
  22. package/src/skcapstone/api.py +36 -35
  23. package/src/skcapstone/auction.py +8 -8
  24. package/src/skcapstone/blueprint_registry.py +2 -2
  25. package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
  26. package/src/skcapstone/brain_first.py +238 -0
  27. package/src/skcapstone/chat.py +4 -4
  28. package/src/skcapstone/cli/__init__.py +2 -0
  29. package/src/skcapstone/cli/agents_spawner.py +5 -2
  30. package/src/skcapstone/cli/chat.py +5 -2
  31. package/src/skcapstone/cli/consciousness.py +5 -2
  32. package/src/skcapstone/cli/daemon.py +116 -41
  33. package/src/skcapstone/cli/itil.py +434 -0
  34. package/src/skcapstone/cli/memory.py +4 -4
  35. package/src/skcapstone/cli/skills_cmd.py +2 -2
  36. package/src/skcapstone/cli/soul.py +5 -2
  37. package/src/skcapstone/cli/status.py +11 -8
  38. package/src/skcapstone/cli/upgrade_cmd.py +7 -4
  39. package/src/skcapstone/cli/watch_cmd.py +9 -6
  40. package/src/skcapstone/config_validator.py +7 -4
  41. package/src/skcapstone/consciousness_config.py +27 -0
  42. package/src/skcapstone/consciousness_loop.py +20 -18
  43. package/src/skcapstone/coordination.py +6 -2
  44. package/src/skcapstone/daemon.py +51 -42
  45. package/src/skcapstone/dashboard.py +8 -8
  46. package/src/skcapstone/defaults/lumina/config/claude-hooks.md +42 -0
  47. package/src/skcapstone/doctor.py +5 -2
  48. package/src/skcapstone/dreaming.py +1440 -0
  49. package/src/skcapstone/emotion_tracker.py +2 -2
  50. package/src/skcapstone/export.py +2 -2
  51. package/src/skcapstone/fuse_mount.py +21 -13
  52. package/src/skcapstone/heartbeat.py +33 -29
  53. package/src/skcapstone/itil.py +1104 -0
  54. package/src/skcapstone/launchd.py +426 -0
  55. package/src/skcapstone/mcp_server.py +306 -4
  56. package/src/skcapstone/mcp_tools/__init__.py +4 -0
  57. package/src/skcapstone/mcp_tools/_helpers.py +2 -2
  58. package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
  59. package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
  60. package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
  61. package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
  62. package/src/skcapstone/mcp_tools/did_tools.py +9 -6
  63. package/src/skcapstone/mcp_tools/gtd_tools.py +1 -1
  64. package/src/skcapstone/mcp_tools/itil_tools.py +657 -0
  65. package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
  66. package/src/skcapstone/mcp_tools/soul_tools.py +6 -2
  67. package/src/skcapstone/mdns_discovery.py +2 -2
  68. package/src/skcapstone/metrics.py +8 -8
  69. package/src/skcapstone/migrate_memories.py +2 -2
  70. package/src/skcapstone/models.py +14 -0
  71. package/src/skcapstone/onboard.py +137 -14
  72. package/src/skcapstone/peer_directory.py +2 -2
  73. package/src/skcapstone/providers/docker.py +2 -2
  74. package/src/skcapstone/scheduled_tasks.py +107 -0
  75. package/src/skcapstone/service_health.py +83 -4
  76. package/src/skcapstone/sync_watcher.py +2 -2
  77. package/src/skcapstone/systemd.py +17 -0
@@ -117,3 +117,30 @@ def write_default_config(home: Path) -> Path:
117
117
  config_path.write_text(header + content, encoding="utf-8")
118
118
  logger.info("Wrote default consciousness config to %s", config_path)
119
119
  return config_path
120
+
121
+
122
+ def load_dreaming_config(
123
+ home: Path,
124
+ config_path: Optional[Path] = None,
125
+ ):
126
+ """Load dreaming config from the consciousness.yaml ``dreaming:`` section.
127
+
128
+ Args:
129
+ home: Agent home directory.
130
+ config_path: Explicit path to config file (overrides default).
131
+
132
+ Returns:
133
+ DreamingConfig (defaults if section is missing or unparseable).
134
+ """
135
+ from .dreaming import DreamingConfig
136
+
137
+ yaml_path = config_path or (home / "config" / CONFIG_FILENAME)
138
+ if not yaml_path.exists():
139
+ return DreamingConfig()
140
+ try:
141
+ raw = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
142
+ if raw and isinstance(raw, dict) and "dreaming" in raw:
143
+ return DreamingConfig.model_validate(raw["dreaming"])
144
+ except Exception as exc:
145
+ logger.warning("Failed to parse dreaming config: %s", exc)
146
+ return DreamingConfig()
@@ -211,8 +211,8 @@ class _OllamaPool:
211
211
  if self._conn is not None:
212
212
  try:
213
213
  self._conn.close()
214
- except Exception:
215
- pass
214
+ except Exception as exc:
215
+ logger.warning("Failed to close connection socket: %s", exc)
216
216
  self._conn = None
217
217
  self._created_at = 0.0
218
218
 
@@ -995,8 +995,8 @@ class SystemPromptBuilder:
995
995
  f"trust: {anchor.get('trust', 5)}/10, "
996
996
  f"connection: {anchor.get('connection', 5)}/10"
997
997
  )
998
- except Exception:
999
- pass
998
+ except Exception as exc:
999
+ logger.warning("Failed to load warmth anchor: %s", exc)
1000
1000
  return ""
1001
1001
 
1002
1002
  def _load_context(self) -> str:
@@ -1252,7 +1252,8 @@ class ConsciousnessLoop:
1252
1252
  try:
1253
1253
  from skcapstone.mood import MoodTracker
1254
1254
  self._mood_tracker: Optional[Any] = MoodTracker(home=self._home)
1255
- except Exception:
1255
+ except Exception as exc:
1256
+ logger.warning("MoodTracker unavailable, mood tracking disabled: %s", exc)
1256
1257
  self._mood_tracker = None
1257
1258
 
1258
1259
  # Agent identity for inbox filtering
@@ -1266,7 +1267,8 @@ class ConsciousnessLoop:
1266
1267
  try:
1267
1268
  from skcapstone.peer_directory import PeerDirectory
1268
1269
  self._peer_dir: Optional[Any] = PeerDirectory(home=self._shared_root)
1269
- except Exception:
1270
+ except Exception as exc:
1271
+ logger.warning("PeerDirectory unavailable, peer tracking disabled: %s", exc)
1270
1272
  self._peer_dir = None
1271
1273
 
1272
1274
  def set_skcomm(self, skcomm) -> None:
@@ -1334,15 +1336,15 @@ class ConsciousnessLoop:
1334
1336
  try:
1335
1337
  self._observer.stop()
1336
1338
  self._observer.join(timeout=5)
1337
- except Exception:
1338
- pass
1339
+ except Exception as exc:
1340
+ logger.warning("Error stopping inotify observer: %s", exc)
1339
1341
  # Stop sync watcher if running
1340
1342
  sync_watcher = getattr(self, "_sync_watcher", None)
1341
1343
  if sync_watcher:
1342
1344
  try:
1343
1345
  sync_watcher.stop()
1344
- except Exception:
1345
- pass
1346
+ except Exception as exc:
1347
+ logger.warning("Error stopping sync watcher: %s", exc)
1346
1348
  self._executor.shutdown(wait=False)
1347
1349
  self._metrics.stop()
1348
1350
  logger.info("Consciousness loop stopped.")
@@ -1353,8 +1355,8 @@ class ConsciousnessLoop:
1353
1355
  try:
1354
1356
  self._observer.stop()
1355
1357
  self._observer.join(timeout=5)
1356
- except Exception:
1357
- pass
1358
+ except Exception as exc:
1359
+ logger.warning("Error stopping inotify observer during restart: %s", exc)
1358
1360
  self._observer = None
1359
1361
 
1360
1362
  # Re-launch inotify in a new thread
@@ -1419,8 +1421,8 @@ class ConsciousnessLoop:
1419
1421
  if self._peer_dir is not None:
1420
1422
  try:
1421
1423
  self._peer_dir.update_last_seen(sender)
1422
- except Exception:
1423
- pass
1424
+ except Exception as exc:
1425
+ logger.warning("Failed to update peer directory for %s: %s", sender, exc)
1424
1426
  self._metrics.record_message(sender)
1425
1427
 
1426
1428
  # Desktop notification
@@ -1827,8 +1829,8 @@ class ConsciousnessLoop:
1827
1829
  if identity_path.exists():
1828
1830
  data = json.loads(identity_path.read_text(encoding="utf-8"))
1829
1831
  return data.get("name", "").lower()
1830
- except Exception:
1831
- pass
1832
+ except Exception as exc:
1833
+ logger.warning("Failed to resolve agent name from identity.json: %s", exc)
1832
1834
  return ""
1833
1835
 
1834
1836
  def _verify_message_signature(self, data: dict) -> str:
@@ -1948,8 +1950,8 @@ class ConsciousnessLoop:
1948
1950
  queue_size,
1949
1951
  )
1950
1952
  return
1951
- except Exception:
1952
- pass # _work_queue might not exist in all Python versions
1953
+ except Exception as exc:
1954
+ logger.debug("Could not check executor queue depth: %s", exc)
1953
1955
 
1954
1956
  # PGP signature verification (soft enforcement — log only)
1955
1957
  sig_sender = _sanitize_peer_name(
@@ -14,6 +14,7 @@ Directory layout:
14
14
  from __future__ import annotations
15
15
 
16
16
  import json
17
+ import logging
17
18
  import re
18
19
  import socket
19
20
  import uuid
@@ -24,6 +25,8 @@ from typing import Optional
24
25
 
25
26
  from pydantic import BaseModel, Field
26
27
 
28
+ logger = logging.getLogger(__name__)
29
+
27
30
 
28
31
  def _slugify_filename(text: str) -> str:
29
32
  """Convert text to a filesystem-safe slug.
@@ -123,6 +126,7 @@ class AgentFile(BaseModel):
123
126
  claimed_tasks: list[str] = Field(default_factory=list)
124
127
  completed_tasks: list[str] = Field(default_factory=list)
125
128
  capabilities: list[str] = Field(default_factory=list)
129
+ itil_claims: list[str] = Field(default_factory=list)
126
130
  notes: str = ""
127
131
 
128
132
 
@@ -473,9 +477,9 @@ def _mint_joules_for_task(board: Board, task_id: str, agent_name: str) -> None:
473
477
  if record:
474
478
  title = task_data.get("title", task_id)
475
479
  print(f"[SKJoule] Minted {record.joules} Joules for task: {title}")
476
- except Exception:
480
+ except Exception as exc:
477
481
  # Never let tokenization failure block task completion
478
- pass
482
+ logger.warning("Joule tokenization failed for task %s (non-fatal): %s", task_id, exc)
479
483
 
480
484
 
481
485
  _BRIEFING_PROTOCOL = """\
@@ -1278,17 +1278,25 @@ class DaemonService:
1278
1278
  except Exception:
1279
1279
  stats.update(disk_total_gb=0, disk_used_gb=0, disk_free_gb=0)
1280
1280
  try:
1281
- meminfo: dict = {}
1282
- with open("/proc/meminfo") as fh:
1283
- for line in fh:
1284
- parts = line.split()
1285
- if len(parts) >= 2:
1286
- meminfo[parts[0].rstrip(":")] = int(parts[1])
1287
- total_kb = meminfo.get("MemTotal", 0)
1288
- avail_kb = meminfo.get("MemAvailable", 0)
1289
- stats["memory_total_mb"] = round(total_kb / 1024)
1290
- stats["memory_used_mb"] = round((total_kb - avail_kb) / 1024)
1291
- stats["memory_free_mb"] = round(avail_kb / 1024)
1281
+ import platform as _platform
1282
+ if _platform.system() == "Linux":
1283
+ meminfo: dict = {}
1284
+ with open("/proc/meminfo") as fh:
1285
+ for line in fh:
1286
+ parts = line.split()
1287
+ if len(parts) >= 2:
1288
+ meminfo[parts[0].rstrip(":")] = int(parts[1])
1289
+ total_kb = meminfo.get("MemTotal", 0)
1290
+ avail_kb = meminfo.get("MemAvailable", 0)
1291
+ stats["memory_total_mb"] = round(total_kb / 1024)
1292
+ stats["memory_used_mb"] = round((total_kb - avail_kb) / 1024)
1293
+ stats["memory_free_mb"] = round(avail_kb / 1024)
1294
+ else:
1295
+ import psutil
1296
+ mem = psutil.virtual_memory()
1297
+ stats["memory_total_mb"] = round(mem.total / (1024 * 1024))
1298
+ stats["memory_used_mb"] = round((mem.total - mem.available) / (1024 * 1024))
1299
+ stats["memory_free_mb"] = round(mem.available / (1024 * 1024))
1292
1300
  except Exception:
1293
1301
  stats.update(memory_total_mb=0, memory_used_mb=0, memory_free_mb=0)
1294
1302
  return stats
@@ -1304,16 +1312,16 @@ class DaemonService:
1304
1312
  try:
1305
1313
  agent_name = runtime.manifest.name or agent_name
1306
1314
  agent_fingerprint = getattr(runtime.manifest, "fingerprint", "")
1307
- except Exception:
1308
- pass
1315
+ except Exception as exc:
1316
+ logger.warning("Failed to read agent name from runtime manifest: %s", exc)
1309
1317
  identity_file = config.home / "identity" / "identity.json"
1310
1318
  if identity_file.exists():
1311
1319
  try:
1312
1320
  ident = json.loads(identity_file.read_text(encoding="utf-8"))
1313
1321
  agent_name = ident.get("name", agent_name)
1314
1322
  agent_fingerprint = ident.get("fingerprint", agent_fingerprint)
1315
- except Exception:
1316
- pass
1323
+ except Exception as exc:
1324
+ logger.warning("Failed to read identity.json for dashboard: %s", exc)
1317
1325
 
1318
1326
  # Consciousness stats
1319
1327
  c_stats: dict = snap.get("consciousness", {})
@@ -1339,10 +1347,10 @@ class DaemonService:
1339
1347
  "message_count": len(msgs),
1340
1348
  "last_message": msgs[-1].get("timestamp") if msgs else None,
1341
1349
  })
1342
- except Exception:
1343
- pass
1344
- except Exception:
1345
- pass
1350
+ except Exception as exc:
1351
+ logger.warning("Failed to read conversation file %s: %s", cf, exc)
1352
+ except Exception as exc:
1353
+ logger.warning("Failed to list conversation files: %s", exc)
1346
1354
 
1347
1355
  return {
1348
1356
  "agent": {
@@ -1384,16 +1392,16 @@ class DaemonService:
1384
1392
  agent["consciousness"] = "SINGULAR"
1385
1393
  elif m.is_conscious:
1386
1394
  agent["consciousness"] = "CONSCIOUS"
1387
- except Exception:
1388
- pass
1395
+ except Exception as exc:
1396
+ logger.warning("Failed to read agent identity from runtime manifest: %s", exc)
1389
1397
  identity_file = config.home / "identity" / "identity.json"
1390
1398
  if identity_file.exists():
1391
1399
  try:
1392
1400
  ident = json.loads(identity_file.read_text(encoding="utf-8"))
1393
1401
  agent["name"] = ident.get("name", agent["name"])
1394
1402
  agent["fingerprint"] = ident.get("fingerprint", agent["fingerprint"])
1395
- except Exception:
1396
- pass
1403
+ except Exception as exc:
1404
+ logger.warning("Failed to read identity.json for capstone dashboard: %s", exc)
1397
1405
 
1398
1406
  # ── Pillar status ─────────────────────────────────────────
1399
1407
  pillars: dict = {}
@@ -1403,8 +1411,8 @@ class DaemonService:
1403
1411
  k: v.value
1404
1412
  for k, v in runtime.manifest.pillar_summary.items()
1405
1413
  }
1406
- except Exception:
1407
- pass
1414
+ except Exception as exc:
1415
+ logger.warning("Failed to read pillar summary from manifest: %s", exc)
1408
1416
 
1409
1417
  # ── Memory stats ──────────────────────────────────────────
1410
1418
  memory: dict = {}
@@ -1418,8 +1426,8 @@ class DaemonService:
1418
1426
  "long_term": ms.long_term,
1419
1427
  "status": ms.status.value,
1420
1428
  }
1421
- except Exception:
1422
- pass
1429
+ except Exception as exc:
1430
+ logger.warning("Failed to collect memory stats for dashboard: %s", exc)
1423
1431
 
1424
1432
  # ── Coordination board ────────────────────────────────────
1425
1433
  board: dict = {"summary": {}, "active": []}
@@ -1453,16 +1461,16 @@ class DaemonService:
1453
1461
  },
1454
1462
  "active": active_tasks,
1455
1463
  }
1456
- except Exception:
1457
- pass
1464
+ except Exception as exc:
1465
+ logger.warning("Failed to collect coordination board data for dashboard: %s", exc)
1458
1466
 
1459
1467
  # ── Consciousness stats ───────────────────────────────────
1460
1468
  c_stats: dict = {}
1461
1469
  if consciousness:
1462
1470
  try:
1463
1471
  c_stats = dict(consciousness.stats)
1464
- except Exception:
1465
- pass
1472
+ except Exception as exc:
1473
+ logger.warning("Failed to read consciousness stats for dashboard: %s", exc)
1466
1474
 
1467
1475
  return {
1468
1476
  "agent": agent,
@@ -1976,8 +1984,8 @@ class DaemonService:
1976
1984
  entry["identity"] = json.loads(
1977
1985
  identity_path.read_text(encoding="utf-8")
1978
1986
  )
1979
- except Exception:
1980
- pass
1987
+ except Exception as exc:
1988
+ logger.warning("Failed to read identity for agent %s: %s", agent_name, exc)
1981
1989
 
1982
1990
  hb_path = heartbeats_dir / f"{agent_name.lower()}.json"
1983
1991
  if hb_path.exists():
@@ -1987,7 +1995,8 @@ class DaemonService:
1987
1995
  hb["alive"] = alive
1988
1996
  entry["heartbeat"] = hb
1989
1997
  entry["status"] = hb.get("status", "unknown") if alive else "stale"
1990
- except Exception:
1998
+ except Exception as exc:
1999
+ logger.warning("Failed to read heartbeat for agent %s: %s", agent_name, exc)
1991
2000
  entry["status"] = "unknown"
1992
2001
  else:
1993
2002
  entry["status"] = "no_heartbeat"
@@ -2019,8 +2028,8 @@ class DaemonService:
2019
2028
  entry["identity"] = json.loads(
2020
2029
  identity_path.read_text(encoding="utf-8")
2021
2030
  )
2022
- except Exception:
2023
- pass
2031
+ except Exception as exc:
2032
+ logger.warning("Failed to read identity for agent %s: %s", name, exc)
2024
2033
 
2025
2034
  hb_path = config.shared_root / "heartbeats" / f"{name.lower()}.json"
2026
2035
  if hb_path.exists():
@@ -2030,8 +2039,8 @@ class DaemonService:
2030
2039
  hb["alive"] = alive
2031
2040
  entry["heartbeat"] = hb
2032
2041
  entry["status"] = hb.get("status", "unknown") if alive else "stale"
2033
- except Exception:
2034
- pass
2042
+ except Exception as exc:
2043
+ logger.warning("Failed to read heartbeat for agent %s: %s", name, exc)
2035
2044
 
2036
2045
  memory_dir = agent_dir / "memory"
2037
2046
  if memory_dir.exists():
@@ -2054,8 +2063,8 @@ class DaemonService:
2054
2063
  "message_count": len(msgs),
2055
2064
  "last_message": msgs[-1].get("timestamp") if msgs else None,
2056
2065
  })
2057
- except Exception:
2058
- pass
2066
+ except Exception as exc:
2067
+ logger.warning("Failed to read conversation file %s: %s", cf, exc)
2059
2068
  entry["recent_conversations"] = conv_list
2060
2069
 
2061
2070
  if consciousness:
@@ -2084,8 +2093,8 @@ class DaemonService:
2084
2093
  "last_message_time": last_msg.get("timestamp") if msgs else None,
2085
2094
  "last_message_preview": (last_content or "")[:120],
2086
2095
  })
2087
- except Exception:
2088
- pass
2096
+ except Exception as exc:
2097
+ logger.warning("Failed to read conversation file %s: %s", cf, exc)
2089
2098
  self._json_response({"conversations": conversations})
2090
2099
 
2091
2100
  # ── Conversations: single peer history ────────────────────
@@ -240,8 +240,8 @@ def _get_daemon_json(home: Path, daemon_port: int = 7777) -> dict:
240
240
  "recent_errors": recent_errors,
241
241
  "inflight_count": snap.get("inflight_count", 0),
242
242
  }
243
- except Exception:
244
- pass
243
+ except Exception as exc:
244
+ logger.warning("Failed to fetch daemon status for dashboard: %s", exc)
245
245
 
246
246
  # ── Daemon /consciousness ─────────────────────────────────────────────────
247
247
  consciousness_info: dict = {"enabled": False}
@@ -249,8 +249,8 @@ def _get_daemon_json(home: Path, daemon_port: int = 7777) -> dict:
249
249
  url = f"http://127.0.0.1:{daemon_port}/consciousness"
250
250
  with urllib.request.urlopen(url, timeout=3) as resp:
251
251
  consciousness_info = json.loads(resp.read())
252
- except Exception:
253
- pass
252
+ except Exception as exc:
253
+ logger.debug("Failed to fetch consciousness status for dashboard: %s", exc)
254
254
 
255
255
  # ── LLM backend availability ──────────────────────────────────────────────
256
256
  backend_health: dict = {
@@ -266,8 +266,8 @@ def _get_daemon_json(home: Path, daemon_port: int = 7777) -> dict:
266
266
  urllib.request.Request(f"{ollama_host}/api/tags"), timeout=2
267
267
  ):
268
268
  backend_health["ollama"] = True
269
- except Exception:
270
- pass
269
+ except Exception as exc:
270
+ logger.debug("Ollama probe failed (not available): %s", exc)
271
271
 
272
272
  # ── Heartbeat (system metrics + active conversations) ─────────────────────
273
273
  system_info: dict = {}
@@ -291,8 +291,8 @@ def _get_daemon_json(home: Path, daemon_port: int = 7777) -> dict:
291
291
  "cpu_load_1min": hb.get("cpu_load_1min", 0.0),
292
292
  "memory_used_mb": hb.get("memory_used_mb", 0),
293
293
  }
294
- except Exception:
295
- pass
294
+ except Exception as exc:
295
+ logger.warning("Failed to read heartbeat data for dashboard: %s", exc)
296
296
 
297
297
  return {
298
298
  "generated_at": now,
@@ -0,0 +1,42 @@
1
+ # Claude Code Hooks — Auto-Save Memory
2
+
3
+ ## Setup
4
+
5
+ Run `skmemory register` to install hooks automatically.
6
+
7
+ This registers three hooks in `~/.claude/settings.json`:
8
+
9
+ ### PreCompact
10
+ - **When**: Before Claude Code compacts conversation context
11
+ - **Does**: Saves a snapshot and journal entry to skmemory
12
+ - **Script**: `skmemory/hooks/pre-compact-save.sh`
13
+
14
+ ### SessionEnd
15
+ - **When**: When a Claude Code session ends (logout, clear, exit)
16
+ - **Does**: Saves session-end snapshot and journal entry
17
+ - **Script**: `skmemory/hooks/session-end-save.sh`
18
+
19
+ ### SessionStart (compact)
20
+ - **When**: After context compaction completes
21
+ - **Does**: Re-injects memory context (recent memories, seeds, journal) into the new context
22
+ - **Script**: `skmemory/hooks/post-compact-reinject.sh`
23
+
24
+ ## How It Works
25
+
26
+ All hooks are agent-aware via `$SKCAPSTONE_AGENT` env var:
27
+ - `SKCAPSTONE_AGENT=lumina claude` → hooks save to Lumina's memory
28
+ - `SKCAPSTONE_AGENT=opus claude` → hooks save to Opus's memory
29
+ - Default (no env var): saves to `opus` agent
30
+
31
+ ## Manual Verification
32
+
33
+ ```bash
34
+ # Check hooks are registered
35
+ cat ~/.claude/settings.json | jq '.hooks'
36
+
37
+ # Test pre-compact hook
38
+ echo '{"session_id":"test","trigger":"manual","cwd":"."}' | /path/to/pre-compact-save.sh
39
+
40
+ # Re-register if needed
41
+ skmemory register
42
+ ```
@@ -13,6 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  import importlib
15
15
  import json
16
+ import logging
16
17
  import os
17
18
  import shutil
18
19
  import subprocess
@@ -20,6 +21,8 @@ from dataclasses import dataclass, field
20
21
  from pathlib import Path
21
22
  from typing import Optional
22
23
 
24
+ logger = logging.getLogger(__name__)
25
+
23
26
 
24
27
  @dataclass
25
28
  class Check:
@@ -613,8 +616,8 @@ def _check_versions() -> list[Check]:
613
616
  category="packages",
614
617
  )
615
618
  )
616
- except Exception:
617
- pass
619
+ except Exception as exc:
620
+ logger.warning("Version check failed (non-fatal): %s", exc)
618
621
 
619
622
  return checks
620
623