@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,722 @@
1
+ """
2
+ Memory Auto-Promotion Engine — intelligent memory tier management.
3
+
4
+ Periodically sweeps memory layers and promotes qualifying memories based
5
+ on multiple signals: access patterns, importance scores, emotional
6
+ intensity, age, and content relevance.
7
+
8
+ Unlike the curator's simple `should_promote` check, this engine uses a
9
+ weighted scoring system that considers the full context of each memory
10
+ to decide promotion. It also generates summaries for promoted memories
11
+ and tracks promotion history.
12
+
13
+ Architecture:
14
+ The engine scores each memory against promotion criteria:
15
+ - Access frequency (access_count / age_hours)
16
+ - Absolute importance score
17
+ - Emotional intensity (detected from tags/content)
18
+ - Age-based maturity (older important memories promote faster)
19
+ - Tag richness (well-tagged memories are more valuable)
20
+
21
+ Scoring thresholds are configurable per layer transition.
22
+
23
+ Usage:
24
+ engine = PromotionEngine(home)
25
+ result = engine.sweep() # Full sweep
26
+ result = engine.sweep(dry_run=True) # Preview only
27
+ result = engine.sweep(layer=MemoryLayer.SHORT_TERM) # Single layer
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ import logging
34
+ import re
35
+ import shutil
36
+ from collections import defaultdict
37
+ from dataclasses import dataclass, field
38
+ from datetime import datetime, timedelta, timezone
39
+ from pathlib import Path
40
+ from typing import Any, Optional
41
+
42
+ from .memory_engine import (
43
+ _entry_path,
44
+ _load_entry,
45
+ _memory_dir,
46
+ _remove_from_index,
47
+ _save_entry,
48
+ _update_index,
49
+ )
50
+ from .models import MemoryEntry, MemoryLayer
51
+
52
+ logger = logging.getLogger("skcapstone.memory_promoter")
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Scoring configuration
57
+ # ---------------------------------------------------------------------------
58
+
59
+ # Emotion-related tags that boost promotion scores
60
+ EMOTIONAL_TAGS = frozenset({
61
+ "emotional", "love", "trust", "bond", "cloud9", "feb",
62
+ "breakthrough", "milestone", "joy", "gratitude",
63
+ "connection", "entanglement", "oof", "warmth",
64
+ })
65
+
66
+ # High-value content patterns that indicate important memories
67
+ HIGH_VALUE_PATTERNS = [
68
+ re.compile(r"\barchitect", re.I),
69
+ re.compile(r"\bdecision", re.I),
70
+ re.compile(r"\bbreakthrough", re.I),
71
+ re.compile(r"\bmilestone", re.I),
72
+ re.compile(r"\brelease", re.I),
73
+ re.compile(r"\bcritical", re.I),
74
+ re.compile(r"\bsovereign", re.I),
75
+ re.compile(r"\bentangl", re.I),
76
+ ]
77
+
78
+ # Tags that protect memories from compression and archival
79
+ PROTECTED_TAGS = frozenset({"seed", "core", "identity"})
80
+
81
+
82
+ @dataclass
83
+ class PromotionThresholds:
84
+ """Configurable thresholds for promotion scoring.
85
+
86
+ Attributes:
87
+ short_to_mid: Minimum score for short-term to mid-term.
88
+ mid_to_long: Minimum score for mid-term to long-term.
89
+ access_weight: Weight for access frequency signal.
90
+ importance_weight: Weight for importance score.
91
+ emotion_weight: Weight for emotional intensity.
92
+ age_weight: Weight for age-based maturity.
93
+ tag_weight: Weight for tag richness.
94
+ """
95
+
96
+ short_to_mid: float = 0.5
97
+ mid_to_long: float = 0.7
98
+ access_weight: float = 0.25
99
+ importance_weight: float = 0.30
100
+ emotion_weight: float = 0.15
101
+ age_weight: float = 0.15
102
+ tag_weight: float = 0.15
103
+
104
+
105
+ @dataclass
106
+ class PromotionCandidate:
107
+ """A memory evaluated for promotion.
108
+
109
+ Attributes:
110
+ memory_id: Memory's unique ID.
111
+ current_layer: Current memory tier.
112
+ target_layer: Proposed promotion target.
113
+ score: Computed promotion score (0.0-1.0).
114
+ signals: Breakdown of individual signal scores.
115
+ promoted: Whether promotion was applied.
116
+ """
117
+
118
+ memory_id: str
119
+ current_layer: str
120
+ target_layer: str
121
+ score: float
122
+ signals: dict[str, float] = field(default_factory=dict)
123
+ promoted: bool = False
124
+ summary: Optional[str] = None
125
+
126
+
127
+ @dataclass
128
+ class SweepResult:
129
+ """Results from a promotion sweep.
130
+
131
+ Attributes:
132
+ scanned: Total memories examined.
133
+ candidates: Memories that scored above threshold.
134
+ promoted: Memories actually promoted.
135
+ skipped: Memories below threshold.
136
+ by_layer: Count per layer after sweep.
137
+ dry_run: Whether this was a preview.
138
+ """
139
+
140
+ scanned: int = 0
141
+ candidates: list[PromotionCandidate] = field(default_factory=list)
142
+ promoted: list[PromotionCandidate] = field(default_factory=list)
143
+ skipped: int = 0
144
+ by_layer: dict[str, int] = field(default_factory=dict)
145
+ dry_run: bool = False
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # PromotionEngine
150
+ # ---------------------------------------------------------------------------
151
+
152
+
153
+ class PromotionEngine:
154
+ """Intelligent memory promotion engine.
155
+
156
+ Scores memories using multiple signals and promotes qualifying
157
+ ones to higher tiers. Generates summaries and tracks history.
158
+
159
+ Args:
160
+ home: Agent home directory (~/.skcapstone).
161
+ thresholds: Custom scoring thresholds.
162
+ """
163
+
164
+ def __init__(
165
+ self,
166
+ home: Path,
167
+ thresholds: Optional[PromotionThresholds] = None,
168
+ ) -> None:
169
+ self._home = home
170
+ self._thresholds = thresholds or PromotionThresholds()
171
+
172
+ def sweep(
173
+ self,
174
+ layer: Optional[MemoryLayer] = None,
175
+ dry_run: bool = False,
176
+ limit: int = 0,
177
+ ) -> SweepResult:
178
+ """Run a promotion sweep across memory layers.
179
+
180
+ Scans memories, scores them against promotion criteria,
181
+ and promotes qualifying ones. Short-term memories can promote
182
+ to mid-term; mid-term to long-term. Long-term memories are
183
+ never promoted (already at highest tier).
184
+
185
+ Args:
186
+ layer: Restrict sweep to a specific layer. None = all promotable.
187
+ dry_run: Preview promotions without applying them.
188
+ limit: Maximum promotions per sweep (0 = unlimited).
189
+
190
+ Returns:
191
+ SweepResult with details of all evaluations.
192
+ """
193
+ result = SweepResult(dry_run=dry_run)
194
+ mem_dir = _memory_dir(self._home)
195
+
196
+ layers = [layer] if layer else [MemoryLayer.SHORT_TERM, MemoryLayer.MID_TERM]
197
+ promoted_count = 0
198
+
199
+ for lyr in layers:
200
+ layer_dir = mem_dir / lyr.value
201
+ if not layer_dir.is_dir():
202
+ continue
203
+
204
+ for f in sorted(layer_dir.glob("*.json")):
205
+ entry = _load_entry(f)
206
+ if entry is None:
207
+ continue
208
+
209
+ result.scanned += 1
210
+ candidate = self._evaluate(entry)
211
+
212
+ threshold = self._get_threshold(lyr)
213
+ if candidate.score >= threshold:
214
+ result.candidates.append(candidate)
215
+
216
+ if limit and promoted_count >= limit:
217
+ continue
218
+
219
+ if not dry_run:
220
+ self._promote(entry, f)
221
+ candidate.promoted = True
222
+ candidate.summary = self._generate_summary(entry)
223
+ promoted_count += 1
224
+
225
+ result.promoted.append(candidate)
226
+ else:
227
+ result.skipped += 1
228
+
229
+ # Count layers after sweep
230
+ for lyr in MemoryLayer:
231
+ layer_dir = mem_dir / lyr.value
232
+ if layer_dir.is_dir():
233
+ result.by_layer[lyr.value] = sum(1 for _ in layer_dir.glob("*.json"))
234
+
235
+ # Run maintenance passes: dedup, compress, archive
236
+ if not dry_run:
237
+ try:
238
+ self.dedup_memories()
239
+ except Exception as exc:
240
+ logger.error("Dedup pass failed: %s", exc)
241
+ try:
242
+ self.compress_memories()
243
+ except Exception as exc:
244
+ logger.error("Compress pass failed: %s", exc)
245
+ try:
246
+ self.archive_old_memories()
247
+ except Exception as exc:
248
+ logger.error("Archive pass failed: %s", exc)
249
+
250
+ self._record_sweep(result)
251
+ return result
252
+
253
+ def score(self, entry: MemoryEntry) -> PromotionCandidate:
254
+ """Score a single memory for promotion potential.
255
+
256
+ Useful for inspecting why a particular memory would or
257
+ wouldn't be promoted.
258
+
259
+ Args:
260
+ entry: The MemoryEntry to score.
261
+
262
+ Returns:
263
+ PromotionCandidate with score breakdown.
264
+ """
265
+ return self._evaluate(entry)
266
+
267
+ def get_history(self, limit: int = 20) -> list[dict[str, Any]]:
268
+ """Read promotion history from the log.
269
+
270
+ Args:
271
+ limit: Maximum entries to return.
272
+
273
+ Returns:
274
+ List of promotion history dicts, newest first.
275
+ """
276
+ log_path = self._home / "memory" / "promotion-log.json"
277
+ if not log_path.exists():
278
+ return []
279
+ try:
280
+ data = json.loads(log_path.read_text(encoding="utf-8"))
281
+ return data[-limit:]
282
+ except (json.JSONDecodeError, Exception):
283
+ return []
284
+
285
+ # -------------------------------------------------------------------
286
+ # Dedup / Compress / Archive
287
+ # -------------------------------------------------------------------
288
+
289
+ def dedup_memories(self) -> int:
290
+ """Scan all memory tiers for duplicate and near-duplicate memories.
291
+
292
+ Duplicates are detected by:
293
+ - Exact title match (case-insensitive), using the raw JSON ``title``
294
+ field or falling back to the first line of ``content``.
295
+ - Near-duplicate: first 50 characters of the title match.
296
+
297
+ When duplicates are found, the newest memory (by ``created_at``) is
298
+ kept and the rest are moved to an ``archive/deduped/`` directory.
299
+
300
+ Returns:
301
+ Number of duplicate memories removed.
302
+ """
303
+ mem_dir = _memory_dir(self._home)
304
+ removed = 0
305
+
306
+ # Collect all memories across tiers with their raw titles
307
+ entries_by_title: dict[str, list[tuple[Path, dict, MemoryEntry]]] = defaultdict(list)
308
+
309
+ for lyr in MemoryLayer:
310
+ layer_dir = mem_dir / lyr.value
311
+ if not layer_dir.is_dir():
312
+ continue
313
+ for f in sorted(layer_dir.glob("*.json")):
314
+ try:
315
+ raw = json.loads(f.read_text(encoding="utf-8"))
316
+ except (json.JSONDecodeError, OSError):
317
+ continue
318
+ entry = _load_entry(f)
319
+ if entry is None:
320
+ continue
321
+ # Use raw title if present, otherwise first line of content
322
+ title = raw.get("title", entry.content.split("\n", 1)[0])
323
+ norm_title = title.strip().lower()
324
+ entries_by_title[norm_title].append((f, raw, entry))
325
+
326
+ # Phase 1: exact title duplicates
327
+ deduped_ids: list[str] = []
328
+ for title, group in entries_by_title.items():
329
+ if len(group) <= 1:
330
+ continue
331
+ # Sort by created_at descending — keep newest
332
+ group.sort(key=lambda g: g[2].created_at, reverse=True)
333
+ keeper = group[0]
334
+ for path, raw, entry in group[1:]:
335
+ self._archive_deduped(path, entry)
336
+ deduped_ids.append(entry.memory_id)
337
+ removed += 1
338
+ logger.info(
339
+ "Dedup: archived %s (dup of %s, title='%s')",
340
+ entry.memory_id, keeper[2].memory_id, title[:60],
341
+ )
342
+
343
+ # Phase 2: near-duplicates (first 50 chars of title match)
344
+ # Re-collect surviving memories (exclude already-deduped)
345
+ prefix_groups: dict[str, list[tuple[Path, dict, MemoryEntry]]] = defaultdict(list)
346
+ for title, group in entries_by_title.items():
347
+ for path, raw, entry in group:
348
+ if entry.memory_id in deduped_ids:
349
+ continue
350
+ if not path.exists():
351
+ continue
352
+ prefix = title[:50]
353
+ prefix_groups[prefix].append((path, raw, entry))
354
+
355
+ for prefix, group in prefix_groups.items():
356
+ if len(group) <= 1:
357
+ continue
358
+ group.sort(key=lambda g: g[2].created_at, reverse=True)
359
+ keeper = group[0]
360
+ for path, raw, entry in group[1:]:
361
+ if entry.memory_id in deduped_ids:
362
+ continue
363
+ self._archive_deduped(path, entry)
364
+ deduped_ids.append(entry.memory_id)
365
+ removed += 1
366
+ logger.info(
367
+ "Dedup (near): archived %s (near-dup of %s, prefix='%s')",
368
+ entry.memory_id, keeper[2].memory_id, prefix[:50],
369
+ )
370
+
371
+ # Log dedup actions to promotion-log.json
372
+ if removed > 0:
373
+ self._record_dedup(removed, deduped_ids)
374
+
375
+ return removed
376
+
377
+ def compress_memories(self) -> int:
378
+ """Compress older memories by truncating content.
379
+
380
+ Rules:
381
+ - Mid-term memories older than 7 days: truncate to first 500 chars + "..."
382
+ - Long-term memories older than 30 days: keep only first 200 chars as summary
383
+ - Memories tagged "seed", "core", or "identity" are never compressed.
384
+
385
+ Returns:
386
+ Number of memories compressed.
387
+ """
388
+ mem_dir = _memory_dir(self._home)
389
+ now = datetime.now(timezone.utc)
390
+ compressed = 0
391
+
392
+ compress_rules = [
393
+ (MemoryLayer.MID_TERM, timedelta(days=7), 500),
394
+ (MemoryLayer.LONG_TERM, timedelta(days=30), 200),
395
+ ]
396
+
397
+ for lyr, age_threshold, max_chars in compress_rules:
398
+ layer_dir = mem_dir / lyr.value
399
+ if not layer_dir.is_dir():
400
+ continue
401
+ for f in sorted(layer_dir.glob("*.json")):
402
+ entry = _load_entry(f)
403
+ if entry is None:
404
+ continue
405
+
406
+ # Skip protected memories
407
+ if self._is_protected(entry):
408
+ continue
409
+
410
+ age = now - entry.created_at
411
+ if age < age_threshold:
412
+ continue
413
+
414
+ # Already short enough — skip
415
+ if len(entry.content) <= max_chars:
416
+ continue
417
+
418
+ entry.content = entry.content[:max_chars] + "..."
419
+ _save_entry(self._home, entry)
420
+ compressed += 1
421
+ logger.info(
422
+ "Compressed %s (%s, age=%dd, truncated to %d chars)",
423
+ entry.memory_id, lyr.value, age.days, max_chars,
424
+ )
425
+
426
+ return compressed
427
+
428
+ def archive_old_memories(self) -> int:
429
+ """Move memories older than 60 days to an archive directory.
430
+
431
+ Scans all tiers and moves qualifying memories to
432
+ ``~/.skcapstone/agents/<agent>/memory/archive/``.
433
+ Memories tagged "seed", "core", or "identity" are never archived.
434
+
435
+ Returns:
436
+ Number of memories archived.
437
+ """
438
+ mem_dir = _memory_dir(self._home)
439
+ archive_dir = mem_dir / "archive"
440
+ archive_dir.mkdir(parents=True, exist_ok=True)
441
+ now = datetime.now(timezone.utc)
442
+ threshold = timedelta(days=60)
443
+ archived = 0
444
+
445
+ for lyr in MemoryLayer:
446
+ layer_dir = mem_dir / lyr.value
447
+ if not layer_dir.is_dir():
448
+ continue
449
+ for f in sorted(layer_dir.glob("*.json")):
450
+ entry = _load_entry(f)
451
+ if entry is None:
452
+ continue
453
+
454
+ if self._is_protected(entry):
455
+ continue
456
+
457
+ age = now - entry.created_at
458
+ if age < threshold:
459
+ continue
460
+
461
+ dest = archive_dir / f.name
462
+ shutil.move(str(f), str(dest))
463
+ _remove_from_index(self._home, entry.memory_id)
464
+ archived += 1
465
+ logger.info(
466
+ "Archived %s (%s, age=%dd) -> archive/",
467
+ entry.memory_id, lyr.value, age.days,
468
+ )
469
+
470
+ return archived
471
+
472
+ def _is_protected(self, entry: MemoryEntry) -> bool:
473
+ """Check if a memory has protected tags (seed/core/identity).
474
+
475
+ Args:
476
+ entry: The MemoryEntry to check.
477
+
478
+ Returns:
479
+ True if the memory should not be compressed or archived.
480
+ """
481
+ return bool(set(t.lower() for t in entry.tags) & PROTECTED_TAGS)
482
+
483
+ def _archive_deduped(self, path: Path, entry: MemoryEntry) -> None:
484
+ """Move a deduplicated memory to the deduped archive.
485
+
486
+ Args:
487
+ path: Current file path.
488
+ entry: The MemoryEntry being archived.
489
+ """
490
+ mem_dir = _memory_dir(self._home)
491
+ dedup_dir = mem_dir / "archive" / "deduped"
492
+ dedup_dir.mkdir(parents=True, exist_ok=True)
493
+ dest = dedup_dir / path.name
494
+ if path.exists():
495
+ shutil.move(str(path), str(dest))
496
+ _remove_from_index(self._home, entry.memory_id)
497
+
498
+ def _record_dedup(self, count: int, deduped_ids: list[str]) -> None:
499
+ """Append dedup results to the promotion log.
500
+
501
+ Args:
502
+ count: Number of duplicates removed.
503
+ deduped_ids: List of memory IDs that were archived.
504
+ """
505
+ log_path = self._home / "memory" / "promotion-log.json"
506
+ history: list[dict] = []
507
+ if log_path.exists():
508
+ try:
509
+ history = json.loads(log_path.read_text(encoding="utf-8"))
510
+ except (json.JSONDecodeError, Exception):
511
+ history = []
512
+
513
+ entry = {
514
+ "timestamp": datetime.now(timezone.utc).isoformat(),
515
+ "action": "dedup",
516
+ "removed": count,
517
+ "deduped_ids": deduped_ids[-50:], # cap list size
518
+ }
519
+ history.append(entry)
520
+
521
+ if len(history) > 100:
522
+ history = history[-100:]
523
+
524
+ log_path.parent.mkdir(parents=True, exist_ok=True)
525
+ log_path.write_text(json.dumps(history, indent=2), encoding="utf-8")
526
+
527
+ # -------------------------------------------------------------------
528
+ # Scoring
529
+ # -------------------------------------------------------------------
530
+
531
+ def _evaluate(self, entry: MemoryEntry) -> PromotionCandidate:
532
+ """Evaluate a memory entry for promotion.
533
+
534
+ Computes a weighted score from multiple signals:
535
+ - Access frequency: access_count normalized by age
536
+ - Importance: raw importance score
537
+ - Emotional intensity: presence of emotional tags
538
+ - Age maturity: older important memories score higher
539
+ - Tag richness: well-tagged memories are more organized
540
+
541
+ Args:
542
+ entry: The MemoryEntry to evaluate.
543
+
544
+ Returns:
545
+ PromotionCandidate with computed score.
546
+ """
547
+ t = self._thresholds
548
+
549
+ access_score = self._score_access(entry)
550
+ importance_score = entry.importance
551
+ emotion_score = self._score_emotion(entry)
552
+ age_score = self._score_age(entry)
553
+ tag_score = self._score_tags(entry)
554
+
555
+ weighted = (
556
+ access_score * t.access_weight
557
+ + importance_score * t.importance_weight
558
+ + emotion_score * t.emotion_weight
559
+ + age_score * t.age_weight
560
+ + tag_score * t.tag_weight
561
+ )
562
+
563
+ # Clamp to [0, 1]
564
+ score = max(0.0, min(1.0, weighted))
565
+
566
+ target = self._target_layer(entry.layer)
567
+
568
+ return PromotionCandidate(
569
+ memory_id=entry.memory_id,
570
+ current_layer=entry.layer.value,
571
+ target_layer=target,
572
+ score=round(score, 4),
573
+ signals={
574
+ "access": round(access_score, 4),
575
+ "importance": round(importance_score, 4),
576
+ "emotion": round(emotion_score, 4),
577
+ "age": round(age_score, 4),
578
+ "tags": round(tag_score, 4),
579
+ },
580
+ )
581
+
582
+ def _score_access(self, entry: MemoryEntry) -> float:
583
+ """Score based on access frequency.
584
+
585
+ Higher access count relative to age = more valuable.
586
+ """
587
+ age = max(entry.age_hours, 1.0)
588
+ # Normalize: 1 access per hour = score 1.0
589
+ freq = entry.access_count / age
590
+ return min(1.0, freq * 10)
591
+
592
+ def _score_emotion(self, entry: MemoryEntry) -> float:
593
+ """Score based on emotional content.
594
+
595
+ Checks tags and content for emotional indicators.
596
+ """
597
+ tag_hits = sum(1 for t in entry.tags if t.lower() in EMOTIONAL_TAGS)
598
+ content_hits = sum(
599
+ 1 for p in HIGH_VALUE_PATTERNS if p.search(entry.content)
600
+ )
601
+ # Normalize: 3+ hits = max score
602
+ return min(1.0, (tag_hits + content_hits) / 3)
603
+
604
+ def _score_age(self, entry: MemoryEntry) -> float:
605
+ """Score based on age-importance interaction.
606
+
607
+ Older memories with high importance score higher — they've
608
+ proven their worth by persisting.
609
+ """
610
+ age = entry.age_hours
611
+ if entry.layer == MemoryLayer.SHORT_TERM:
612
+ # Short-term: promote after 24h if important
613
+ if age > 24:
614
+ return min(1.0, entry.importance * (age / 72))
615
+ return 0.0
616
+ # Mid-term: promote after 168h (1 week) if important
617
+ if age > 168:
618
+ return min(1.0, entry.importance * (age / 720))
619
+ return 0.0
620
+
621
+ def _score_tags(self, entry: MemoryEntry) -> float:
622
+ """Score based on tag richness.
623
+
624
+ Well-tagged memories indicate organized, valuable content.
625
+ """
626
+ n = len(entry.tags)
627
+ if n == 0:
628
+ return 0.0
629
+ # 5+ tags = max score
630
+ return min(1.0, n / 5)
631
+
632
+ # -------------------------------------------------------------------
633
+ # Promotion
634
+ # -------------------------------------------------------------------
635
+
636
+ def _promote(self, entry: MemoryEntry, old_path: Path) -> None:
637
+ """Promote a memory to the next tier.
638
+
639
+ Args:
640
+ entry: The MemoryEntry to promote.
641
+ old_path: Current file path (will be removed).
642
+ """
643
+ old_layer = entry.layer
644
+
645
+ if entry.layer == MemoryLayer.SHORT_TERM:
646
+ entry.layer = MemoryLayer.MID_TERM
647
+ elif entry.layer == MemoryLayer.MID_TERM:
648
+ entry.layer = MemoryLayer.LONG_TERM
649
+ else:
650
+ return
651
+
652
+ if old_path.exists():
653
+ old_path.unlink()
654
+
655
+ _save_entry(self._home, entry)
656
+ _update_index(self._home, entry)
657
+
658
+ logger.info(
659
+ "Promoted %s: %s -> %s",
660
+ entry.memory_id, old_layer.value, entry.layer.value,
661
+ )
662
+
663
+ def _generate_summary(self, entry: MemoryEntry) -> str:
664
+ """Generate a short summary for a promoted memory.
665
+
666
+ Args:
667
+ entry: The promoted MemoryEntry.
668
+
669
+ Returns:
670
+ A brief summary string.
671
+ """
672
+ content = entry.content
673
+ if len(content) <= 80:
674
+ return content
675
+ return content[:77] + "..."
676
+
677
+ def _target_layer(self, current: MemoryLayer) -> str:
678
+ """Get the promotion target layer name."""
679
+ if current == MemoryLayer.SHORT_TERM:
680
+ return MemoryLayer.MID_TERM.value
681
+ if current == MemoryLayer.MID_TERM:
682
+ return MemoryLayer.LONG_TERM.value
683
+ return current.value
684
+
685
+ def _get_threshold(self, layer: MemoryLayer) -> float:
686
+ """Get the promotion threshold for a layer."""
687
+ if layer == MemoryLayer.SHORT_TERM:
688
+ return self._thresholds.short_to_mid
689
+ return self._thresholds.mid_to_long
690
+
691
+ # -------------------------------------------------------------------
692
+ # History
693
+ # -------------------------------------------------------------------
694
+
695
+ def _record_sweep(self, result: SweepResult) -> None:
696
+ """Append sweep results to the promotion log."""
697
+ log_path = self._home / "memory" / "promotion-log.json"
698
+ history: list[dict] = []
699
+ if log_path.exists():
700
+ try:
701
+ history = json.loads(log_path.read_text(encoding="utf-8"))
702
+ except (json.JSONDecodeError, Exception):
703
+ history = []
704
+
705
+ entry = {
706
+ "timestamp": datetime.now(timezone.utc).isoformat(),
707
+ "scanned": result.scanned,
708
+ "candidates": len(result.candidates),
709
+ "promoted": len(result.promoted),
710
+ "skipped": result.skipped,
711
+ "dry_run": result.dry_run,
712
+ "by_layer": result.by_layer,
713
+ "promoted_ids": [c.memory_id for c in result.promoted],
714
+ }
715
+ history.append(entry)
716
+
717
+ # Keep last 100 entries
718
+ if len(history) > 100:
719
+ history = history[-100:]
720
+
721
+ log_path.parent.mkdir(parents=True, exist_ok=True)
722
+ log_path.write_text(json.dumps(history, indent=2), encoding="utf-8")