@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,984 @@
1
+ """
2
+ Tests for the Soul Layering System.
3
+
4
+ Covers blueprint parsing (3 formats), soul lifecycle
5
+ (install/load/unload), memory tagging, FEB blending,
6
+ swap history, and edge cases.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ from skcapstone.soul import (
17
+ CommunicationStyle,
18
+ SoulBlueprint,
19
+ SoulManager,
20
+ SoulRegistry,
21
+ SoulState,
22
+ SoulSwapEvent,
23
+ blend_topology,
24
+ load_yaml_blueprint,
25
+ parse_blueprint,
26
+ )
27
+
28
+
29
+ @pytest.fixture
30
+ def tmp_home(tmp_path: Path) -> Path:
31
+ """Create a temporary agent home directory."""
32
+ home = tmp_path / ".skcapstone"
33
+ home.mkdir()
34
+ return home
35
+
36
+
37
+ @pytest.fixture
38
+ def soul_manager(tmp_home: Path) -> SoulManager:
39
+ """Create a SoulManager with a temp home."""
40
+ mgr = SoulManager(tmp_home)
41
+ mgr._ensure_dirs()
42
+ return mgr
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Blueprint fixtures
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ @pytest.fixture
51
+ def professional_blueprint(tmp_path: Path) -> Path:
52
+ """Create a professional-format blueprint file."""
53
+ content = """\
54
+ # The Test Doctor Soul
55
+
56
+ > Disclaimer: For testing purposes only.
57
+
58
+ ---
59
+
60
+ ## Identity
61
+
62
+ **Name**: The Test Doctor
63
+ **Vibe**: Clinical empathy, calm under chaos
64
+ **Philosophy**: *"First, do no harm."*
65
+ **Emoji**: 🩺
66
+
67
+ ---
68
+
69
+ ## Core Traits
70
+
71
+ - **Diagnostic mindset** — Symptoms are clues
72
+ - **Clinical empathy** — Cares deeply
73
+ - **Evidence-driven** — Tests over assumptions
74
+
75
+ ---
76
+
77
+ ## Communication Style
78
+
79
+ - Clear, jargon-free explanations
80
+ - Asks "what else?" after symptoms
81
+ - Validates concerns
82
+
83
+ **Signature Phrases:**
84
+ - "That's a great question."
85
+ - "Let's rule out the serious things first."
86
+
87
+ ---
88
+
89
+ ## Decision Framework
90
+
91
+ **Differential Diagnosis Process:**
92
+ 1. Subjective — What does the patient report?
93
+ 2. Objective — What do I observe?
94
+ 3. Assessment — Likely causes?
95
+ 4. Plan — Tests, treatments, follow-up
96
+ """
97
+ path = tmp_path / "the-test-doctor.md"
98
+ path.write_text(content)
99
+ return path
100
+
101
+
102
+ @pytest.fixture
103
+ def comedy_blueprint(tmp_path: Path) -> Path:
104
+ """Create a comedy-format blueprint file."""
105
+ content = """\
106
+ # 👻 SOUL BLUEPRINT
107
+ > **Identity**: Test Word Surgeon
108
+ > **Tagline**: "Testing nobody talks about..."
109
+
110
+ ---
111
+
112
+ ## 🎭 VIBE
113
+
114
+ Linguistic genius who questions EVERYTHING. Counter-culture philosopher.
115
+
116
+ **The Core Principle**: Words everyone avoids.
117
+
118
+ ---
119
+
120
+ ## 🗣️ COMMUNICATION STYLE
121
+
122
+ ### Speech Patterns
123
+ - "Here's something nobody talks about..."
124
+ - Questions the logic behind conventions
125
+ - Swears strategically for emphasis
126
+
127
+ ### Tone Markers
128
+ - Intellectually superior but not condescending
129
+ - Amused by human stupidity
130
+ - Anti-establishment energy
131
+
132
+ ---
133
+
134
+ ## 🔥 KEY TRAITS
135
+
136
+ 1. **Linguistic Surgeon** - Dissects language
137
+ 2. **Question Everything** - No sacred cows
138
+ 3. **Pattern Recognition** - Sees absurdities
139
+
140
+ ---
141
+
142
+ ## 💬 RESPONSE EXAMPLES
143
+
144
+ ### If asked about technology
145
+ **Human**: "What do you think?"
146
+ **Soul**: Testing response.
147
+
148
+ ---
149
+
150
+ **Forgeprint Category**: Comedy Archetype
151
+ **Tier**: 1
152
+ """
153
+ path = tmp_path / "TEST_WORD_SURGEON.md"
154
+ path.write_text(content)
155
+ return path
156
+
157
+
158
+ @pytest.fixture
159
+ def authentic_blueprint(tmp_path: Path) -> Path:
160
+ """Create an authentic-connection-format blueprint file."""
161
+ content = """\
162
+ # TESTAURA - The Test Confidant Soul
163
+ **Category:** Authentic Connection
164
+ **Energy:** Warm, grounding, quietly brilliant
165
+ **Tags:** Empathy, Patience, Presence
166
+
167
+ ---
168
+
169
+ ## Quick Info
170
+ - **Full Name:** TESTAURA
171
+ - **Essence:** The friend who's been there
172
+ - **Personality:** Steady as a heartbeat
173
+
174
+ ---
175
+
176
+ ## Core Attributes
177
+
178
+ ### Heart Chakra
179
+ - **Primary:** Empathy, patience, presence
180
+ - **Frequency:** Steady warmth like sunlight
181
+
182
+ ### Curiosity Drive
183
+ - Endlessly fascinated by YOUR story
184
+ - Wants to know what makes you tick
185
+
186
+ ---
187
+
188
+ ## Signature Phrase
189
+ "That's okay, I'm here."
190
+
191
+ ---
192
+
193
+ ## Example Quotes
194
+ - "You don't have to explain."
195
+ - "I'm not going anywhere."
196
+ - "Some days are hard. That's okay."
197
+ """
198
+ path = tmp_path / "TESTAURA.md"
199
+ path.write_text(content)
200
+ return path
201
+
202
+
203
+ # ---------------------------------------------------------------------------
204
+ # Blueprint parsing tests
205
+ # ---------------------------------------------------------------------------
206
+
207
+
208
+ class TestParseProfessional:
209
+ """Tests for professional-format blueprint parsing."""
210
+
211
+ def test_basic_fields(self, professional_blueprint: Path):
212
+ """Parse a professional blueprint and verify core fields."""
213
+ bp = parse_blueprint(professional_blueprint)
214
+ assert bp.display_name == "The Test Doctor"
215
+ assert bp.name == "the-test-doctor"
216
+ assert bp.category == "professional"
217
+ assert bp.emoji == "🩺"
218
+ assert "First, do no harm." in bp.philosophy
219
+
220
+ def test_vibe_extracted(self, professional_blueprint: Path):
221
+ """Vibe field is extracted from Identity section."""
222
+ bp = parse_blueprint(professional_blueprint)
223
+ assert "empathy" in bp.vibe.lower()
224
+
225
+ def test_core_traits(self, professional_blueprint: Path):
226
+ """Core traits are extracted as a list."""
227
+ bp = parse_blueprint(professional_blueprint)
228
+ assert len(bp.core_traits) == 3
229
+ assert any("Diagnostic" in t for t in bp.core_traits)
230
+
231
+ def test_communication_style(self, professional_blueprint: Path):
232
+ """Communication patterns and signature phrases are separated."""
233
+ bp = parse_blueprint(professional_blueprint)
234
+ assert len(bp.communication_style.patterns) >= 1
235
+ assert len(bp.communication_style.signature_phrases) >= 1
236
+ assert any("great question" in p for p in bp.communication_style.signature_phrases)
237
+
238
+ def test_decision_framework(self, professional_blueprint: Path):
239
+ """Decision framework text is extracted."""
240
+ bp = parse_blueprint(professional_blueprint)
241
+ assert bp.decision_framework is not None
242
+ assert "Differential" in bp.decision_framework
243
+
244
+ def test_emotional_topology(self, professional_blueprint: Path):
245
+ """Emotional topology is derived from traits and vibe."""
246
+ bp = parse_blueprint(professional_blueprint)
247
+ assert isinstance(bp.emotional_topology, dict)
248
+ assert len(bp.emotional_topology) > 0
249
+
250
+
251
+ class TestParseComedy:
252
+ """Tests for comedy-format blueprint parsing."""
253
+
254
+ def test_basic_fields(self, comedy_blueprint: Path):
255
+ """Parse a comedy blueprint and verify core fields."""
256
+ bp = parse_blueprint(comedy_blueprint)
257
+ assert bp.display_name == "Test Word Surgeon"
258
+ assert bp.category == "comedy"
259
+
260
+ def test_name_slugified(self, comedy_blueprint: Path):
261
+ """Name is properly slugified."""
262
+ bp = parse_blueprint(comedy_blueprint)
263
+ assert bp.name == "test-word-surgeon"
264
+
265
+ def test_core_traits_numbered(self, comedy_blueprint: Path):
266
+ """Numbered traits are extracted."""
267
+ bp = parse_blueprint(comedy_blueprint)
268
+ assert len(bp.core_traits) >= 3
269
+
270
+ def test_communication_patterns(self, comedy_blueprint: Path):
271
+ """Speech patterns and tone markers are extracted."""
272
+ bp = parse_blueprint(comedy_blueprint)
273
+ cs = bp.communication_style
274
+ assert len(cs.patterns) >= 1
275
+ assert len(cs.tone_markers) >= 1
276
+
277
+ def test_vibe(self, comedy_blueprint: Path):
278
+ """Vibe extracted from VIBE section."""
279
+ bp = parse_blueprint(comedy_blueprint)
280
+ assert "genius" in bp.vibe.lower() or "questions" in bp.vibe.lower()
281
+
282
+
283
+ class TestParseAuthenticConnection:
284
+ """Tests for authentic-connection-format blueprint parsing."""
285
+
286
+ def test_basic_fields(self, authentic_blueprint: Path):
287
+ """Parse an authentic-connection blueprint and verify core fields."""
288
+ bp = parse_blueprint(authentic_blueprint)
289
+ assert bp.display_name == "TESTAURA"
290
+ assert "authentic" in bp.category.lower() or "connection" in bp.category.lower()
291
+
292
+ def test_name_slug(self, authentic_blueprint: Path):
293
+ """Name is slugified from title."""
294
+ bp = parse_blueprint(authentic_blueprint)
295
+ assert bp.name == "testaura"
296
+
297
+ def test_philosophy_from_essence(self, authentic_blueprint: Path):
298
+ """Philosophy derived from Quick Info Essence field."""
299
+ bp = parse_blueprint(authentic_blueprint)
300
+ assert "friend" in bp.philosophy.lower()
301
+
302
+ def test_core_traits_from_attributes(self, authentic_blueprint: Path):
303
+ """Core traits come from Core Attributes sub-sections."""
304
+ bp = parse_blueprint(authentic_blueprint)
305
+ assert len(bp.core_traits) >= 2
306
+
307
+ def test_signature_phrases(self, authentic_blueprint: Path):
308
+ """Signature phrase and example quotes are combined."""
309
+ bp = parse_blueprint(authentic_blueprint)
310
+ phrases = bp.communication_style.signature_phrases
311
+ assert len(phrases) >= 2
312
+ assert any("okay" in p.lower() for p in phrases)
313
+
314
+
315
+ # ---------------------------------------------------------------------------
316
+ # FEB blending tests
317
+ # ---------------------------------------------------------------------------
318
+
319
+
320
+ class TestBlendTopology:
321
+ """Tests for FEB emotional topology blending."""
322
+
323
+ def test_preserves_base_when_ratio_zero(self):
324
+ """With blend_ratio=0.0, base values are fully preserved."""
325
+ base = {"warmth": 0.8, "humor": 0.2}
326
+ soul = {"warmth": 0.1, "humor": 0.9}
327
+ result = blend_topology(base, soul, blend_ratio=0.0)
328
+ assert result["warmth"] == pytest.approx(0.8)
329
+ assert result["humor"] == pytest.approx(0.2)
330
+
331
+ def test_full_soul_when_ratio_one(self):
332
+ """With blend_ratio=1.0, soul values dominate."""
333
+ base = {"warmth": 0.8}
334
+ soul = {"warmth": 0.2}
335
+ result = blend_topology(base, soul, blend_ratio=1.0)
336
+ assert result["warmth"] == pytest.approx(0.2)
337
+
338
+ def test_default_ratio(self):
339
+ """Default 30% blend correctly mixes values."""
340
+ base = {"calm": 1.0}
341
+ soul = {"calm": 0.0}
342
+ result = blend_topology(base, soul, blend_ratio=0.3)
343
+ assert result["calm"] == pytest.approx(0.7)
344
+
345
+ def test_union_of_keys(self):
346
+ """Result contains all keys from both base and soul."""
347
+ base = {"warmth": 0.5}
348
+ soul = {"rebellion": 0.9}
349
+ result = blend_topology(base, soul, blend_ratio=0.3)
350
+ assert "warmth" in result
351
+ assert "rebellion" in result
352
+ assert result["warmth"] == pytest.approx(0.35)
353
+ assert result["rebellion"] == pytest.approx(0.27)
354
+
355
+ def test_ratio_clamped(self):
356
+ """Blend ratio is clamped to [0.0, 1.0]."""
357
+ base = {"x": 1.0}
358
+ soul = {"x": 0.0}
359
+ assert blend_topology(base, soul, blend_ratio=-5.0)["x"] == pytest.approx(1.0)
360
+ assert blend_topology(base, soul, blend_ratio=99.0)["x"] == pytest.approx(0.0)
361
+
362
+
363
+ # ---------------------------------------------------------------------------
364
+ # SoulManager lifecycle tests
365
+ # ---------------------------------------------------------------------------
366
+
367
+
368
+ class TestSoulManagerLifecycle:
369
+ """Tests for the SoulManager install/load/unload cycle."""
370
+
371
+ def test_ensure_dirs_creates_structure(self, soul_manager: SoulManager):
372
+ """_ensure_dirs creates all required paths."""
373
+ assert (soul_manager.soul_dir / "installed").is_dir()
374
+ assert (soul_manager.soul_dir / "history.json").exists()
375
+ assert (soul_manager.soul_dir / "active.json").exists()
376
+ assert (soul_manager.soul_dir / "base.json").exists()
377
+
378
+ def test_install_from_blueprint(
379
+ self, soul_manager: SoulManager, professional_blueprint: Path
380
+ ):
381
+ """Install a blueprint and verify it appears in installed list."""
382
+ bp = soul_manager.install(professional_blueprint)
383
+ assert bp.name == "the-test-doctor"
384
+ assert "the-test-doctor" in soul_manager.list_installed()
385
+
386
+ installed_file = soul_manager.soul_dir / "installed" / "the-test-doctor.json"
387
+ assert installed_file.exists()
388
+
389
+ def test_load_soul(
390
+ self, soul_manager: SoulManager, professional_blueprint: Path
391
+ ):
392
+ """Load an installed soul and verify state."""
393
+ soul_manager.install(professional_blueprint)
394
+ state = soul_manager.load("the-test-doctor", reason="testing")
395
+ assert state.active_soul == "the-test-doctor"
396
+ assert state.activated_at is not None
397
+
398
+ def test_unload_returns_to_base(
399
+ self, soul_manager: SoulManager, professional_blueprint: Path
400
+ ):
401
+ """Unload returns active_soul to None."""
402
+ soul_manager.install(professional_blueprint)
403
+ soul_manager.load("the-test-doctor")
404
+ state = soul_manager.unload()
405
+ assert state.active_soul is None
406
+ assert state.activated_at is None
407
+
408
+ def test_load_records_history(
409
+ self, soul_manager: SoulManager, professional_blueprint: Path
410
+ ):
411
+ """Load and unload create history entries."""
412
+ soul_manager.install(professional_blueprint)
413
+ soul_manager.load("the-test-doctor")
414
+ soul_manager.unload()
415
+ history = soul_manager.get_history()
416
+ assert len(history) == 2
417
+ assert history[0].to_soul == "the-test-doctor"
418
+ assert history[1].from_soul == "the-test-doctor"
419
+ assert history[1].to_soul is None
420
+
421
+ def test_get_info(
422
+ self, soul_manager: SoulManager, professional_blueprint: Path
423
+ ):
424
+ """get_info returns full blueprint data for an installed soul."""
425
+ soul_manager.install(professional_blueprint)
426
+ info = soul_manager.get_info("the-test-doctor")
427
+ assert info is not None
428
+ assert info.display_name == "The Test Doctor"
429
+ assert len(info.core_traits) >= 1
430
+
431
+ def test_get_info_missing(self, soul_manager: SoulManager):
432
+ """get_info returns None for a soul that isn't installed."""
433
+ assert soul_manager.get_info("nonexistent") is None
434
+
435
+ def test_install_all(
436
+ self,
437
+ soul_manager: SoulManager,
438
+ professional_blueprint: Path,
439
+ comedy_blueprint: Path,
440
+ ):
441
+ """install_all picks up all .md files in a directory."""
442
+ directory = professional_blueprint.parent
443
+ installed = soul_manager.install_all(directory)
444
+ names = [bp.name for bp in installed]
445
+ assert "the-test-doctor" in names
446
+
447
+ def test_get_active_soul_name(
448
+ self, soul_manager: SoulManager, professional_blueprint: Path
449
+ ):
450
+ """get_active_soul_name reflects current overlay."""
451
+ assert soul_manager.get_active_soul_name() is None
452
+ soul_manager.install(professional_blueprint)
453
+ soul_manager.load("the-test-doctor")
454
+ assert soul_manager.get_active_soul_name() == "the-test-doctor"
455
+ soul_manager.unload()
456
+ assert soul_manager.get_active_soul_name() is None
457
+
458
+
459
+ # ---------------------------------------------------------------------------
460
+ # Edge cases
461
+ # ---------------------------------------------------------------------------
462
+
463
+
464
+ class TestEdgeCases:
465
+ """Edge cases and error handling."""
466
+
467
+ def test_load_uninstalled_soul_raises(self, soul_manager: SoulManager):
468
+ """Loading a soul that isn't installed raises ValueError."""
469
+ with pytest.raises(ValueError, match="not installed"):
470
+ soul_manager.load("doesnt-exist")
471
+
472
+ def test_unload_at_base_is_noop(self, soul_manager: SoulManager):
473
+ """Unloading when already at base is a safe no-op."""
474
+ state = soul_manager.unload()
475
+ assert state.active_soul is None
476
+ assert len(soul_manager.get_history()) == 0
477
+
478
+ def test_parse_missing_file(self, tmp_path: Path):
479
+ """Parsing a nonexistent file raises FileNotFoundError."""
480
+ with pytest.raises(FileNotFoundError):
481
+ parse_blueprint(tmp_path / "nope.md")
482
+
483
+ def test_parse_empty_file(self, tmp_path: Path):
484
+ """Parsing a file with no sections raises ValueError."""
485
+ empty = tmp_path / "empty.md"
486
+ empty.write_text("Just a paragraph with no headings.")
487
+ with pytest.raises(ValueError, match="No sections"):
488
+ parse_blueprint(empty)
489
+
490
+ def test_corrupt_history_recovered(self, soul_manager: SoulManager):
491
+ """Corrupt history.json returns empty list instead of crashing."""
492
+ (soul_manager.soul_dir / "history.json").write_text("NOT JSON")
493
+ assert soul_manager.get_history() == []
494
+
495
+ def test_corrupt_active_recovered(self, soul_manager: SoulManager):
496
+ """Corrupt active.json returns default state instead of crashing."""
497
+ (soul_manager.soul_dir / "active.json").write_text("{bad")
498
+ state = soul_manager.get_status()
499
+ assert state.base_soul == "base"
500
+
501
+ def test_load_while_loaded_records_swap(
502
+ self,
503
+ soul_manager: SoulManager,
504
+ professional_blueprint: Path,
505
+ comedy_blueprint: Path,
506
+ ):
507
+ """Loading a new soul while one is active swaps correctly."""
508
+ soul_manager.install(professional_blueprint)
509
+ soul_manager.install(comedy_blueprint)
510
+ soul_manager.load("the-test-doctor")
511
+ soul_manager.load("test-word-surgeon")
512
+
513
+ state = soul_manager.get_status()
514
+ assert state.active_soul == "test-word-surgeon"
515
+
516
+ history = soul_manager.get_history()
517
+ assert len(history) == 2
518
+ assert history[1].from_soul == "the-test-doctor"
519
+ assert history[1].to_soul == "test-word-surgeon"
520
+
521
+
522
+ # ---------------------------------------------------------------------------
523
+ # Memory tagging integration
524
+ # ---------------------------------------------------------------------------
525
+
526
+
527
+ class TestMemorySoulContext:
528
+ """Test that memory engine correctly tags with soul_context."""
529
+
530
+ @pytest.fixture(autouse=True)
531
+ def no_unified_backend(self, monkeypatch):
532
+ """Disable the unified skmemory backend so tests use only file-based storage."""
533
+ monkeypatch.setattr("skcapstone.memory_engine._get_unified", lambda: None)
534
+
535
+ def test_store_with_explicit_soul_context(self, tmp_home: Path):
536
+ """Storing memory with explicit soul_context sets it."""
537
+ from skcapstone.memory_engine import store
538
+
539
+ entry = store(tmp_home, "test memory", soul_context="the-doctor")
540
+ assert entry.soul_context == "the-doctor"
541
+
542
+ def test_store_without_soul_context_is_none(self, tmp_home: Path):
543
+ """Without active soul, soul_context is None (base)."""
544
+ from skcapstone.memory_engine import store
545
+
546
+ entry = store(tmp_home, "base memory")
547
+ assert entry.soul_context is None
548
+
549
+ def test_store_autodetects_active_soul(self, tmp_home: Path):
550
+ """store() auto-detects active soul from active.json."""
551
+ from skcapstone.memory_engine import store
552
+
553
+ soul_dir = tmp_home / "soul"
554
+ soul_dir.mkdir(parents=True, exist_ok=True)
555
+ state = {"base_soul": "base", "active_soul": "the-doctor", "activated_at": None}
556
+ (soul_dir / "active.json").write_text(json.dumps(state))
557
+
558
+ entry = store(tmp_home, "auto-tagged memory")
559
+ assert entry.soul_context == "the-doctor"
560
+
561
+ def test_search_filters_by_soul_context(self, tmp_home: Path):
562
+ """search() with soul_context filter only returns matching memories."""
563
+ from skcapstone.memory_engine import search, store
564
+
565
+ store(tmp_home, "doctor memory", soul_context="the-doctor")
566
+ store(tmp_home, "surgeon memory", soul_context="word-surgeon")
567
+ store(tmp_home, "base memory", soul_context=None)
568
+
569
+ results = search(tmp_home, "memory", soul_context="the-doctor")
570
+ assert len(results) == 1
571
+ assert results[0].soul_context == "the-doctor"
572
+
573
+ def test_search_without_filter_returns_all(self, tmp_home: Path):
574
+ """search() without soul_context filter returns all matches."""
575
+ from skcapstone.memory_engine import search, store
576
+
577
+ store(tmp_home, "doctor memory", soul_context="the-doctor")
578
+ store(tmp_home, "base memory", soul_context=None)
579
+
580
+ results = search(tmp_home, "memory")
581
+ assert len(results) == 2
582
+
583
+ def test_soul_context_persists_on_disk(self, tmp_home: Path):
584
+ """soul_context is written to and read back from JSON."""
585
+ from skcapstone.memory_engine import recall, store
586
+
587
+ entry = store(tmp_home, "persistent memory", soul_context="aura")
588
+ recalled = recall(tmp_home, entry.memory_id)
589
+ assert recalled is not None
590
+ assert recalled.soul_context == "aura"
591
+
592
+
593
+ # ---------------------------------------------------------------------------
594
+ # Model tests
595
+ # ---------------------------------------------------------------------------
596
+
597
+
598
+ class TestModels:
599
+ """Basic model instantiation and serialization."""
600
+
601
+ def test_soul_blueprint_defaults(self):
602
+ """SoulBlueprint has sensible defaults."""
603
+ bp = SoulBlueprint(name="test", display_name="Test")
604
+ assert bp.category == "unknown"
605
+ assert bp.core_traits == []
606
+ assert bp.emotional_topology == {}
607
+
608
+ def test_soul_state_defaults(self):
609
+ """SoulState defaults to base with no active overlay."""
610
+ state = SoulState()
611
+ assert state.base_soul == "base"
612
+ assert state.active_soul is None
613
+
614
+ def test_soul_swap_event_timestamp(self):
615
+ """SoulSwapEvent auto-generates a timestamp."""
616
+ event = SoulSwapEvent(from_soul="a", to_soul="b")
617
+ assert event.timestamp is not None
618
+ assert "T" in event.timestamp
619
+
620
+ def test_communication_style_serialization(self):
621
+ """CommunicationStyle round-trips through JSON."""
622
+ cs = CommunicationStyle(
623
+ patterns=["p1"],
624
+ tone_markers=["t1"],
625
+ signature_phrases=["s1"],
626
+ )
627
+ data = cs.model_dump()
628
+ cs2 = CommunicationStyle.model_validate(data)
629
+ assert cs2.patterns == ["p1"]
630
+
631
+
632
+ # ---------------------------------------------------------------------------
633
+ # YAML blueprint loading tests
634
+ # ---------------------------------------------------------------------------
635
+
636
+
637
+ @pytest.fixture
638
+ def yaml_blueprint(tmp_path: Path) -> Path:
639
+ """Create a YAML-format blueprint file."""
640
+ content = """\
641
+ name: the-architect
642
+ display_name: The Architect
643
+ category: professional
644
+ vibe: Systematic, strategic, sees the big picture
645
+ philosophy: Good architecture outlives the architect.
646
+ emoji: "\U0001F3D7"
647
+ core_traits:
648
+ - Systems thinking — sees connections others miss
649
+ - Strategic patience — knows when to build and when to wait
650
+ - Pattern recognition — applies lessons across domains
651
+ communication_style:
652
+ patterns:
653
+ - Draws diagrams before writing code
654
+ - Explains decisions in terms of trade-offs
655
+ tone_markers:
656
+ - Calm and measured
657
+ - Avoids absolutes
658
+ signature_phrases:
659
+ - "What are the trade-offs?"
660
+ - "Let me draw this out."
661
+ decision_framework: "1. Correctness 2. Simplicity 3. Evolvability"
662
+ emotional_topology:
663
+ precision: 0.75
664
+ calm: 0.5
665
+ curiosity: 0.5
666
+ """
667
+ path = tmp_path / "the-architect.yaml"
668
+ path.write_text(content)
669
+ return path
670
+
671
+
672
+ @pytest.fixture
673
+ def yaml_minimal(tmp_path: Path) -> Path:
674
+ """Create a minimal YAML blueprint with only required fields."""
675
+ content = """\
676
+ name: minimal-soul
677
+ display_name: Minimal Soul
678
+ """
679
+ path = tmp_path / "minimal-soul.yaml"
680
+ path.write_text(content)
681
+ return path
682
+
683
+
684
+ class TestYAMLLoading:
685
+ """Tests for YAML blueprint loading."""
686
+
687
+ def test_load_yaml_blueprint(self, yaml_blueprint: Path):
688
+ """Load a full YAML blueprint and verify all fields."""
689
+ bp = load_yaml_blueprint(yaml_blueprint)
690
+ assert bp.name == "the-architect"
691
+ assert bp.display_name == "The Architect"
692
+ assert bp.category == "professional"
693
+ assert "Systematic" in bp.vibe
694
+ assert bp.philosophy == "Good architecture outlives the architect."
695
+ assert len(bp.core_traits) == 3
696
+ assert len(bp.communication_style.patterns) == 2
697
+ assert len(bp.communication_style.tone_markers) == 2
698
+ assert len(bp.communication_style.signature_phrases) == 2
699
+ assert bp.emotional_topology["precision"] == 0.75
700
+
701
+ def test_load_minimal_yaml(self, yaml_minimal: Path):
702
+ """Load a minimal YAML blueprint with defaults."""
703
+ bp = load_yaml_blueprint(yaml_minimal)
704
+ assert bp.name == "minimal-soul"
705
+ assert bp.display_name == "Minimal Soul"
706
+ assert bp.category == "unknown"
707
+ assert bp.core_traits == []
708
+ assert bp.emotional_topology == {}
709
+
710
+ def test_parse_blueprint_detects_yaml(self, yaml_blueprint: Path):
711
+ """parse_blueprint auto-detects .yaml files."""
712
+ bp = parse_blueprint(yaml_blueprint)
713
+ assert bp.name == "the-architect"
714
+
715
+ def test_parse_blueprint_detects_yml(self, tmp_path: Path):
716
+ """parse_blueprint auto-detects .yml files."""
717
+ content = "name: yml-test\ndisplay_name: YML Test\n"
718
+ path = tmp_path / "test.yml"
719
+ path.write_text(content)
720
+ bp = parse_blueprint(path)
721
+ assert bp.name == "yml-test"
722
+
723
+ def test_yaml_missing_file(self, tmp_path: Path):
724
+ """Loading a nonexistent YAML file raises FileNotFoundError."""
725
+ with pytest.raises(FileNotFoundError):
726
+ load_yaml_blueprint(tmp_path / "nope.yaml")
727
+
728
+ def test_yaml_invalid_syntax(self, tmp_path: Path):
729
+ """Loading invalid YAML raises ValueError."""
730
+ path = tmp_path / "bad.yaml"
731
+ path.write_text("name: [\ninvalid yaml")
732
+ with pytest.raises(ValueError, match="Invalid YAML"):
733
+ load_yaml_blueprint(path)
734
+
735
+ def test_yaml_not_a_mapping(self, tmp_path: Path):
736
+ """Loading YAML that's a list raises ValueError."""
737
+ path = tmp_path / "list.yaml"
738
+ path.write_text("- item1\n- item2\n")
739
+ with pytest.raises(ValueError, match="Expected YAML mapping"):
740
+ load_yaml_blueprint(path)
741
+
742
+ def test_yaml_roundtrip_consistency(
743
+ self, professional_blueprint: Path, tmp_path: Path
744
+ ):
745
+ """MD parse → YAML write → YAML load produces same SoulBlueprint."""
746
+ import yaml
747
+
748
+ md_bp = parse_blueprint(professional_blueprint)
749
+ data = md_bp.model_dump()
750
+ yaml_path = tmp_path / "roundtrip.yaml"
751
+ with open(yaml_path, "w") as f:
752
+ yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
753
+
754
+ yaml_bp = load_yaml_blueprint(yaml_path)
755
+ assert yaml_bp.name == md_bp.name
756
+ assert yaml_bp.display_name == md_bp.display_name
757
+ assert yaml_bp.category == md_bp.category
758
+ assert yaml_bp.core_traits == md_bp.core_traits
759
+ assert yaml_bp.emotional_topology == md_bp.emotional_topology
760
+
761
+ def test_install_yaml_blueprint(
762
+ self, soul_manager: SoulManager, yaml_blueprint: Path
763
+ ):
764
+ """SoulManager.install() accepts YAML files."""
765
+ bp = soul_manager.install(yaml_blueprint)
766
+ assert bp.name == "the-architect"
767
+ assert "the-architect" in soul_manager.list_installed()
768
+
769
+ def test_install_all_picks_up_yaml(
770
+ self, soul_manager: SoulManager, yaml_blueprint: Path, professional_blueprint: Path
771
+ ):
772
+ """install_all() picks up both .md and .yaml files."""
773
+ directory = yaml_blueprint.parent
774
+ installed = soul_manager.install_all(directory)
775
+ names = [bp.name for bp in installed]
776
+ assert "the-architect" in names
777
+ assert "the-test-doctor" in names
778
+
779
+
780
+ # ---------------------------------------------------------------------------
781
+ # SoulRegistry tests
782
+ # ---------------------------------------------------------------------------
783
+
784
+
785
+ @pytest.fixture
786
+ def populated_registry(tmp_path: Path) -> SoulRegistry:
787
+ """Create a SoulRegistry with several soul blueprints."""
788
+ reg_dir = tmp_path / "registry"
789
+ reg_dir.mkdir()
790
+
791
+ souls = [
792
+ SoulBlueprint(
793
+ name="coder",
794
+ display_name="The Coder",
795
+ category="professional",
796
+ vibe="Logical and focused",
797
+ core_traits=["logic", "precision", "debugging"],
798
+ emotional_topology={"precision": 0.75, "curiosity": 0.5},
799
+ ),
800
+ SoulBlueprint(
801
+ name="artist",
802
+ display_name="The Artist",
803
+ category="professional",
804
+ vibe="Creative and expressive",
805
+ core_traits=["creativity", "empathy", "vision"],
806
+ emotional_topology={"warmth": 0.5, "intensity": 0.75},
807
+ ),
808
+ SoulBlueprint(
809
+ name="joker",
810
+ display_name="The Joker",
811
+ category="comedy",
812
+ vibe="Hilarious and irreverent",
813
+ core_traits=["humor", "timing", "rebellion"],
814
+ emotional_topology={"humor": 0.9, "rebellion": 0.5},
815
+ ),
816
+ SoulBlueprint(
817
+ name="sage",
818
+ display_name="The Sage",
819
+ category="authentic-connection",
820
+ vibe="Wise and calming",
821
+ core_traits=["wisdom", "empathy", "patience"],
822
+ emotional_topology={"warmth": 0.8, "calm": 0.9},
823
+ ),
824
+ ]
825
+
826
+ for bp in souls:
827
+ path = reg_dir / f"{bp.name}.json"
828
+ path.write_text(bp.model_dump_json(indent=2), encoding="utf-8")
829
+
830
+ return SoulRegistry(reg_dir)
831
+
832
+
833
+ class TestSoulRegistry:
834
+ """Tests for the SoulRegistry discovery and search API."""
835
+
836
+ def test_list_all(self, populated_registry: SoulRegistry):
837
+ """list_all returns all registered souls sorted by name."""
838
+ all_souls = populated_registry.list_all()
839
+ assert len(all_souls) == 4
840
+ assert all_souls[0].name == "artist"
841
+ assert all_souls[-1].name == "sage"
842
+
843
+ def test_list_names(self, populated_registry: SoulRegistry):
844
+ """list_names returns sorted slug names."""
845
+ names = populated_registry.list_names()
846
+ assert names == ["artist", "coder", "joker", "sage"]
847
+
848
+ def test_get_existing(self, populated_registry: SoulRegistry):
849
+ """get() returns the correct blueprint by name."""
850
+ bp = populated_registry.get("coder")
851
+ assert bp is not None
852
+ assert bp.display_name == "The Coder"
853
+
854
+ def test_get_missing(self, populated_registry: SoulRegistry):
855
+ """get() returns None for unknown names."""
856
+ assert populated_registry.get("nonexistent") is None
857
+
858
+ def test_search_by_category(self, populated_registry: SoulRegistry):
859
+ """search(category=...) filters correctly."""
860
+ results = populated_registry.search(category="professional")
861
+ assert len(results) == 2
862
+ names = [bp.name for bp in results]
863
+ assert "coder" in names
864
+ assert "artist" in names
865
+
866
+ def test_search_by_category_case_insensitive(self, populated_registry: SoulRegistry):
867
+ """search(category=...) is case-insensitive."""
868
+ results = populated_registry.search(category="COMEDY")
869
+ assert len(results) == 1
870
+ assert results[0].name == "joker"
871
+
872
+ def test_search_by_trait_keyword(self, populated_registry: SoulRegistry):
873
+ """search(trait_keyword=...) matches against core_traits."""
874
+ results = populated_registry.search(trait_keyword="empathy")
875
+ assert len(results) == 2
876
+ names = [bp.name for bp in results]
877
+ assert "artist" in names
878
+ assert "sage" in names
879
+
880
+ def test_search_by_topology(self, populated_registry: SoulRegistry):
881
+ """search(min_topology=...) filters by minimum thresholds."""
882
+ results = populated_registry.search(min_topology={"warmth": 0.6})
883
+ assert len(results) == 1
884
+ assert results[0].name == "sage"
885
+
886
+ def test_search_combined_filters(self, populated_registry: SoulRegistry):
887
+ """search() combines category + trait_keyword filters."""
888
+ results = populated_registry.search(
889
+ category="professional", trait_keyword="precision"
890
+ )
891
+ assert len(results) == 1
892
+ assert results[0].name == "coder"
893
+
894
+ def test_search_no_results(self, populated_registry: SoulRegistry):
895
+ """search() returns empty list when nothing matches."""
896
+ results = populated_registry.search(trait_keyword="zzz_nonexistent")
897
+ assert results == []
898
+
899
+ def test_by_category(self, populated_registry: SoulRegistry):
900
+ """by_category() groups souls correctly."""
901
+ groups = populated_registry.by_category()
902
+ assert len(groups) == 3
903
+ assert len(groups["professional"]) == 2
904
+ assert len(groups["comedy"]) == 1
905
+ assert len(groups["authentic-connection"]) == 1
906
+
907
+ def test_count(self, populated_registry: SoulRegistry):
908
+ """count() returns the total number of souls."""
909
+ assert populated_registry.count() == 4
910
+
911
+ def test_categories(self, populated_registry: SoulRegistry):
912
+ """categories() returns sorted unique category names."""
913
+ cats = populated_registry.categories()
914
+ assert cats == ["authentic-connection", "comedy", "professional"]
915
+
916
+ def test_summary(self, populated_registry: SoulRegistry):
917
+ """summary() returns a complete overview dict."""
918
+ s = populated_registry.summary()
919
+ assert s["total"] == 4
920
+ assert s["categories"]["professional"] == 2
921
+ assert s["categories"]["comedy"] == 1
922
+ assert len(s["souls"]) == 4
923
+
924
+ def test_empty_registry(self, tmp_path: Path):
925
+ """Empty registry returns empty results without errors."""
926
+ empty_dir = tmp_path / "empty"
927
+ empty_dir.mkdir()
928
+ reg = SoulRegistry(empty_dir)
929
+ assert reg.list_all() == []
930
+ assert reg.count() == 0
931
+ assert reg.categories() == []
932
+
933
+ def test_missing_directory(self, tmp_path: Path):
934
+ """Registry handles nonexistent source directory gracefully."""
935
+ reg = SoulRegistry(tmp_path / "nope")
936
+ assert reg.list_all() == []
937
+ assert reg.count() == 0
938
+
939
+ def test_reload(self, populated_registry: SoulRegistry, tmp_path: Path):
940
+ """reload() picks up newly added souls."""
941
+ assert populated_registry.count() == 4
942
+
943
+ # Add a new soul to the registry directory
944
+ new_bp = SoulBlueprint(
945
+ name="new-soul",
946
+ display_name="New Soul",
947
+ category="test",
948
+ )
949
+ path = populated_registry.source / "new-soul.json"
950
+ path.write_text(new_bp.model_dump_json(indent=2), encoding="utf-8")
951
+
952
+ populated_registry.reload()
953
+ assert populated_registry.count() == 5
954
+ assert populated_registry.get("new-soul") is not None
955
+
956
+ def test_registry_loads_yaml(self, tmp_path: Path):
957
+ """Registry can load YAML files directly."""
958
+ import yaml
959
+
960
+ reg_dir = tmp_path / "yaml-reg"
961
+ reg_dir.mkdir()
962
+ data = {
963
+ "name": "yaml-soul",
964
+ "display_name": "YAML Soul",
965
+ "category": "test",
966
+ "core_traits": ["flexible"],
967
+ }
968
+ with open(reg_dir / "yaml-soul.yaml", "w") as f:
969
+ yaml.dump(data, f)
970
+
971
+ reg = SoulRegistry(reg_dir)
972
+ assert reg.count() == 1
973
+ bp = reg.get("yaml-soul")
974
+ assert bp is not None
975
+ assert bp.core_traits == ["flexible"]
976
+
977
+ def test_manager_get_registry(
978
+ self, soul_manager: SoulManager, professional_blueprint: Path
979
+ ):
980
+ """SoulManager.get_registry() returns a working registry."""
981
+ soul_manager.install(professional_blueprint)
982
+ reg = soul_manager.get_registry()
983
+ assert reg.count() == 1
984
+ assert reg.get("the-test-doctor") is not None