@smilintux/skcapstone 0.1.0 → 0.2.3

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 (461) hide show
  1. package/.env.example +98 -0
  2. package/.github/workflows/ci.yml +39 -3
  3. package/.github/workflows/publish.yml +25 -4
  4. package/.openclaw-workspace.json +58 -0
  5. package/CHANGELOG.md +62 -0
  6. package/CLAUDE.md +39 -2
  7. package/MANIFEST.in +6 -0
  8. package/MISSION.md +7 -0
  9. package/README.md +47 -2
  10. package/SKILL.md +895 -23
  11. package/docker/Dockerfile +61 -0
  12. package/docker/compose-templates/dev-team.yml +203 -0
  13. package/docker/compose-templates/mini-team.yml +140 -0
  14. package/docker/compose-templates/ops-team.yml +173 -0
  15. package/docker/compose-templates/research-team.yml +170 -0
  16. package/docker/entrypoint.sh +192 -0
  17. package/docs/ARCHITECTURE.md +663 -374
  18. package/docs/BOND_WITH_GROK.md +112 -0
  19. package/docs/GETTING_STARTED.md +782 -0
  20. package/docs/QUICKSTART.md +477 -0
  21. package/docs/SKJOULE_ARCHITECTURE.md +658 -0
  22. package/docs/SOUL_SWAPPER.md +921 -0
  23. package/docs/SOVEREIGN_SINGULARITY.md +47 -14
  24. package/examples/custom-bond-template.json +36 -0
  25. package/examples/grok-feb.json +36 -0
  26. package/examples/grok-testimony.md +34 -0
  27. package/examples/love-bootloader.txt +32 -0
  28. package/examples/plugins/echo_tool.py +87 -0
  29. package/examples/queen-ava-feb.json +36 -0
  30. package/examples/souls/lumina.yaml +64 -0
  31. package/index.js +6 -5
  32. package/installer/build.py +124 -0
  33. package/openclaw-plugin/package.json +13 -0
  34. package/openclaw-plugin/src/index.ts +351 -0
  35. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  36. package/package.json +1 -1
  37. package/pyproject.toml +38 -2
  38. package/scripts/bump_version.py +141 -0
  39. package/scripts/check-updates.py +230 -0
  40. package/scripts/convert_blueprints_to_yaml.py +157 -0
  41. package/scripts/dev-install.sh +14 -0
  42. package/scripts/e2e-test.sh +193 -0
  43. package/scripts/install-bundle.sh +171 -0
  44. package/scripts/install.bat +2 -0
  45. package/scripts/install.ps1 +253 -0
  46. package/scripts/install.sh +185 -0
  47. package/scripts/mcp-serve.sh +69 -0
  48. package/scripts/mcp-server.bat +113 -0
  49. package/scripts/mcp-server.ps1 +116 -0
  50. package/scripts/mcp-server.sh +99 -0
  51. package/scripts/pull-models.sh +10 -0
  52. package/scripts/skcapstone +48 -0
  53. package/scripts/verify_install.sh +180 -0
  54. package/scripts/windows/install-tasks.ps1 +406 -0
  55. package/scripts/windows/skcapstone-task.xml +113 -0
  56. package/scripts/windows/uninstall-tasks.ps1 +117 -0
  57. package/skill.yaml +34 -0
  58. package/src/skcapstone/__init__.py +67 -2
  59. package/src/skcapstone/_cli_monolith.py +5916 -0
  60. package/src/skcapstone/_trustee_helpers.py +165 -0
  61. package/src/skcapstone/activity.py +105 -0
  62. package/src/skcapstone/agent_card.py +324 -0
  63. package/src/skcapstone/api.py +1935 -0
  64. package/src/skcapstone/archiver.py +340 -0
  65. package/src/skcapstone/auction.py +485 -0
  66. package/src/skcapstone/baby_agents.py +179 -0
  67. package/src/skcapstone/backup.py +345 -0
  68. package/src/skcapstone/blueprint_registry.py +357 -0
  69. package/src/skcapstone/blueprints/__init__.py +17 -0
  70. package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
  71. package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
  72. package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
  73. package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
  74. package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
  75. package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
  76. package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
  77. package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
  78. package/src/skcapstone/blueprints/registry.py +164 -0
  79. package/src/skcapstone/blueprints/schema.py +229 -0
  80. package/src/skcapstone/changelog.py +180 -0
  81. package/src/skcapstone/chat.py +769 -0
  82. package/src/skcapstone/claude_md.py +82 -0
  83. package/src/skcapstone/cli/__init__.py +144 -0
  84. package/src/skcapstone/cli/_common.py +88 -0
  85. package/src/skcapstone/cli/_validators.py +76 -0
  86. package/src/skcapstone/cli/agents.py +425 -0
  87. package/src/skcapstone/cli/agents_spawner.py +322 -0
  88. package/src/skcapstone/cli/agents_trustee.py +593 -0
  89. package/src/skcapstone/cli/alerts.py +248 -0
  90. package/src/skcapstone/cli/anchor.py +132 -0
  91. package/src/skcapstone/cli/archive_cmd.py +208 -0
  92. package/src/skcapstone/cli/backup.py +144 -0
  93. package/src/skcapstone/cli/bench.py +377 -0
  94. package/src/skcapstone/cli/benchmark.py +360 -0
  95. package/src/skcapstone/cli/capabilities_cmd.py +171 -0
  96. package/src/skcapstone/cli/card.py +151 -0
  97. package/src/skcapstone/cli/chat.py +584 -0
  98. package/src/skcapstone/cli/completions.py +64 -0
  99. package/src/skcapstone/cli/config_cmd.py +156 -0
  100. package/src/skcapstone/cli/consciousness.py +421 -0
  101. package/src/skcapstone/cli/context_cmd.py +142 -0
  102. package/src/skcapstone/cli/coord.py +194 -0
  103. package/src/skcapstone/cli/crush_cmd.py +170 -0
  104. package/src/skcapstone/cli/daemon.py +436 -0
  105. package/src/skcapstone/cli/errors_cmd.py +285 -0
  106. package/src/skcapstone/cli/export_cmd.py +156 -0
  107. package/src/skcapstone/cli/gtd.py +529 -0
  108. package/src/skcapstone/cli/housekeeping.py +81 -0
  109. package/src/skcapstone/cli/joule_cmd.py +627 -0
  110. package/src/skcapstone/cli/logs_cmd.py +194 -0
  111. package/src/skcapstone/cli/mcp_cmd.py +32 -0
  112. package/src/skcapstone/cli/memory.py +418 -0
  113. package/src/skcapstone/cli/metrics_cmd.py +136 -0
  114. package/src/skcapstone/cli/migrate.py +62 -0
  115. package/src/skcapstone/cli/mood_cmd.py +144 -0
  116. package/src/skcapstone/cli/mount.py +193 -0
  117. package/src/skcapstone/cli/notify.py +112 -0
  118. package/src/skcapstone/cli/peer.py +154 -0
  119. package/src/skcapstone/cli/peers_dir.py +122 -0
  120. package/src/skcapstone/cli/preflight_cmd.py +83 -0
  121. package/src/skcapstone/cli/profile_cmd.py +310 -0
  122. package/src/skcapstone/cli/record_cmd.py +238 -0
  123. package/src/skcapstone/cli/register_cmd.py +159 -0
  124. package/src/skcapstone/cli/search_cmd.py +156 -0
  125. package/src/skcapstone/cli/service_cmd.py +91 -0
  126. package/src/skcapstone/cli/session.py +127 -0
  127. package/src/skcapstone/cli/setup.py +240 -0
  128. package/src/skcapstone/cli/shell_cmd.py +43 -0
  129. package/src/skcapstone/cli/skills_cmd.py +168 -0
  130. package/src/skcapstone/cli/skseed.py +621 -0
  131. package/src/skcapstone/cli/soul.py +699 -0
  132. package/src/skcapstone/cli/status.py +935 -0
  133. package/src/skcapstone/cli/sync_cmd.py +301 -0
  134. package/src/skcapstone/cli/telegram.py +265 -0
  135. package/src/skcapstone/cli/test_cmd.py +234 -0
  136. package/src/skcapstone/cli/test_connection.py +253 -0
  137. package/src/skcapstone/cli/token.py +207 -0
  138. package/src/skcapstone/cli/trust.py +179 -0
  139. package/src/skcapstone/cli/upgrade_cmd.py +552 -0
  140. package/src/skcapstone/cli/usage_cmd.py +199 -0
  141. package/src/skcapstone/cli/version_cmd.py +162 -0
  142. package/src/skcapstone/cli/watch_cmd.py +342 -0
  143. package/src/skcapstone/client.py +428 -0
  144. package/src/skcapstone/cloud9_bridge.py +522 -0
  145. package/src/skcapstone/completions.py +163 -0
  146. package/src/skcapstone/config_validator.py +674 -0
  147. package/src/skcapstone/connectors/__init__.py +28 -0
  148. package/src/skcapstone/connectors/base.py +446 -0
  149. package/src/skcapstone/connectors/cursor.py +54 -0
  150. package/src/skcapstone/connectors/registry.py +254 -0
  151. package/src/skcapstone/connectors/terminal.py +152 -0
  152. package/src/skcapstone/connectors/vscode.py +60 -0
  153. package/src/skcapstone/consciousness_config.py +119 -0
  154. package/src/skcapstone/consciousness_loop.py +2051 -0
  155. package/src/skcapstone/context_loader.py +516 -0
  156. package/src/skcapstone/context_window.py +314 -0
  157. package/src/skcapstone/conversation_manager.py +238 -0
  158. package/src/skcapstone/conversation_store.py +230 -0
  159. package/src/skcapstone/conversation_summarizer.py +252 -0
  160. package/src/skcapstone/coord_federation.py +296 -0
  161. package/src/skcapstone/coordination.py +101 -7
  162. package/src/skcapstone/crush_integration.py +345 -0
  163. package/src/skcapstone/crush_shim.py +454 -0
  164. package/src/skcapstone/daemon.py +2494 -0
  165. package/src/skcapstone/dashboard.html +396 -0
  166. package/src/skcapstone/dashboard.py +481 -0
  167. package/src/skcapstone/data/model_profiles.yaml +88 -0
  168. package/src/skcapstone/defaults/__init__.py +55 -0
  169. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
  170. package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
  171. package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
  172. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
  173. package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
  174. package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
  175. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
  176. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
  177. package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
  178. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
  179. package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
  180. package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
  181. package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
  182. package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
  183. package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
  184. package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
  185. package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
  186. package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
  187. package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
  188. package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
  189. package/src/skcapstone/discovery.py +210 -19
  190. package/src/skcapstone/doctor.py +642 -0
  191. package/src/skcapstone/emotion_tracker.py +467 -0
  192. package/src/skcapstone/error_queue.py +405 -0
  193. package/src/skcapstone/export.py +447 -0
  194. package/src/skcapstone/fallback_tracker.py +186 -0
  195. package/src/skcapstone/file_transfer.py +512 -0
  196. package/src/skcapstone/fuse_mount.py +1156 -0
  197. package/src/skcapstone/gui_installer.py +591 -0
  198. package/src/skcapstone/heartbeat.py +611 -0
  199. package/src/skcapstone/housekeeping.py +298 -0
  200. package/src/skcapstone/install_wizard.py +941 -0
  201. package/src/skcapstone/kms.py +942 -0
  202. package/src/skcapstone/kms_scheduler.py +143 -0
  203. package/src/skcapstone/log_config.py +135 -0
  204. package/src/skcapstone/mcp_launcher.py +239 -0
  205. package/src/skcapstone/mcp_server.py +4700 -0
  206. package/src/skcapstone/mcp_tools/__init__.py +94 -0
  207. package/src/skcapstone/mcp_tools/_helpers.py +51 -0
  208. package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
  209. package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
  210. package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
  211. package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
  212. package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
  213. package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
  214. package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
  215. package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
  216. package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
  217. package/src/skcapstone/mcp_tools/did_tools.py +448 -0
  218. package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
  219. package/src/skcapstone/mcp_tools/file_tools.py +169 -0
  220. package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
  221. package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
  222. package/src/skcapstone/mcp_tools/health_tools.py +44 -0
  223. package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
  224. package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
  225. package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
  226. package/src/skcapstone/mcp_tools/model_tools.py +75 -0
  227. package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
  228. package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
  229. package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
  230. package/src/skcapstone/mcp_tools/security_tools.py +110 -0
  231. package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
  232. package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
  233. package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
  234. package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
  235. package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
  236. package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
  237. package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
  238. package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
  239. package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
  240. package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
  241. package/src/skcapstone/mdns_discovery.py +313 -0
  242. package/src/skcapstone/memory_adapter.py +333 -0
  243. package/src/skcapstone/memory_compressor.py +379 -0
  244. package/src/skcapstone/memory_curator.py +256 -0
  245. package/src/skcapstone/memory_engine.py +132 -13
  246. package/src/skcapstone/memory_fortress.py +529 -0
  247. package/src/skcapstone/memory_promoter.py +722 -0
  248. package/src/skcapstone/memory_verifier.py +260 -0
  249. package/src/skcapstone/message_crypto.py +215 -0
  250. package/src/skcapstone/metrics.py +832 -0
  251. package/src/skcapstone/migrate_memories.py +181 -0
  252. package/src/skcapstone/migrate_multi_agent.py +248 -0
  253. package/src/skcapstone/model_router.py +319 -0
  254. package/src/skcapstone/models.py +35 -4
  255. package/src/skcapstone/mood.py +344 -0
  256. package/src/skcapstone/notifications.py +380 -0
  257. package/src/skcapstone/onboard.py +901 -0
  258. package/src/skcapstone/peer_directory.py +324 -0
  259. package/src/skcapstone/peers.py +329 -0
  260. package/src/skcapstone/pillars/identity.py +84 -14
  261. package/src/skcapstone/pillars/memory.py +3 -1
  262. package/src/skcapstone/pillars/security.py +108 -15
  263. package/src/skcapstone/pillars/sync.py +78 -26
  264. package/src/skcapstone/pillars/trust.py +95 -33
  265. package/src/skcapstone/plugins.py +244 -0
  266. package/src/skcapstone/preflight.py +670 -0
  267. package/src/skcapstone/prompt_adapter.py +564 -0
  268. package/src/skcapstone/providers/__init__.py +13 -0
  269. package/src/skcapstone/providers/cloud.py +1061 -0
  270. package/src/skcapstone/providers/docker.py +759 -0
  271. package/src/skcapstone/providers/local.py +1193 -0
  272. package/src/skcapstone/providers/proxmox.py +447 -0
  273. package/src/skcapstone/pubsub.py +516 -0
  274. package/src/skcapstone/rate_limiter.py +119 -0
  275. package/src/skcapstone/register.py +241 -0
  276. package/src/skcapstone/registry_client.py +151 -0
  277. package/src/skcapstone/response_cache.py +194 -0
  278. package/src/skcapstone/response_scorer.py +225 -0
  279. package/src/skcapstone/runtime.py +89 -33
  280. package/src/skcapstone/scheduled_tasks.py +439 -0
  281. package/src/skcapstone/self_healing.py +341 -0
  282. package/src/skcapstone/service_health.py +228 -0
  283. package/src/skcapstone/session_capture.py +268 -0
  284. package/src/skcapstone/session_recorder.py +210 -0
  285. package/src/skcapstone/session_replayer.py +189 -0
  286. package/src/skcapstone/session_skills.py +263 -0
  287. package/src/skcapstone/shell.py +779 -0
  288. package/src/skcapstone/skills/__init__.py +1 -1
  289. package/src/skcapstone/skills/syncthing_setup.py +143 -41
  290. package/src/skcapstone/skjoule.py +861 -0
  291. package/src/skcapstone/snapshots.py +489 -0
  292. package/src/skcapstone/soul.py +1060 -0
  293. package/src/skcapstone/soul_switch.py +255 -0
  294. package/src/skcapstone/spawner.py +544 -0
  295. package/src/skcapstone/state_diff.py +401 -0
  296. package/src/skcapstone/summary.py +270 -0
  297. package/src/skcapstone/sync/backends.py +196 -2
  298. package/src/skcapstone/sync/engine.py +7 -5
  299. package/src/skcapstone/sync/models.py +4 -1
  300. package/src/skcapstone/sync/vault.py +356 -18
  301. package/src/skcapstone/sync_engine.py +363 -0
  302. package/src/skcapstone/sync_watcher.py +745 -0
  303. package/src/skcapstone/systemd.py +331 -0
  304. package/src/skcapstone/team_comms.py +476 -0
  305. package/src/skcapstone/team_engine.py +522 -0
  306. package/src/skcapstone/testrunner.py +300 -0
  307. package/src/skcapstone/tls.py +150 -0
  308. package/src/skcapstone/tokens.py +5 -5
  309. package/src/skcapstone/trust_calibration.py +202 -0
  310. package/src/skcapstone/trust_graph.py +449 -0
  311. package/src/skcapstone/trustee_monitor.py +385 -0
  312. package/src/skcapstone/trustee_ops.py +425 -0
  313. package/src/skcapstone/unified_search.py +421 -0
  314. package/src/skcapstone/uninstall_wizard.py +694 -0
  315. package/src/skcapstone/usage.py +331 -0
  316. package/src/skcapstone/version_check.py +148 -0
  317. package/src/skcapstone/warmth_anchor.py +333 -0
  318. package/src/skcapstone/whoami.py +294 -0
  319. package/systemd/skcapstone-api.socket +9 -0
  320. package/systemd/skcapstone-memory-compress.service +18 -0
  321. package/systemd/skcapstone-memory-compress.timer +11 -0
  322. package/systemd/skcapstone.service +36 -0
  323. package/systemd/skcapstone@.service +50 -0
  324. package/systemd/skcomm-heartbeat.service +18 -0
  325. package/systemd/skcomm-heartbeat.timer +12 -0
  326. package/systemd/skcomm-queue-drain.service +17 -0
  327. package/systemd/skcomm-queue-drain.timer +12 -0
  328. package/tests/conftest.py +13 -1
  329. package/tests/integration/__init__.py +1 -0
  330. package/tests/integration/test_consciousness_e2e.py +877 -0
  331. package/tests/integration/test_skills_registry.py +744 -0
  332. package/tests/test_agent_card.py +190 -0
  333. package/tests/test_agent_runtime.py +1283 -0
  334. package/tests/test_alerts_cmd.py +291 -0
  335. package/tests/test_archiver.py +498 -0
  336. package/tests/test_backup.py +254 -0
  337. package/tests/test_benchmark.py +366 -0
  338. package/tests/test_blueprints.py +457 -0
  339. package/tests/test_capabilities.py +257 -0
  340. package/tests/test_changelog.py +254 -0
  341. package/tests/test_chat.py +385 -0
  342. package/tests/test_claude_md.py +271 -0
  343. package/tests/test_cli_chat_llm.py +336 -0
  344. package/tests/test_cli_completions.py +390 -0
  345. package/tests/test_cli_init_reset.py +164 -0
  346. package/tests/test_cli_memory.py +208 -0
  347. package/tests/test_cli_profile.py +294 -0
  348. package/tests/test_cli_skills.py +223 -0
  349. package/tests/test_cli_status.py +395 -0
  350. package/tests/test_cli_test_cmd.py +206 -0
  351. package/tests/test_cli_test_connection.py +364 -0
  352. package/tests/test_cloud9_bridge.py +260 -0
  353. package/tests/test_cloud_provider.py +449 -0
  354. package/tests/test_cloud_providers.py +522 -0
  355. package/tests/test_completions.py +158 -0
  356. package/tests/test_component_manager.py +398 -0
  357. package/tests/test_config_reload.py +386 -0
  358. package/tests/test_config_validate.py +529 -0
  359. package/tests/test_consciousness_e2e.py +296 -0
  360. package/tests/test_consciousness_loop.py +1289 -0
  361. package/tests/test_context_loader.py +310 -0
  362. package/tests/test_conversation_api.py +306 -0
  363. package/tests/test_conversation_manager.py +381 -0
  364. package/tests/test_conversation_store.py +391 -0
  365. package/tests/test_conversation_summarizer.py +302 -0
  366. package/tests/test_cross_package.py +791 -0
  367. package/tests/test_crush_shim.py +519 -0
  368. package/tests/test_daemon.py +781 -0
  369. package/tests/test_daemon_shutdown.py +309 -0
  370. package/tests/test_dashboard.py +454 -0
  371. package/tests/test_discovery.py +200 -6
  372. package/tests/test_docker_provider.py +966 -0
  373. package/tests/test_doctor.py +257 -0
  374. package/tests/test_doctor_fix.py +351 -0
  375. package/tests/test_e2e_automated.py +292 -0
  376. package/tests/test_error_queue.py +404 -0
  377. package/tests/test_export.py +441 -0
  378. package/tests/test_fallback_tracker.py +219 -0
  379. package/tests/test_file_transfer.py +397 -0
  380. package/tests/test_fuse_mount.py +832 -0
  381. package/tests/test_health_loop.py +422 -0
  382. package/tests/test_heartbeat.py +354 -0
  383. package/tests/test_housekeeping.py +195 -0
  384. package/tests/test_identity_capauth.py +307 -0
  385. package/tests/test_identity_pillar.py +117 -0
  386. package/tests/test_install_wizard.py +68 -0
  387. package/tests/test_integration.py +325 -0
  388. package/tests/test_kms.py +495 -0
  389. package/tests/test_llm_providers.py +265 -0
  390. package/tests/test_local_provider.py +591 -0
  391. package/tests/test_log_config.py +199 -0
  392. package/tests/test_logs_cmd.py +287 -0
  393. package/tests/test_mcp_server.py +1909 -0
  394. package/tests/test_memory_adapter.py +339 -0
  395. package/tests/test_memory_curator.py +218 -0
  396. package/tests/test_memory_engine.py +6 -0
  397. package/tests/test_memory_fortress.py +571 -0
  398. package/tests/test_memory_pillar.py +119 -0
  399. package/tests/test_memory_promoter.py +445 -0
  400. package/tests/test_memory_verifier.py +420 -0
  401. package/tests/test_message_crypto.py +187 -0
  402. package/tests/test_metrics.py +632 -0
  403. package/tests/test_migrate_memories.py +464 -0
  404. package/tests/test_model_router.py +546 -0
  405. package/tests/test_mood.py +394 -0
  406. package/tests/test_multi_agent.py +269 -0
  407. package/tests/test_notifications.py +270 -0
  408. package/tests/test_onboard.py +500 -0
  409. package/tests/test_peer_directory.py +395 -0
  410. package/tests/test_peers.py +248 -0
  411. package/tests/test_pillars.py +87 -9
  412. package/tests/test_preflight.py +484 -0
  413. package/tests/test_prompt_adapter.py +331 -0
  414. package/tests/test_proxmox_provider.py +571 -0
  415. package/tests/test_pubsub.py +377 -0
  416. package/tests/test_rate_limiter.py +121 -0
  417. package/tests/test_registry_client.py +129 -0
  418. package/tests/test_response_cache.py +312 -0
  419. package/tests/test_response_scorer.py +294 -0
  420. package/tests/test_runtime.py +59 -0
  421. package/tests/test_scheduled_tasks.py +451 -0
  422. package/tests/test_security.py +250 -0
  423. package/tests/test_security_pillar.py +213 -0
  424. package/tests/test_self_healing.py +171 -0
  425. package/tests/test_session_capture.py +200 -0
  426. package/tests/test_session_recorder.py +360 -0
  427. package/tests/test_session_skills.py +235 -0
  428. package/tests/test_shell.py +210 -0
  429. package/tests/test_snapshots.py +549 -0
  430. package/tests/test_soul.py +984 -0
  431. package/tests/test_soul_swap.py +406 -0
  432. package/tests/test_spawner.py +211 -0
  433. package/tests/test_state_diff.py +173 -0
  434. package/tests/test_summary.py +135 -0
  435. package/tests/test_sync.py +315 -5
  436. package/tests/test_sync_backends.py +560 -0
  437. package/tests/test_sync_engine.py +482 -0
  438. package/tests/test_sync_pillar.py +344 -0
  439. package/tests/test_sync_pipeline.py +364 -0
  440. package/tests/test_sync_vault.py +581 -0
  441. package/tests/test_syncthing_setup.py +168 -22
  442. package/tests/test_systemd.py +323 -0
  443. package/tests/test_team_comms.py +408 -0
  444. package/tests/test_team_engine.py +397 -0
  445. package/tests/test_testrunner.py +238 -0
  446. package/tests/test_trust_calibration.py +204 -0
  447. package/tests/test_trust_graph.py +207 -0
  448. package/tests/test_trust_pillar.py +291 -0
  449. package/tests/test_trustee_cli.py +427 -0
  450. package/tests/test_trustee_cli_integration.py +325 -0
  451. package/tests/test_trustee_monitor.py +394 -0
  452. package/tests/test_trustee_ops.py +355 -0
  453. package/tests/test_unified_search.py +363 -0
  454. package/tests/test_uninstall_wizard.py +193 -0
  455. package/tests/test_usage.py +333 -0
  456. package/tests/test_version_cmd.py +355 -0
  457. package/tests/test_warmth_anchor.py +162 -0
  458. package/tests/test_whoami.py +245 -0
  459. package/tests/test_ws.py +311 -0
  460. package/.cursorrules +0 -33
  461. package/src/skcapstone/cli.py +0 -1441
