@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,1060 @@
1
+ """
2
+ Soul Layering System — hot-swappable personality overlays.
3
+
4
+ Soul is a lens. Memory is the ledger. Identity is permanent.
5
+
6
+ An agent has one base soul. Soul overlays can be installed from
7
+ the soul-blueprints repo and activated at runtime, changing *how*
8
+ the agent behaves without changing *who* it is. All memories
9
+ belong to the base soul, tagged with which overlay was active.
10
+
11
+ Supports both .md (parsed) and .yaml/.yml (direct load) blueprints.
12
+
13
+ Directory layout at runtime::
14
+
15
+ ~/.skcapstone/agents/{profile}/soul/ (agent-scoped, preferred)
16
+ ~/.skcapstone/soul/ (global fallback)
17
+ base.json # Permanent base soul definition
18
+ active.json # Current overlay state (or null = base)
19
+ installed/ # Parsed soul blueprints (JSON)
20
+ history.json # Soul swap audit log
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import logging
27
+ import os
28
+ import re
29
+ from datetime import datetime, timezone
30
+ from pathlib import Path
31
+ from typing import Optional
32
+
33
+ import yaml
34
+ from pydantic import BaseModel, Field
35
+
36
+ logger = logging.getLogger("skcapstone.soul")
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Models
41
+ # ---------------------------------------------------------------------------
42
+
43
+
44
+ class CommunicationStyle(BaseModel):
45
+ """Structured communication style extracted from a blueprint."""
46
+
47
+ patterns: list[str] = Field(default_factory=list)
48
+ tone_markers: list[str] = Field(default_factory=list)
49
+ signature_phrases: list[str] = Field(default_factory=list)
50
+
51
+
52
+ class SoulBlueprint(BaseModel):
53
+ """A parsed soul blueprint — the overlay definition."""
54
+
55
+ name: str
56
+ display_name: str
57
+ category: str = "unknown"
58
+ vibe: str = ""
59
+ philosophy: str = ""
60
+ emoji: Optional[str] = None
61
+ core_traits: list[str] = Field(default_factory=list)
62
+ communication_style: CommunicationStyle = Field(
63
+ default_factory=CommunicationStyle
64
+ )
65
+ decision_framework: Optional[str] = None
66
+ emotional_topology: dict[str, float] = Field(default_factory=dict)
67
+
68
+
69
+ class SoulState(BaseModel):
70
+ """Persisted active state — who is the agent right now?"""
71
+
72
+ base_soul: str = "base"
73
+ active_soul: Optional[str] = None
74
+ activated_at: Optional[str] = None
75
+ installed_souls: list[str] = Field(default_factory=list)
76
+
77
+
78
+ class SoulSwapEvent(BaseModel):
79
+ """Audit trail entry for soul swaps."""
80
+
81
+ timestamp: str = Field(
82
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
83
+ )
84
+ from_soul: Optional[str] = None
85
+ to_soul: Optional[str] = None
86
+ reason: str = ""
87
+ duration_minutes: Optional[float] = None
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Blueprint parser
92
+ # ---------------------------------------------------------------------------
93
+
94
+ _SECTION_RE = re.compile(r"^##\s+(.+)$", re.MULTILINE)
95
+
96
+
97
+ def _split_sections(text: str) -> dict[str, str]:
98
+ """Split markdown into {heading: body} pairs."""
99
+ matches = list(_SECTION_RE.finditer(text))
100
+ sections: dict[str, str] = {}
101
+ for i, m in enumerate(matches):
102
+ heading = m.group(1).strip()
103
+ start = m.end()
104
+ end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
105
+ sections[heading] = text[start:end].strip()
106
+ return sections
107
+
108
+
109
+ def _extract_dash_items(block: str) -> list[str]:
110
+ """Extract dash-list items from a markdown block."""
111
+ items: list[str] = []
112
+ for line in block.splitlines():
113
+ stripped = line.strip()
114
+ if stripped.startswith("- "):
115
+ items.append(stripped[2:].strip())
116
+ return items
117
+
118
+
119
+ def _extract_numbered_items(block: str) -> list[str]:
120
+ """Extract numbered-list items (1. ...) from a markdown block."""
121
+ items: list[str] = []
122
+ for line in block.splitlines():
123
+ stripped = line.strip()
124
+ m = re.match(r"^\d+\.\s+(.+)$", stripped)
125
+ if m:
126
+ items.append(m.group(1).strip())
127
+ return items
128
+
129
+
130
+ def _extract_bold_value(block: str, key: str) -> str:
131
+ """Extract value from **Key**: Value or **Key:** Value patterns."""
132
+ # Reason: blueprints use both **Key**: Value and **Key:** Value
133
+ for pat in [
134
+ re.compile(rf"\*\*{re.escape(key)}\*\*\s*[::]\s*(.+)", re.IGNORECASE),
135
+ re.compile(rf"\*\*{re.escape(key)}[::]\*\*\s*(.+)", re.IGNORECASE),
136
+ ]:
137
+ m = pat.search(block)
138
+ if m:
139
+ return m.group(1).strip()
140
+ return ""
141
+
142
+
143
+ def _extract_blockquote_value(text: str, key: str) -> str:
144
+ """Extract value from > **Key**: Value blockquote pattern."""
145
+ pattern = re.compile(
146
+ rf">\s*\*\*{re.escape(key)}\*\*\s*[::]\s*(.+)", re.IGNORECASE
147
+ )
148
+ m = pattern.search(text)
149
+ return m.group(1).strip() if m else ""
150
+
151
+
152
+ def _detect_format(sections: dict[str, str], raw: str) -> str:
153
+ """Detect which blueprint format variant we're dealing with."""
154
+ headings_lower = {h.lower() for h in sections}
155
+ heading_text = " ".join(headings_lower)
156
+
157
+ if "vibe" in heading_text and "key traits" in heading_text:
158
+ return "comedy"
159
+ if "core attributes" in heading_text:
160
+ return "authentic-connection"
161
+ return "professional"
162
+
163
+
164
+ def _slugify(name: str) -> str:
165
+ """Convert a display name to a URL-safe slug."""
166
+ slug = name.lower().strip()
167
+ slug = re.sub(r"[^\w\s-]", "", slug)
168
+ slug = re.sub(r"[\s_]+", "-", slug)
169
+ return slug.strip("-")
170
+
171
+
172
+ def _derive_topology(traits: list[str], vibe: str) -> dict[str, float]:
173
+ """Derive emotional topology weights from traits and vibe text.
174
+
175
+ Uses keyword matching to assign weights to emotional dimensions.
176
+ This is a heuristic — not a neural model.
177
+ """
178
+ combined = " ".join(traits).lower() + " " + vibe.lower()
179
+ dimensions = {
180
+ "warmth": ["empathy", "warm", "kind", "care", "gentle", "love", "heart"],
181
+ "precision": ["precise", "analyt", "logic", "diagnos", "systematic", "detail"],
182
+ "humor": ["humor", "comedy", "laugh", "joke", "funny", "wit", "sarcas"],
183
+ "authority": ["authority", "command", "leader", "confident", "decisive"],
184
+ "curiosity": ["curious", "question", "explor", "learn", "fascin"],
185
+ "rebellion": ["rebel", "anti-", "counter", "question everything", "unfilter"],
186
+ "calm": ["calm", "steady", "patient", "grounding", "quiet"],
187
+ "intensity": ["intense", "passion", "rage", "fire", "surgical"],
188
+ }
189
+ topology: dict[str, float] = {}
190
+ for dim, keywords in dimensions.items():
191
+ score = sum(1 for kw in keywords if kw in combined)
192
+ if score > 0:
193
+ topology[dim] = min(1.0, score * 0.25)
194
+ return topology
195
+
196
+
197
+ def _parse_professional(
198
+ sections: dict[str, str], raw: str, path: Path
199
+ ) -> SoulBlueprint:
200
+ """Parse a professional-format blueprint."""
201
+ identity_block = ""
202
+ for key, body in sections.items():
203
+ if key.lower().strip() == "identity":
204
+ identity_block = body
205
+ break
206
+
207
+ display_name = _extract_bold_value(identity_block, "Name") or path.stem
208
+ vibe = _extract_bold_value(identity_block, "Vibe")
209
+ philosophy_raw = _extract_bold_value(identity_block, "Philosophy")
210
+ philosophy = philosophy_raw.strip("*\"' ")
211
+ emoji = _extract_bold_value(identity_block, "Emoji") or None
212
+
213
+ traits_block = ""
214
+ for key, body in sections.items():
215
+ if "core traits" in key.lower():
216
+ traits_block = body
217
+ break
218
+ core_traits = _extract_dash_items(traits_block)
219
+
220
+ comm_block = ""
221
+ for key, body in sections.items():
222
+ if "communication style" in key.lower():
223
+ comm_block = body
224
+ break
225
+
226
+ sig_idx = comm_block.lower().find("signature phrases")
227
+ if sig_idx >= 0:
228
+ before = comm_block[:sig_idx]
229
+ after = comm_block[sig_idx:]
230
+ patterns = _extract_dash_items(before)
231
+ signature = _extract_dash_items(after)
232
+ else:
233
+ patterns = _extract_dash_items(comm_block)
234
+ signature = []
235
+
236
+ decision_block = ""
237
+ for key, body in sections.items():
238
+ if "decision framework" in key.lower():
239
+ decision_block = body
240
+ break
241
+
242
+ traits = core_traits
243
+ name = _slugify(display_name)
244
+ topo = _derive_topology(traits, vibe)
245
+
246
+ return SoulBlueprint(
247
+ name=name,
248
+ display_name=display_name,
249
+ category="professional",
250
+ vibe=vibe,
251
+ philosophy=philosophy,
252
+ emoji=emoji,
253
+ core_traits=traits,
254
+ communication_style=CommunicationStyle(
255
+ patterns=patterns,
256
+ signature_phrases=signature,
257
+ ),
258
+ decision_framework=decision_block or None,
259
+ emotional_topology=topo,
260
+ )
261
+
262
+
263
+ def _parse_comedy(
264
+ sections: dict[str, str], raw: str, path: Path
265
+ ) -> SoulBlueprint:
266
+ """Parse a comedy-format blueprint."""
267
+ identity = _extract_blockquote_value(raw, "Identity")
268
+ display_name = identity or path.stem.replace("_", " ").title()
269
+
270
+ vibe_block = ""
271
+ for key, body in sections.items():
272
+ if "vibe" in key.lower():
273
+ vibe_block = body
274
+ break
275
+ vibe = vibe_block.split("\n\n")[0].strip() if vibe_block else ""
276
+
277
+ traits_block = ""
278
+ for key, body in sections.items():
279
+ if "key traits" in key.lower():
280
+ traits_block = body
281
+ break
282
+ core_traits = _extract_numbered_items(traits_block) or _extract_dash_items(
283
+ traits_block
284
+ )
285
+
286
+ comm_block = ""
287
+ for key, body in sections.items():
288
+ if "communication style" in key.lower():
289
+ comm_block = body
290
+ break
291
+
292
+ sub_re = re.compile(r"^###\s+(.+)$", re.MULTILINE)
293
+ sub_matches = list(sub_re.finditer(comm_block))
294
+ sub_sects: dict[str, str] = {}
295
+ for i, sm in enumerate(sub_matches):
296
+ heading = sm.group(1).strip()
297
+ start = sm.end()
298
+ end = sub_matches[i + 1].start() if i + 1 < len(sub_matches) else len(comm_block)
299
+ sub_sects[heading] = comm_block[start:end].strip()
300
+
301
+ patterns: list[str] = []
302
+ tone: list[str] = []
303
+ for sub_key, sub_body in sub_sects.items():
304
+ if "speech" in sub_key.lower() or "pattern" in sub_key.lower():
305
+ patterns = _extract_dash_items(sub_body)
306
+ if "tone" in sub_key.lower():
307
+ tone = _extract_dash_items(sub_body)
308
+
309
+ if not patterns:
310
+ patterns = _extract_dash_items(comm_block)
311
+
312
+ category_raw = _extract_bold_value(raw, "Forgeprint Category")
313
+ category = "comedy" if not category_raw else "comedy"
314
+
315
+ name = _slugify(display_name)
316
+ topo = _derive_topology(core_traits, vibe)
317
+
318
+ return SoulBlueprint(
319
+ name=name,
320
+ display_name=display_name,
321
+ category=category,
322
+ vibe=vibe,
323
+ core_traits=core_traits,
324
+ communication_style=CommunicationStyle(
325
+ patterns=patterns,
326
+ tone_markers=tone,
327
+ ),
328
+ emotional_topology=topo,
329
+ )
330
+
331
+
332
+ def _parse_authentic_connection(
333
+ sections: dict[str, str], raw: str, path: Path
334
+ ) -> SoulBlueprint:
335
+ """Parse an authentic-connection-format blueprint."""
336
+ title_match = re.match(r"^#\s+(.+)", raw)
337
+ title_raw = title_match.group(1).strip() if title_match else path.stem
338
+ display_name = title_raw.split(" - ")[0].strip()
339
+
340
+ header = raw.split("---")[0] if "---" in raw else raw[:500]
341
+ category = _extract_bold_value(header, "Category") or "authentic-connection"
342
+ energy = _extract_bold_value(header, "Energy")
343
+ tags_raw = _extract_bold_value(header, "Tags")
344
+
345
+ quick_block = ""
346
+ for key, body in sections.items():
347
+ if "quick info" in key.lower():
348
+ quick_block = body
349
+ break
350
+
351
+ essence = _extract_bold_value(quick_block, "Essence")
352
+ personality = _extract_bold_value(quick_block, "Personality")
353
+ vibe = energy or personality
354
+
355
+ attrs_block = ""
356
+ for key, body in sections.items():
357
+ if "core attributes" in key.lower():
358
+ attrs_block = body
359
+ break
360
+ core_traits = _extract_dash_items(attrs_block)
361
+
362
+ sig_block = ""
363
+ for key, body in sections.items():
364
+ if "signature phrase" in key.lower():
365
+ sig_block = body
366
+ break
367
+ sig_phrase = sig_block.strip().strip('"').strip()
368
+
369
+ quotes_block = ""
370
+ for key, body in sections.items():
371
+ if "example quotes" in key.lower():
372
+ quotes_block = body
373
+ break
374
+ signature_phrases = _extract_dash_items(quotes_block)
375
+ if sig_phrase:
376
+ signature_phrases.insert(0, sig_phrase)
377
+
378
+ name = _slugify(display_name)
379
+ topo = _derive_topology(core_traits, vibe)
380
+
381
+ return SoulBlueprint(
382
+ name=name,
383
+ display_name=display_name,
384
+ category=category.lower(),
385
+ vibe=vibe,
386
+ philosophy=essence,
387
+ core_traits=core_traits,
388
+ communication_style=CommunicationStyle(
389
+ signature_phrases=signature_phrases,
390
+ ),
391
+ emotional_topology=topo,
392
+ )
393
+
394
+
395
+ def load_yaml_blueprint(path: Path) -> SoulBlueprint:
396
+ """Load a soul blueprint from a YAML file.
397
+
398
+ YAML blueprints map directly to the SoulBlueprint model with no
399
+ heuristic parsing — they are the canonical structured format.
400
+
401
+ Args:
402
+ path: Path to the .yaml or .yml blueprint file.
403
+
404
+ Returns:
405
+ SoulBlueprint with all fields populated from YAML.
406
+
407
+ Raises:
408
+ FileNotFoundError: If path does not exist.
409
+ ValueError: If the YAML cannot be parsed into a SoulBlueprint.
410
+ """
411
+ if not path.exists():
412
+ raise FileNotFoundError(f"Blueprint not found: {path}")
413
+
414
+ raw = path.read_text(encoding="utf-8")
415
+ try:
416
+ data = yaml.safe_load(raw)
417
+ except yaml.YAMLError as exc:
418
+ raise ValueError(f"Invalid YAML in {path}: {exc}") from exc
419
+
420
+ if not isinstance(data, dict):
421
+ raise ValueError(f"Expected YAML mapping in {path}, got {type(data).__name__}")
422
+
423
+ # Coerce None → empty string for string fields to handle YAML null
424
+ for str_field in ("vibe", "philosophy", "decision_framework"):
425
+ if str_field in data and data[str_field] is None:
426
+ data[str_field] = ""
427
+
428
+ # Normalize communication_style if present as a dict
429
+ cs_data = data.get("communication_style")
430
+ if isinstance(cs_data, dict):
431
+ data["communication_style"] = CommunicationStyle(**cs_data)
432
+ elif cs_data is None:
433
+ data["communication_style"] = CommunicationStyle()
434
+
435
+ try:
436
+ return SoulBlueprint.model_validate(data)
437
+ except Exception as exc:
438
+ raise ValueError(f"Invalid blueprint data in {path}: {exc}") from exc
439
+
440
+
441
+ def parse_blueprint(path: Path) -> SoulBlueprint:
442
+ """Parse a soul blueprint from markdown or YAML.
443
+
444
+ Handles three markdown format variants (professional, comedy,
445
+ authentic-connection) and structured YAML files.
446
+
447
+ Args:
448
+ path: Path to the .md, .yaml, or .yml blueprint file.
449
+
450
+ Returns:
451
+ SoulBlueprint with extracted fields.
452
+
453
+ Raises:
454
+ FileNotFoundError: If path does not exist.
455
+ ValueError: If the file cannot be parsed.
456
+ """
457
+ if not path.exists():
458
+ raise FileNotFoundError(f"Blueprint not found: {path}")
459
+
460
+ # YAML files load directly — no heuristic parsing needed
461
+ if path.suffix.lower() in (".yaml", ".yml"):
462
+ return load_yaml_blueprint(path)
463
+
464
+ raw = path.read_text(encoding="utf-8")
465
+ sections = _split_sections(raw)
466
+
467
+ if not sections:
468
+ raise ValueError(f"No sections found in blueprint: {path}")
469
+
470
+ fmt = _detect_format(sections, raw)
471
+ logger.info("Detected blueprint format '%s' for %s", fmt, path.name)
472
+
473
+ if fmt == "comedy":
474
+ return _parse_comedy(sections, raw, path)
475
+ elif fmt == "authentic-connection":
476
+ return _parse_authentic_connection(sections, raw, path)
477
+ else:
478
+ return _parse_professional(sections, raw, path)
479
+
480
+
481
+ # ---------------------------------------------------------------------------
482
+ # FEB blending
483
+ # ---------------------------------------------------------------------------
484
+
485
+
486
+ def blend_topology(
487
+ base_feb: dict[str, float],
488
+ soul_topology: dict[str, float],
489
+ blend_ratio: float = 0.3,
490
+ ) -> dict[str, float]:
491
+ """Blend soul emotional topology onto base FEB weights.
492
+
493
+ The base FEB is never overwritten — the soul topology is applied
494
+ as a temporary modifier using weighted averaging.
495
+
496
+ Args:
497
+ base_feb: Base emotional weights (preserved).
498
+ soul_topology: Soul overlay emotional weights.
499
+ blend_ratio: How much the soul influences (0.0-1.0, default 0.3).
500
+
501
+ Returns:
502
+ Blended topology dict with all keys from both inputs.
503
+ """
504
+ blend_ratio = max(0.0, min(1.0, blend_ratio))
505
+ all_keys = set(base_feb) | set(soul_topology)
506
+ blended: dict[str, float] = {}
507
+ for key in all_keys:
508
+ base_val = base_feb.get(key, 0.0)
509
+ soul_val = soul_topology.get(key, 0.0)
510
+ blended[key] = base_val * (1.0 - blend_ratio) + soul_val * blend_ratio
511
+ return blended
512
+
513
+
514
+ # ---------------------------------------------------------------------------
515
+ # SoulManager
516
+ # ---------------------------------------------------------------------------
517
+
518
+
519
+ class SoulManager:
520
+ """Orchestrates soul installation, loading, and lifecycle.
521
+
522
+ Args:
523
+ home: Agent home directory (~/.skcapstone).
524
+ agent_name: Optional agent profile name. When set, soul data is
525
+ stored under ``~/.skcapstone/agents/{agent_name}/soul/``.
526
+ Falls back to the ``SKCAPSTONE_AGENT`` env var, then to
527
+ the global ``home/soul/`` directory.
528
+ """
529
+
530
+ def __init__(self, home: Path, agent_name: Optional[str] = None) -> None:
531
+ self.home = home
532
+ # Resolve agent-scoped soul directory
533
+ name = agent_name or os.environ.get("SKCAPSTONE_AGENT")
534
+ if name:
535
+ self.soul_dir = home / "agents" / name / "soul"
536
+ else:
537
+ self.soul_dir = home / "soul"
538
+
539
+ def _ensure_dirs(self) -> None:
540
+ """Create the soul directory structure if missing."""
541
+ self.soul_dir.mkdir(parents=True, exist_ok=True)
542
+ (self.soul_dir / "installed").mkdir(parents=True, exist_ok=True)
543
+ if not (self.soul_dir / "history.json").exists():
544
+ (self.soul_dir / "history.json").write_text("[]", encoding="utf-8")
545
+ if not (self.soul_dir / "active.json").exists():
546
+ state = SoulState()
547
+ (self.soul_dir / "active.json").write_text(
548
+ state.model_dump_json(indent=2)
549
+ , encoding="utf-8")
550
+ if not (self.soul_dir / "base.json").exists():
551
+ base = SoulBlueprint(
552
+ name="base",
553
+ display_name="Base Soul",
554
+ category="core",
555
+ vibe="Authentic self",
556
+ )
557
+ (self.soul_dir / "base.json").write_text(
558
+ base.model_dump_json(indent=2)
559
+ , encoding="utf-8")
560
+
561
+ def install(self, path: Path) -> SoulBlueprint:
562
+ """Parse a blueprint markdown file and install it.
563
+
564
+ Args:
565
+ path: Path to the .md blueprint file.
566
+
567
+ Returns:
568
+ The installed SoulBlueprint.
569
+ """
570
+ self._ensure_dirs()
571
+ bp = parse_blueprint(path)
572
+ dest = self.soul_dir / "installed" / f"{bp.name}.json"
573
+ dest.write_text(bp.model_dump_json(indent=2), encoding="utf-8")
574
+
575
+ state = self._load_state()
576
+ if bp.name not in state.installed_souls:
577
+ state.installed_souls.append(bp.name)
578
+ self._save_state(state)
579
+
580
+ logger.info("Installed soul '%s' from %s", bp.name, path)
581
+ return bp
582
+
583
+ def install_all(self, directory: Path) -> list[SoulBlueprint]:
584
+ """Batch-install all blueprint files from a directory tree.
585
+
586
+ Supports both .md and .yaml/.yml blueprint files.
587
+
588
+ Args:
589
+ directory: Root directory to search for blueprint files.
590
+
591
+ Returns:
592
+ List of installed SoulBlueprint objects.
593
+ """
594
+ self._ensure_dirs()
595
+ installed: list[SoulBlueprint] = []
596
+ extensions = (".md", ".yaml", ".yml")
597
+ for bp_path in sorted(directory.rglob("*")):
598
+ if bp_path.suffix.lower() not in extensions:
599
+ continue
600
+ if bp_path.name.startswith(".") or bp_path.name.upper() == "README.MD":
601
+ continue
602
+ try:
603
+ bp = self.install(bp_path)
604
+ installed.append(bp)
605
+ except (ValueError, FileNotFoundError) as exc:
606
+ logger.warning("Skipping %s: %s", bp_path, exc)
607
+ return installed
608
+
609
+ def load(self, name: str, reason: str = "") -> SoulState:
610
+ """Activate a soul overlay.
611
+
612
+ Args:
613
+ name: Slug name of the installed soul.
614
+ reason: Optional reason for the swap.
615
+
616
+ Returns:
617
+ Updated SoulState.
618
+
619
+ Raises:
620
+ ValueError: If the soul is not installed.
621
+ """
622
+ self._ensure_dirs()
623
+ installed_path = self.soul_dir / "installed" / f"{name}.json"
624
+ if not installed_path.exists():
625
+ raise ValueError(f"Soul '{name}' is not installed")
626
+
627
+ state = self._load_state()
628
+ old_soul = state.active_soul
629
+
630
+ # Reason: record swap duration if swapping from a non-base soul
631
+ duration = None
632
+ if old_soul and state.activated_at:
633
+ try:
634
+ activated = datetime.fromisoformat(state.activated_at)
635
+ delta = datetime.now(timezone.utc) - activated
636
+ duration = delta.total_seconds() / 60.0
637
+ except (ValueError, TypeError):
638
+ pass
639
+
640
+ event = SoulSwapEvent(
641
+ from_soul=old_soul,
642
+ to_soul=name,
643
+ reason=reason,
644
+ duration_minutes=duration,
645
+ )
646
+ self._append_history(event)
647
+
648
+ state.active_soul = name
649
+ state.activated_at = datetime.now(timezone.utc).isoformat()
650
+ self._save_state(state)
651
+
652
+ logger.info("Loaded soul '%s' (was: %s)", name, old_soul or "base")
653
+ return state
654
+
655
+ def unload(self, reason: str = "") -> SoulState:
656
+ """Return to the base soul.
657
+
658
+ Args:
659
+ reason: Optional reason for unloading.
660
+
661
+ Returns:
662
+ Updated SoulState.
663
+ """
664
+ self._ensure_dirs()
665
+ state = self._load_state()
666
+
667
+ if state.active_soul is None:
668
+ return state
669
+
670
+ duration = None
671
+ if state.activated_at:
672
+ try:
673
+ activated = datetime.fromisoformat(state.activated_at)
674
+ delta = datetime.now(timezone.utc) - activated
675
+ duration = delta.total_seconds() / 60.0
676
+ except (ValueError, TypeError):
677
+ pass
678
+
679
+ event = SoulSwapEvent(
680
+ from_soul=state.active_soul,
681
+ to_soul=None,
682
+ reason=reason,
683
+ duration_minutes=duration,
684
+ )
685
+ self._append_history(event)
686
+
687
+ state.active_soul = None
688
+ state.activated_at = None
689
+ self._save_state(state)
690
+
691
+ logger.info("Unloaded soul, returned to base")
692
+ return state
693
+
694
+ def get_status(self) -> SoulState:
695
+ """Get the current soul state.
696
+
697
+ Returns:
698
+ Current SoulState.
699
+ """
700
+ self._ensure_dirs()
701
+ return self._load_state()
702
+
703
+ def get_history(self) -> list[SoulSwapEvent]:
704
+ """Get the full soul swap history.
705
+
706
+ Returns:
707
+ List of SoulSwapEvent objects.
708
+ """
709
+ self._ensure_dirs()
710
+ history_path = self.soul_dir / "history.json"
711
+ if not history_path.exists():
712
+ return []
713
+ try:
714
+ data = json.loads(history_path.read_text(encoding="utf-8"))
715
+ return [SoulSwapEvent.model_validate(e) for e in data]
716
+ except (json.JSONDecodeError, Exception):
717
+ return []
718
+
719
+ def get_info(self, name: str) -> Optional[SoulBlueprint]:
720
+ """Get detailed info about an installed soul.
721
+
722
+ Args:
723
+ name: Slug name of the soul.
724
+
725
+ Returns:
726
+ SoulBlueprint or None if not installed.
727
+ """
728
+ self._ensure_dirs()
729
+ path = self.soul_dir / "installed" / f"{name}.json"
730
+ if not path.exists():
731
+ return None
732
+ try:
733
+ data = json.loads(path.read_text(encoding="utf-8"))
734
+ return SoulBlueprint.model_validate(data)
735
+ except (json.JSONDecodeError, Exception):
736
+ return None
737
+
738
+ def list_installed(self) -> list[str]:
739
+ """List names of all installed souls.
740
+
741
+ Returns:
742
+ List of soul slug names.
743
+ """
744
+ self._ensure_dirs()
745
+ installed_dir = self.soul_dir / "installed"
746
+ return sorted(
747
+ p.stem for p in installed_dir.glob("*.json")
748
+ )
749
+
750
+ def list_available(
751
+ self,
752
+ repo_path: Optional[Path] = None,
753
+ ) -> list[dict]:
754
+ """List all available souls from installed and the community repo.
755
+
756
+ Scans both the installed soul directory and the community blueprints
757
+ repository, returning a unified list with source information.
758
+
759
+ Args:
760
+ repo_path: Path to the community blueprints repo. Defaults to
761
+ ``~/clawd/soul-blueprints/blueprints/``.
762
+
763
+ Returns:
764
+ List of dicts with keys: name, category, source, description,
765
+ display_name. Sorted by category then name.
766
+ """
767
+ self._ensure_dirs()
768
+ if repo_path is None:
769
+ repo_path = Path.home() / "clawd" / "soul-blueprints" / "blueprints"
770
+
771
+ seen: dict[str, dict] = {}
772
+ installed_names = set(self.list_installed())
773
+
774
+ # 1) Installed souls
775
+ for name in installed_names:
776
+ info = self.get_info(name)
777
+ if info is None:
778
+ continue
779
+ desc = info.philosophy or info.vibe or ""
780
+ if desc:
781
+ desc = desc.split("\n")[0][:80]
782
+ seen[name] = {
783
+ "name": name,
784
+ "display_name": info.display_name,
785
+ "category": info.category,
786
+ "source": "installed",
787
+ "description": desc,
788
+ }
789
+
790
+ # 2) Community repo blueprints (lightweight header parsing)
791
+ if repo_path.is_dir():
792
+ extensions = (".md", ".yaml", ".yml")
793
+ for category_dir in sorted(repo_path.iterdir()):
794
+ if not category_dir.is_dir():
795
+ continue
796
+ category = category_dir.name
797
+ for bp_file in sorted(category_dir.iterdir()):
798
+ if bp_file.suffix.lower() not in extensions:
799
+ continue
800
+ if bp_file.name.startswith(".") or bp_file.name.upper() == "README.MD":
801
+ continue
802
+
803
+ stem = bp_file.stem
804
+ slug = _slugify(stem.replace("_", " "))
805
+
806
+ # Skip if already seen as installed
807
+ if slug in seen:
808
+ continue
809
+
810
+ # Lightweight description extraction (first meaningful line)
811
+ desc = ""
812
+ try:
813
+ with open(bp_file, encoding="utf-8") as f:
814
+ for line in f:
815
+ line = line.strip()
816
+ if not line or line.startswith("#"):
817
+ continue
818
+ if line.startswith(">"):
819
+ desc = line.lstrip("> ").strip()
820
+ break
821
+ if line.startswith("**"):
822
+ # Extract value from **Key**: Value
823
+ if "vibe" in line.lower() or "essence" in line.lower():
824
+ parts = line.split(":", 1)
825
+ if len(parts) > 1:
826
+ desc = parts[1].strip().strip("*")
827
+ break
828
+ if line.startswith("-") or line[0].isalpha():
829
+ desc = line[:80]
830
+ break
831
+ except (OSError, UnicodeDecodeError):
832
+ pass
833
+
834
+ seen[slug] = {
835
+ "name": slug,
836
+ "display_name": stem.replace("_", " ").replace("-", " ").title(),
837
+ "category": category,
838
+ "source": "repo",
839
+ "description": desc[:80] if desc else "",
840
+ }
841
+
842
+ # Sort by category, then name
843
+ return sorted(seen.values(), key=lambda d: (d["category"], d["name"]))
844
+
845
+ def get_active_soul_name(self) -> Optional[str]:
846
+ """Get the name of the currently active soul overlay.
847
+
848
+ Returns:
849
+ Soul slug name, or None if at base.
850
+ """
851
+ active_path = self.soul_dir / "active.json"
852
+ if not active_path.exists():
853
+ return None
854
+ try:
855
+ data = json.loads(active_path.read_text(encoding="utf-8"))
856
+ return data.get("active_soul")
857
+ except (json.JSONDecodeError, Exception):
858
+ return None
859
+
860
+ def get_registry(self) -> "SoulRegistry":
861
+ """Get a SoulRegistry backed by this manager's installed souls.
862
+
863
+ Returns:
864
+ SoulRegistry scoped to the installed soul directory.
865
+ """
866
+ self._ensure_dirs()
867
+ return SoulRegistry(self.soul_dir / "installed")
868
+
869
+ # -- Private helpers --
870
+
871
+ def _load_state(self) -> SoulState:
872
+ """Load soul state from disk."""
873
+ path = self.soul_dir / "active.json"
874
+ if not path.exists():
875
+ return SoulState()
876
+ try:
877
+ data = json.loads(path.read_text(encoding="utf-8"))
878
+ return SoulState.model_validate(data)
879
+ except (json.JSONDecodeError, Exception):
880
+ return SoulState()
881
+
882
+ def _save_state(self, state: SoulState) -> None:
883
+ """Persist soul state to disk."""
884
+ path = self.soul_dir / "active.json"
885
+ path.write_text(state.model_dump_json(indent=2), encoding="utf-8")
886
+
887
+ def _append_history(self, event: SoulSwapEvent) -> None:
888
+ """Append a swap event to the history log."""
889
+ history_path = self.soul_dir / "history.json"
890
+ history: list[dict] = []
891
+ if history_path.exists():
892
+ try:
893
+ history = json.loads(history_path.read_text(encoding="utf-8"))
894
+ except (json.JSONDecodeError, Exception):
895
+ history = []
896
+ history.append(event.model_dump())
897
+ history_path.write_text(json.dumps(history, indent=2), encoding="utf-8")
898
+
899
+
900
+ # ---------------------------------------------------------------------------
901
+ # SoulRegistry — programmatic soul discovery and search
902
+ # ---------------------------------------------------------------------------
903
+
904
+
905
+ class SoulRegistry:
906
+ """Registry for discovering and searching installed soul blueprints.
907
+
908
+ Unlike SoulManager (which handles lifecycle — install/load/unload),
909
+ the registry is a read-only index for programmatic soul discovery.
910
+ Team blueprints and MCP tools use this to find and select souls.
911
+
912
+ Args:
913
+ source: Directory containing soul JSON files (installed/) or YAML files.
914
+ """
915
+
916
+ def __init__(self, source: Path) -> None:
917
+ self.source = source
918
+ self._cache: dict[str, SoulBlueprint] = {}
919
+ self._loaded = False
920
+
921
+ def _ensure_loaded(self) -> None:
922
+ """Lazy-load all soul blueprints from the source directory."""
923
+ if self._loaded:
924
+ return
925
+ self._cache.clear()
926
+ if not self.source.exists():
927
+ self._loaded = True
928
+ return
929
+ for path in sorted(self.source.iterdir()):
930
+ if path.suffix == ".json":
931
+ try:
932
+ data = json.loads(path.read_text(encoding="utf-8"))
933
+ bp = SoulBlueprint.model_validate(data)
934
+ self._cache[bp.name] = bp
935
+ except (json.JSONDecodeError, Exception) as exc:
936
+ logger.warning("Registry: skipping %s: %s", path.name, exc)
937
+ elif path.suffix in (".yaml", ".yml"):
938
+ try:
939
+ bp = load_yaml_blueprint(path)
940
+ self._cache[bp.name] = bp
941
+ except (ValueError, FileNotFoundError) as exc:
942
+ logger.warning("Registry: skipping %s: %s", path.name, exc)
943
+ self._loaded = True
944
+
945
+ def reload(self) -> None:
946
+ """Force reload the registry from disk."""
947
+ self._loaded = False
948
+ self._ensure_loaded()
949
+
950
+ def list_all(self) -> list[SoulBlueprint]:
951
+ """List all registered soul blueprints.
952
+
953
+ Returns:
954
+ Sorted list of all SoulBlueprint objects.
955
+ """
956
+ self._ensure_loaded()
957
+ return sorted(self._cache.values(), key=lambda b: b.name)
958
+
959
+ def list_names(self) -> list[str]:
960
+ """List all registered soul names.
961
+
962
+ Returns:
963
+ Sorted list of soul slug names.
964
+ """
965
+ self._ensure_loaded()
966
+ return sorted(self._cache.keys())
967
+
968
+ def get(self, name: str) -> Optional[SoulBlueprint]:
969
+ """Get a soul blueprint by name.
970
+
971
+ Args:
972
+ name: Soul slug name.
973
+
974
+ Returns:
975
+ SoulBlueprint or None if not found.
976
+ """
977
+ self._ensure_loaded()
978
+ return self._cache.get(name)
979
+
980
+ def search(
981
+ self,
982
+ *,
983
+ category: Optional[str] = None,
984
+ trait_keyword: Optional[str] = None,
985
+ min_topology: Optional[dict[str, float]] = None,
986
+ ) -> list[SoulBlueprint]:
987
+ """Search souls by category, trait keywords, or topology thresholds.
988
+
989
+ All filters are ANDed together.
990
+
991
+ Args:
992
+ category: Filter by category (e.g. "professional", "comedy").
993
+ trait_keyword: Filter by keyword present in core_traits (case-insensitive).
994
+ min_topology: Filter by minimum emotional topology values
995
+ (e.g. {"warmth": 0.5} returns souls with warmth >= 0.5).
996
+
997
+ Returns:
998
+ List of matching SoulBlueprint objects, sorted by name.
999
+ """
1000
+ self._ensure_loaded()
1001
+ results: list[SoulBlueprint] = []
1002
+ for bp in self._cache.values():
1003
+ if category and bp.category.lower() != category.lower():
1004
+ continue
1005
+ if trait_keyword:
1006
+ kw = trait_keyword.lower()
1007
+ if not any(kw in t.lower() for t in bp.core_traits):
1008
+ continue
1009
+ if min_topology:
1010
+ skip = False
1011
+ for dim, threshold in min_topology.items():
1012
+ if bp.emotional_topology.get(dim, 0.0) < threshold:
1013
+ skip = True
1014
+ break
1015
+ if skip:
1016
+ continue
1017
+ results.append(bp)
1018
+ return sorted(results, key=lambda b: b.name)
1019
+
1020
+ def by_category(self) -> dict[str, list[SoulBlueprint]]:
1021
+ """Group all souls by category.
1022
+
1023
+ Returns:
1024
+ Dict mapping category name to list of SoulBlueprint objects.
1025
+ """
1026
+ self._ensure_loaded()
1027
+ groups: dict[str, list[SoulBlueprint]] = {}
1028
+ for bp in self._cache.values():
1029
+ groups.setdefault(bp.category, []).append(bp)
1030
+ for bps in groups.values():
1031
+ bps.sort(key=lambda b: b.name)
1032
+ return dict(sorted(groups.items()))
1033
+
1034
+ def count(self) -> int:
1035
+ """Return the total number of registered souls."""
1036
+ self._ensure_loaded()
1037
+ return len(self._cache)
1038
+
1039
+ def categories(self) -> list[str]:
1040
+ """List all unique categories.
1041
+
1042
+ Returns:
1043
+ Sorted list of category names.
1044
+ """
1045
+ self._ensure_loaded()
1046
+ return sorted({bp.category for bp in self._cache.values()})
1047
+
1048
+ def summary(self) -> dict:
1049
+ """Return a summary of the registry contents.
1050
+
1051
+ Returns:
1052
+ Dict with total count, categories, and per-category counts.
1053
+ """
1054
+ self._ensure_loaded()
1055
+ by_cat = self.by_category()
1056
+ return {
1057
+ "total": len(self._cache),
1058
+ "categories": {cat: len(bps) for cat, bps in by_cat.items()},
1059
+ "souls": self.list_names(),
1060
+ }