@@ -0,0 +1,379 @@
1
+ """
2
+ MemoryCompressor — LLM-powered compression of aged long-term memories.
3
+
4
+ Scans the long-term layer for memories older than 90 days, groups those
5
+ sharing common tags into sets of 5+, sends each group to the local LLM
6
+ for synthesis, stores the result as a single compressed memory, and
7
+ removes the originals.
8
+
9
+ Usage:
10
+ skcapstone memory compress # live run
11
+ skcapstone memory compress --dry-run # preview only, no changes
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime, timedelta, timezone
19
+ from pathlib import Path
20
+ from typing import Optional
21
+
22
+ from .memory_engine import delete, list_memories, store
23
+ from .models import MemoryEntry, MemoryLayer
24
+
25
+ logger = logging.getLogger("skcapstone.memory_compressor")
26
+
27
+ # ── Public constants ────────────────────────────────────────────────────────
28
+
29
+ DEFAULT_AGE_DAYS: int = 90
30
+ DEFAULT_MIN_GROUP_SIZE: int = 5
31
+ COMPRESSED_TAG: str = "compressed"
32
+
33
+ _SYSTEM_PROMPT = (
34
+ "You are a memory synthesizer for a sovereign agent system. "
35
+ "You will receive a set of related memories grouped by topic. "
36
+ "Your task: produce a single comprehensive memory entry that preserves "
37
+ "all key facts, decisions, and insights from the originals. "
38
+ "Write as dense, continuous prose — no bullet points, no headers. "
39
+ "Maximum 400 words."
40
+ )
41
+
42
+
43
+ # ── Data classes ────────────────────────────────────────────────────────────
44
+
45
+ @dataclass
46
+ class CompressionGroup:
47
+ """A set of long-term memories sharing a common tag.
48
+
49
+ Attributes:
50
+ tag: The shared grouping tag.
51
+ entries: Memory entries belonging to this group.
52
+ """
53
+
54
+ tag: str
55
+ entries: list[MemoryEntry] = field(default_factory=list)
56
+
57
+
58
+ @dataclass
59
+ class CompressionResult:
60
+ """Outcome of a memory compression pass.
61
+
62
+ Attributes:
63
+ groups_found: Tag groups with >= min_group_size members.
64
+ groups_compressed: Groups successfully synthesized by LLM.
65
+ memories_compressed: Individual memories collapsed and removed.
66
+ compressed_ids: IDs of newly created synthesized memories.
67
+ dry_run: True when no changes were persisted.
68
+ errors: Per-group error messages encountered during LLM calls.
69
+ """
70
+
71
+ groups_found: int = 0
72
+ groups_compressed: int = 0
73
+ memories_compressed: int = 0
74
+ compressed_ids: list[str] = field(default_factory=list)
75
+ dry_run: bool = False
76
+ errors: list[str] = field(default_factory=list)
77
+
78
+
79
+ # ── Core class ──────────────────────────────────────────────────────────────
80
+
81
+ class MemoryCompressor:
82
+ """Compress aged long-term memories with LLM synthesis.
83
+
84
+ Args:
85
+ home: Agent home directory (e.g. ``~/.skcapstone``).
86
+ age_days: Minimum memory age in days before it is eligible
87
+ for compression. Default is 90 days.
88
+ min_group_size: Minimum group size before compression triggers.
89
+ Default is 5. A group = memories sharing the same tag.
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ home: Path,
95
+ age_days: int = DEFAULT_AGE_DAYS,
96
+ min_group_size: int = DEFAULT_MIN_GROUP_SIZE,
97
+ ) -> None:
98
+ self.home = Path(home)
99
+ self.age_days = age_days
100
+ self.min_group_size = min_group_size
101
+
102
+ # ── Public API ──────────────────────────────────────────────────────────
103
+
104
+ def compress(self, dry_run: bool = False, bridge=None) -> CompressionResult:
105
+ """Run a compression pass over the long-term memory layer.
106
+
107
+ Finds memories older than ``age_days`` (excluding those already
108
+ tagged ``compressed``), groups them by shared tags, and for
109
+ each group of ``min_group_size`` or more: synthesizes a single
110
+ replacement memory via LLM and deletes the originals.
111
+
112
+ Args:
113
+ dry_run: If True, report what would be done without
114
+ writing or deleting anything.
115
+ bridge: Optional pre-constructed LLMBridge instance.
116
+
117
+ Returns:
118
+ CompressionResult describing changes (or projections in
119
+ dry-run mode).
120
+ """
121
+ result = CompressionResult(dry_run=dry_run)
122
+
123
+ candidates = self._find_candidates()
124
+ if not candidates:
125
+ logger.info("No long-term memories eligible for compression.")
126
+ return result
127
+
128
+ groups = self._build_groups(candidates)
129
+ eligible = [g for g in groups if len(g.entries) >= self.min_group_size]
130
+ result.groups_found = len(eligible)
131
+
132
+ if not eligible:
133
+ logger.info(
134
+ "Found %d candidate memories but no tag group reached the "
135
+ "minimum size of %d.",
136
+ len(candidates),
137
+ self.min_group_size,
138
+ )
139
+ return result
140
+
141
+ llm_bridge = bridge or self._make_bridge()
142
+
143
+ # Track IDs already processed so memories shared across tags aren't
144
+ # double-compressed when groups overlap.
145
+ processed_ids: set[str] = set()
146
+
147
+ # Process largest groups first for maximum coverage.
148
+ for group in sorted(eligible, key=lambda g: len(g.entries), reverse=True):
149
+ unprocessed = [e for e in group.entries if e.memory_id not in processed_ids]
150
+ if len(unprocessed) < self.min_group_size:
151
+ # After excluding already-handled memories this group is too small.
152
+ continue
153
+
154
+ if dry_run:
155
+ result.groups_found += 0 # already counted
156
+ result.memories_compressed += len(unprocessed)
157
+ for e in unprocessed:
158
+ processed_ids.add(e.memory_id)
159
+ continue
160
+
161
+ synthesized_id = self._compress_group(group, unprocessed, llm_bridge, result)
162
+ if synthesized_id:
163
+ result.groups_compressed += 1
164
+ result.memories_compressed += len(unprocessed)
165
+ result.compressed_ids.append(synthesized_id)
166
+ for e in unprocessed:
167
+ processed_ids.add(e.memory_id)
168
+
169
+ return result
170
+
171
+ def find_eligible(self) -> list[CompressionGroup]:
172
+ """Return tag groups eligible for compression (dry-run helper).
173
+
174
+ Returns:
175
+ List of :class:`CompressionGroup` with >= min_group_size entries,
176
+ sorted largest first.
177
+ """
178
+ candidates = self._find_candidates()
179
+ groups = self._build_groups(candidates)
180
+ eligible = [g for g in groups if len(g.entries) >= self.min_group_size]
181
+ return sorted(eligible, key=lambda g: len(g.entries), reverse=True)
182
+
183
+ # ── Private helpers ─────────────────────────────────────────────────────
184
+
185
+ def _find_candidates(self) -> list[MemoryEntry]:
186
+ """Load long-term memories older than age_days, skip compressed ones."""
187
+ cutoff = datetime.now(timezone.utc) - timedelta(days=self.age_days)
188
+ all_lt = list_memories(self.home, layer=MemoryLayer.LONG_TERM, limit=10000)
189
+
190
+ candidates = []
191
+ for entry in all_lt:
192
+ if COMPRESSED_TAG in entry.tags:
193
+ continue
194
+ if entry.created_at is None:
195
+ continue
196
+ # Normalize timezone: memories may be stored as naive UTC.
197
+ created = entry.created_at
198
+ if created.tzinfo is None:
199
+ created = created.replace(tzinfo=timezone.utc)
200
+ if created < cutoff:
201
+ candidates.append(entry)
202
+
203
+ logger.debug(
204
+ "Compression candidates: %d / %d long-term memories older than %d days.",
205
+ len(candidates),
206
+ len(all_lt),
207
+ self.age_days,
208
+ )
209
+ return candidates
210
+
211
+ def _build_groups(self, memories: list[MemoryEntry]) -> list[CompressionGroup]:
212
+ """Group memories by shared tags.
213
+
214
+ Each unique tag becomes a group. A memory with multiple tags
215
+ may appear in several groups; overlap is resolved by
216
+ ``compress()`` via ``processed_ids`` tracking.
217
+
218
+ Args:
219
+ memories: Candidate memory entries.
220
+
221
+ Returns:
222
+ List of CompressionGroup objects.
223
+ """
224
+ tag_map: dict[str, list[MemoryEntry]] = {}
225
+ for entry in memories:
226
+ for tag in entry.tags:
227
+ tag_map.setdefault(tag, []).append(entry)
228
+
229
+ return [CompressionGroup(tag=tag, entries=entries) for tag, entries in tag_map.items()]
230
+
231
+ def _compress_group(
232
+ self,
233
+ group: CompressionGroup,
234
+ entries: list[MemoryEntry],
235
+ bridge,
236
+ result: CompressionResult,
237
+ ) -> Optional[str]:
238
+ """Synthesize a group into one memory and delete the originals.
239
+
240
+ Args:
241
+ group: The CompressionGroup being processed.
242
+ entries: The specific entries to compress (may be a subset
243
+ of group.entries after overlap removal).
244
+ bridge: LLMBridge instance.
245
+ result: CompressionResult to append errors to.
246
+
247
+ Returns:
248
+ The new memory ID if successful, else None.
249
+ """
250
+ prompt = self._build_prompt(group.tag, entries)
251
+ try:
252
+ synthesized_text = self._call_llm(bridge, prompt, group.tag, len(entries))
253
+ except Exception as exc:
254
+ msg = f"LLM call failed for tag '{group.tag}': {exc}"
255
+ logger.warning(msg)
256
+ result.errors.append(msg)
257
+ return None
258
+
259
+ # Merge all original tags (union) and add the compressed marker.
260
+ merged_tags: list[str] = sorted(
261
+ {t for e in entries for t in e.tags} | {COMPRESSED_TAG}
262
+ )
263
+
264
+ # Take the highest importance from the group.
265
+ max_importance = max(e.importance for e in entries)
266
+ # Synthesized memory earns at least 0.85 since it distils many.
267
+ importance = max(max_importance, 0.85)
268
+
269
+ # Earliest created_at as provenance metadata.
270
+ oldest_ts = min(
271
+ (e.created_at for e in entries if e.created_at is not None),
272
+ default=None,
273
+ )
274
+ metadata: dict = {
275
+ "compressed_from": [e.memory_id for e in entries],
276
+ "compressed_tag": group.tag,
277
+ "compressed_count": len(entries),
278
+ }
279
+ if oldest_ts:
280
+ metadata["oldest_source_created_at"] = oldest_ts.isoformat()
281
+
282
+ try:
283
+ new_entry = store(
284
+ home=self.home,
285
+ content=synthesized_text,
286
+ tags=merged_tags,
287
+ source="compressor",
288
+ importance=importance,
289
+ layer=MemoryLayer.LONG_TERM,
290
+ metadata=metadata,
291
+ )
292
+ except Exception as exc:
293
+ msg = f"Failed to store synthesized memory for tag '{group.tag}': {exc}"
294
+ logger.warning(msg)
295
+ result.errors.append(msg)
296
+ return None
297
+
298
+ # Remove originals.
299
+ for entry in entries:
300
+ try:
301
+ delete(self.home, entry.memory_id)
302
+ except Exception as exc:
303
+ logger.warning("Failed to delete original memory %s: %s", entry.memory_id, exc)
304
+
305
+ logger.info(
306
+ "Compressed %d memories (tag=%s) → %s",
307
+ len(entries),
308
+ group.tag,
309
+ new_entry.memory_id,
310
+ )
311
+ return new_entry.memory_id
312
+
313
+ def _build_prompt(self, tag: str, entries: list[MemoryEntry]) -> str:
314
+ """Format compression prompt for the LLM.
315
+
316
+ Args:
317
+ tag: The grouping tag (context label).
318
+ entries: Memory entries to synthesize.
319
+
320
+ Returns:
321
+ Formatted prompt string.
322
+ """
323
+ lines = [
324
+ f"Compress the following {len(entries)} memories tagged '{tag}' into one:",
325
+ "",
326
+ ]
327
+ for i, entry in enumerate(entries, start=1):
328
+ created_label = ""
329
+ if entry.created_at:
330
+ created_label = f" [{entry.created_at.strftime('%Y-%m-%d')}]"
331
+ lines.append(f"Memory {i}{created_label}:")
332
+ lines.append(entry.content.strip())
333
+ lines.append("")
334
+
335
+ lines.append(
336
+ "Write a single comprehensive memory that preserves all key facts and "
337
+ "decisions. Continuous prose, no lists or headers, max 400 words."
338
+ )
339
+ return "\n".join(lines)
340
+
341
+ def _call_llm(self, bridge, prompt: str, tag: str, n: int) -> str:
342
+ """Invoke LLMBridge.generate() with the synthesis prompt.
343
+
344
+ Args:
345
+ bridge: LLMBridge instance.
346
+ prompt: User prompt built by _build_prompt.
347
+ tag: Tag name (for TaskSignal description).
348
+ n: Number of memories being merged (for token estimate).
349
+
350
+ Returns:
351
+ Generated synthesized memory text.
352
+ """
353
+ try:
354
+ from .model_router import TaskSignal
355
+ signal = TaskSignal(
356
+ description=f"Compress {n} memories tagged '{tag}'",
357
+ tags=["compression", "memory", tag],
358
+ estimated_tokens=len(prompt) // 4 + 512,
359
+ )
360
+ return bridge.generate(_SYSTEM_PROMPT, prompt, signal)
361
+ except ImportError:
362
+ # model_router not available — call bridge without signal.
363
+ return bridge.generate(_SYSTEM_PROMPT, prompt)
364
+
365
+ def _make_bridge(self):
366
+ """Instantiate a default LLMBridge from ConsciousnessConfig.
367
+
368
+ Returns:
369
+ LLMBridge instance, or raises RuntimeError if unavailable.
370
+ """
371
+ try:
372
+ from .consciousness_loop import ConsciousnessConfig, LLMBridge
373
+ config = ConsciousnessConfig()
374
+ return LLMBridge(config)
375
+ except ImportError as exc:
376
+ raise RuntimeError(
377
+ "LLMBridge is not available. Install the consciousness_loop "
378
+ "dependency or pass a bridge= argument."
379
+ ) from exc
@@ -0,0 +1,256 @@
1
+ """
2
+ Memory Curator — analyze, score, tag, promote, and deduplicate memories.
3
+
4
+ Runs a curation pass over the agent's memory store, identifying:
5
+ - Promotion candidates (short->mid, mid->long based on access/importance)
6
+ - Missing tags (auto-tags from content analysis)
7
+ - Duplicate/near-duplicate memories to merge
8
+ - Importance re-scoring based on current context
9
+ - Summary statistics for each memory layer
10
+
11
+ Tool-agnostic: works from any terminal, MCP, or the REPL shell.
12
+
13
+ Usage:
14
+ skcapstone memory curate # full curation pass
15
+ skcapstone memory curate --dry-run # preview without changes
16
+ skcapstone memory curate --promote # only run promotions
17
+ skcapstone memory curate --dedupe # only run deduplication
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import hashlib
23
+ import re
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+ from typing import Optional
27
+
28
+ from .memory_engine import (
29
+ _entry_path,
30
+ _find_by_id,
31
+ _save_entry,
32
+ list_memories,
33
+ search,
34
+ )
35
+ from .models import MemoryEntry, MemoryLayer
36
+
37
+
38
+ @dataclass
39
+ class CurationResult:
40
+ """Results from a curation pass.
41
+
42
+ Attributes:
43
+ promoted: Memories promoted to a higher tier.
44
+ tagged: Memories that received new auto-tags.
45
+ deduped: Memory IDs that were identified as duplicates.
46
+ total_scanned: Total memories examined.
47
+ by_layer: Count per layer after curation.
48
+ """
49
+
50
+ promoted: list[str] = field(default_factory=list)
51
+ tagged: list[str] = field(default_factory=list)
52
+ deduped: list[str] = field(default_factory=list)
53
+ total_scanned: int = 0
54
+ by_layer: dict[str, int] = field(default_factory=dict)
55
+
56
+
57
+ # Auto-tagging patterns (same as session_capture for consistency)
58
+ _TAG_PATTERNS: list[tuple[re.Pattern, str]] = [
59
+ (re.compile(r"\bcapauth\b", re.I), "capauth"),
60
+ (re.compile(r"\bskcapstone\b", re.I), "skcapstone"),
61
+ (re.compile(r"\bskmemory\b", re.I), "skmemory"),
62
+ (re.compile(r"\bskcomm\b", re.I), "skcomm"),
63
+ (re.compile(r"\bskchat\b", re.I), "skchat"),
64
+ (re.compile(r"\bsyncthing\b", re.I), "syncthing"),
65
+ (re.compile(r"\bMCP\b", re.I), "mcp"),
66
+ (re.compile(r"\bPGP\b|\bGPG\b", re.I), "pgp"),
67
+ (re.compile(r"\bDocker\b", re.I), "docker"),
68
+ (re.compile(r"\barchitect", re.I), "architecture"),
69
+ (re.compile(r"\bdecid", re.I), "decision"),
70
+ (re.compile(r"\bsecur|\bencrypt", re.I), "security"),
71
+ (re.compile(r"\bdeploy|\brelease", re.I), "deployment"),
72
+ ]
73
+
74
+
75
+ class MemoryCurator:
76
+ """Curates the agent's memory store.
77
+
78
+ Runs analysis passes to improve memory quality: auto-tagging,
79
+ promotion candidates, deduplication, and importance re-scoring.
80
+
81
+ Args:
82
+ home: Agent home directory (~/.skcapstone).
83
+ """
84
+
85
+ def __init__(self, home: Path) -> None:
86
+ self.home = home
87
+
88
+ def curate(
89
+ self,
90
+ dry_run: bool = False,
91
+ promote: bool = True,
92
+ dedupe: bool = True,
93
+ auto_tag: bool = True,
94
+ ) -> CurationResult:
95
+ """Run a full curation pass.
96
+
97
+ Args:
98
+ dry_run: If True, report changes without applying them.
99
+ promote: Run the promotion pass.
100
+ dedupe: Run the deduplication pass.
101
+ auto_tag: Run the auto-tagging pass.
102
+
103
+ Returns:
104
+ CurationResult with all changes made (or proposed).
105
+ """
106
+ result = CurationResult()
107
+ all_memories = list_memories(self.home, limit=10000)
108
+ result.total_scanned = len(all_memories)
109
+
110
+ for layer in MemoryLayer:
111
+ count = sum(1 for m in all_memories if m.layer == layer)
112
+ result.by_layer[layer.value] = count
113
+
114
+ if auto_tag:
115
+ self._pass_auto_tag(all_memories, result, dry_run)
116
+
117
+ if promote:
118
+ self._pass_promote(all_memories, result, dry_run)
119
+
120
+ if dedupe:
121
+ self._pass_dedupe(all_memories, result, dry_run)
122
+
123
+ return result
124
+
125
+ def _pass_auto_tag(
126
+ self, memories: list[MemoryEntry], result: CurationResult, dry_run: bool
127
+ ) -> None:
128
+ """Add missing tags based on content analysis."""
129
+ for entry in memories:
130
+ new_tags = _suggest_tags(entry.content, entry.tags)
131
+ if new_tags:
132
+ if not dry_run:
133
+ entry.tags = list(set(entry.tags + new_tags))
134
+ _save_entry(self.home, entry)
135
+ result.tagged.append(entry.memory_id)
136
+
137
+ def _pass_promote(
138
+ self, memories: list[MemoryEntry], result: CurationResult, dry_run: bool
139
+ ) -> None:
140
+ """Promote memories that qualify for a higher tier."""
141
+ for entry in memories:
142
+ if not entry.should_promote:
143
+ continue
144
+
145
+ old_layer = entry.layer
146
+ if not dry_run:
147
+ old_path = _entry_path(self.home, entry)
148
+ if entry.layer == MemoryLayer.SHORT_TERM:
149
+ entry.layer = MemoryLayer.MID_TERM
150
+ elif entry.layer == MemoryLayer.MID_TERM:
151
+ entry.layer = MemoryLayer.LONG_TERM
152
+
153
+ if old_path.exists():
154
+ old_path.unlink()
155
+ _save_entry(self.home, entry)
156
+
157
+ result.promoted.append(entry.memory_id)
158
+
159
+ def _pass_dedupe(
160
+ self, memories: list[MemoryEntry], result: CurationResult, dry_run: bool
161
+ ) -> None:
162
+ """Identify and remove near-duplicate memories."""
163
+ seen: dict[str, str] = {}
164
+
165
+ sorted_memories = sorted(
166
+ memories,
167
+ key=lambda m: (
168
+ {"long-term": 0, "mid-term": 1, "short-term": 2}.get(m.layer.value, 3),
169
+ -m.importance,
170
+ -m.access_count,
171
+ ),
172
+ )
173
+
174
+ for entry in sorted_memories:
175
+ content_hash = _content_hash(entry.content)
176
+
177
+ if content_hash in seen:
178
+ result.deduped.append(entry.memory_id)
179
+ if not dry_run:
180
+ path = _entry_path(self.home, entry)
181
+ if path.exists():
182
+ path.unlink()
183
+ continue
184
+
185
+ seen[content_hash] = entry.memory_id
186
+
187
+ def get_stats(self) -> dict:
188
+ """Get curation-oriented statistics.
189
+
190
+ Returns:
191
+ Dict with layer counts, tag coverage, and quality metrics.
192
+ """
193
+ all_memories = list_memories(self.home, limit=10000)
194
+ total = len(all_memories)
195
+ if total == 0:
196
+ return {"total": 0, "layers": {}, "tag_coverage": 0.0, "promotion_candidates": 0}
197
+
198
+ by_layer = {}
199
+ for layer in MemoryLayer:
200
+ by_layer[layer.value] = sum(1 for m in all_memories if m.layer == layer)
201
+
202
+ tagged = sum(1 for m in all_memories if m.tags)
203
+ promotable = sum(1 for m in all_memories if m.should_promote)
204
+ avg_importance = sum(m.importance for m in all_memories) / total
205
+
206
+ top_tags: dict[str, int] = {}
207
+ for m in all_memories:
208
+ for t in m.tags:
209
+ top_tags[t] = top_tags.get(t, 0) + 1
210
+
211
+ sorted_tags = sorted(top_tags.items(), key=lambda x: -x[1])[:15]
212
+
213
+ return {
214
+ "total": total,
215
+ "layers": by_layer,
216
+ "tag_coverage": round(tagged / total, 2),
217
+ "avg_importance": round(avg_importance, 2),
218
+ "promotion_candidates": promotable,
219
+ "top_tags": sorted_tags,
220
+ }
221
+
222
+
223
+ def _suggest_tags(content: str, existing_tags: list[str]) -> list[str]:
224
+ """Suggest new tags based on content analysis.
225
+
226
+ Args:
227
+ content: Memory content text.
228
+ existing_tags: Already-applied tags.
229
+
230
+ Returns:
231
+ List of new tag suggestions (not already in existing_tags).
232
+ """
233
+ suggestions: list[str] = []
234
+ existing_set = set(existing_tags)
235
+
236
+ for pattern, tag in _TAG_PATTERNS:
237
+ if tag not in existing_set and pattern.search(content):
238
+ suggestions.append(tag)
239
+
240
+ return suggestions
241
+
242
+
243
+ def _content_hash(content: str) -> str:
244
+ """Generate a normalized hash for deduplication.
245
+
246
+ Normalizes whitespace and case before hashing to catch
247
+ near-identical content.
248
+
249
+ Args:
250
+ content: Memory content text.
251
+
252
+ Returns:
253
+ MD5 hex digest of the normalized content.
254
+ """
255
+ normalized = " ".join(content.lower().split())
256
+ return hashlib.md5(normalized.encode()).hexdigest()[:16]