@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,744 @@
1
+ """Integration tests for the skills-registry subsystem.
2
+
3
+ Covers:
4
+ - discover_skills(): listing skills from local filesystem
5
+ - enable/disable semantics (ACTIVE vs DEGRADED vs MISSING status)
6
+ - skskills_list_tools MCP handler: correct JSON response structure
7
+ - skskills_run_tool MCP handler: executes tools and returns results
8
+
9
+ Run only these tests:
10
+ pytest tests/integration/test_skills_registry.py -v
11
+
12
+ Skip slow integration tests in CI:
13
+ pytest -m "not integration" tests/
14
+
15
+ Related coordination task: f8dfda3493c0ed72
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ from pathlib import Path
23
+ from textwrap import dedent
24
+ from typing import Any
25
+ from unittest.mock import AsyncMock, MagicMock, patch
26
+
27
+ import pytest
28
+
29
+ # Pre-import to ensure the module stays in sys.modules throughout the test
30
+ # run. patch.dict(sys.modules, ...) snapshots the current state; importing
31
+ # here means the module is included in that snapshot and is never removed
32
+ # between tests (which would trigger a broken pydantic re-import).
33
+ from skcapstone.mcp_tools.skills_tools import (
34
+ HANDLERS,
35
+ TOOLS,
36
+ _handle_skskills_list_tools,
37
+ _handle_skskills_run_tool,
38
+ )
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Fixtures
43
+ # ---------------------------------------------------------------------------
44
+
45
+
46
+ @pytest.fixture
47
+ def skskills_home(tmp_path: Path) -> Path:
48
+ """Populated SKSkills home directory for integration tests."""
49
+ home = tmp_path / "skskills"
50
+
51
+ # Global installed skills
52
+ for skill_name, version, tags in [
53
+ ("syncthing-setup", "1.0.0", ["sync"]),
54
+ ("pgp-identity", "0.2.0", ["identity"]),
55
+ ]:
56
+ skill_dir = home / "installed" / skill_name
57
+ skill_dir.mkdir(parents=True)
58
+ (skill_dir / "skill.yaml").write_text(dedent(f"""\
59
+ name: {skill_name}
60
+ version: "{version}"
61
+ description: Test skill {skill_name}
62
+ tags: {tags}
63
+ author:
64
+ name: tester
65
+ """))
66
+
67
+ # Per-agent skill (jarvis)
68
+ agent_skill = home / "agents" / "jarvis" / "code-review"
69
+ agent_skill.mkdir(parents=True)
70
+ (agent_skill / "skill.yaml").write_text(dedent("""\
71
+ name: code-review
72
+ version: "0.1.0"
73
+ description: Code review skill
74
+ tags: [review]
75
+ author:
76
+ name: tester
77
+ """))
78
+
79
+ return home
80
+
81
+
82
+ @pytest.fixture
83
+ def agent_home(tmp_path: Path) -> Path:
84
+ """Minimal skcapstone agent home directory."""
85
+ home = tmp_path / ".skcapstone"
86
+ home.mkdir()
87
+ (home / "skills").mkdir()
88
+ return home
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # TestSkillDiscovery — discover_skills() filesystem integration
93
+ # ---------------------------------------------------------------------------
94
+
95
+
96
+ @pytest.mark.integration
97
+ class TestSkillDiscovery:
98
+ """Test discover_skills() reading from real filesystem (tmp_path)."""
99
+
100
+ def test_discovers_global_skills(self, agent_home: Path, skskills_home: Path):
101
+ """Should list all skills in ~/.skskills/installed/."""
102
+ from skcapstone.discovery import discover_skills
103
+
104
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
105
+ with patch("skcapstone.discovery._probe_remote_registry"):
106
+ state = discover_skills(agent_home)
107
+
108
+ assert state.installed == 2
109
+ assert "syncthing-setup" in state.skill_names
110
+ assert "pgp-identity" in state.skill_names
111
+
112
+ def test_discovers_per_agent_skills(self, agent_home: Path, skskills_home: Path):
113
+ """Should include per-agent skills when agent is specified."""
114
+ from skcapstone.discovery import discover_skills
115
+
116
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
117
+ with patch("skcapstone.discovery._probe_remote_registry"):
118
+ state = discover_skills(agent_home, agent="jarvis")
119
+
120
+ assert "code-review" in state.skill_names
121
+ assert state.installed == 3 # 2 global + 1 agent
122
+
123
+ def test_no_duplicate_when_skill_in_both_namespaces(
124
+ self, agent_home: Path, skskills_home: Path
125
+ ):
126
+ """Same skill in global + agent namespace should appear once."""
127
+ from skcapstone.discovery import discover_skills
128
+
129
+ # Add syncthing-setup also in jarvis agent namespace
130
+ dup = skskills_home / "agents" / "jarvis" / "syncthing-setup"
131
+ dup.mkdir(parents=True)
132
+ (dup / "skill.yaml").write_text("name: syncthing-setup\nversion: '1.0.0'\n")
133
+
134
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
135
+ with patch("skcapstone.discovery._probe_remote_registry"):
136
+ state = discover_skills(agent_home, agent="jarvis")
137
+
138
+ assert state.skill_names.count("syncthing-setup") == 1
139
+
140
+ def test_status_active_when_skills_found(self, agent_home: Path, skskills_home: Path):
141
+ """Status should be ACTIVE when at least one skill is installed."""
142
+ from skcapstone.discovery import discover_skills
143
+ from skcapstone.models import PillarStatus
144
+
145
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
146
+ with patch("skcapstone.discovery._probe_remote_registry"):
147
+ state = discover_skills(agent_home)
148
+
149
+ assert state.status == PillarStatus.ACTIVE
150
+
151
+ def test_status_degraded_when_home_empty(self, agent_home: Path, tmp_path: Path):
152
+ """Status should be DEGRADED when skskills home exists but has no skills."""
153
+ from skcapstone.discovery import discover_skills
154
+ from skcapstone.models import PillarStatus
155
+
156
+ empty_home = tmp_path / "empty_skskills"
157
+ empty_home.mkdir()
158
+ (empty_home / "installed").mkdir()
159
+
160
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(empty_home)}):
161
+ with patch("skcapstone.discovery._probe_remote_registry"):
162
+ state = discover_skills(agent_home)
163
+
164
+ assert state.status == PillarStatus.DEGRADED
165
+
166
+ def test_status_missing_when_no_skskills_home(self, agent_home: Path, tmp_path: Path):
167
+ """Status should be MISSING when skskills home directory does not exist."""
168
+ from skcapstone.discovery import discover_skills
169
+ from skcapstone.models import PillarStatus
170
+
171
+ nonexistent = tmp_path / "no_such_dir"
172
+
173
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(nonexistent)}):
174
+ with patch("skcapstone.discovery._probe_remote_registry"):
175
+ state = discover_skills(agent_home)
176
+
177
+ assert state.status == PillarStatus.MISSING
178
+
179
+ def test_skill_names_sorted(self, agent_home: Path, skskills_home: Path):
180
+ """skill_names should be returned in sorted order."""
181
+ from skcapstone.discovery import discover_skills
182
+
183
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
184
+ with patch("skcapstone.discovery._probe_remote_registry"):
185
+ state = discover_skills(agent_home)
186
+
187
+ assert state.skill_names == sorted(state.skill_names)
188
+
189
+ def test_per_agent_skcapstone_skills(self, agent_home: Path, skskills_home: Path):
190
+ """Per-agent skcapstone skills (highest priority) should be discovered."""
191
+ from skcapstone.discovery import discover_skills
192
+
193
+ # Create per-agent skcapstone skill (priority 1)
194
+ skcap_agent_dir = agent_home / "skills" / "agents" / "opus"
195
+ skcap_agent_dir.mkdir(parents=True)
196
+ local_skill = skcap_agent_dir / "local-deploy"
197
+ local_skill.mkdir()
198
+ (local_skill / "skill.yaml").write_text("name: local-deploy\nversion: '0.1.0'\n")
199
+
200
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
201
+ with patch("skcapstone.discovery._probe_remote_registry"):
202
+ state = discover_skills(agent_home, agent="opus")
203
+
204
+ assert "local-deploy" in state.skill_names
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # TestRegistryClientIntegration — RegistryClient with mocked HTTP
209
+ # ---------------------------------------------------------------------------
210
+
211
+
212
+ class TestRegistryClientIntegration:
213
+ """Integration tests for RegistryClient against a mocked RemoteRegistry."""
214
+
215
+ def _make_mock_module(self, skills: list[dict[str, Any]]) -> tuple:
216
+ """Return (mock_sys_module, mock_remote_instance) with the given skill entries."""
217
+ mock_entries = [MagicMock() for _ in skills]
218
+ for entry, data in zip(mock_entries, skills):
219
+ entry.model_dump.return_value = data
220
+
221
+ mock_index = MagicMock()
222
+ mock_index.skills = mock_entries
223
+
224
+ mock_remote = MagicMock()
225
+ mock_remote.fetch_index.return_value = mock_index
226
+ mock_remote.search.return_value = mock_entries
227
+
228
+ mock_module = MagicMock()
229
+ mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
230
+ return mock_module, mock_remote
231
+
232
+ def test_list_skills_returns_all(self):
233
+ """list_skills() should return one dict per registry entry."""
234
+ from skcapstone.registry_client import RegistryClient
235
+
236
+ catalog = [
237
+ {"name": "syncthing-setup", "version": "1.0.0", "tags": ["sync"]},
238
+ {"name": "pgp-identity", "version": "0.2.0", "tags": ["identity"]},
239
+ ]
240
+ mock_module, _ = self._make_mock_module(catalog)
241
+
242
+ with patch.dict("sys.modules", {"skskills.remote": mock_module}):
243
+ client = RegistryClient("https://test.example.com/api")
244
+ result = client.list_skills()
245
+
246
+ assert len(result) == 2
247
+ names = {s["name"] for s in result}
248
+ assert names == {"syncthing-setup", "pgp-identity"}
249
+
250
+ def test_search_returns_matching_skills(self):
251
+ """search() should delegate to RemoteRegistry.search() and return dicts."""
252
+ from skcapstone.registry_client import RegistryClient
253
+
254
+ sync_entry = {"name": "syncthing-setup", "version": "1.0.0", "tags": ["sync"]}
255
+ sync_mock = MagicMock()
256
+ sync_mock.model_dump.return_value = sync_entry
257
+
258
+ mock_remote = MagicMock()
259
+ mock_remote.search.return_value = [sync_mock]
260
+
261
+ mock_module = MagicMock()
262
+ mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
263
+
264
+ with patch.dict("sys.modules", {"skskills.remote": mock_module}):
265
+ client = RegistryClient("https://test.example.com/api")
266
+ result = client.search("syncthing")
267
+
268
+ assert len(result) == 1
269
+ assert result[0]["name"] == "syncthing-setup"
270
+
271
+ def test_is_available_true_when_registry_responds(self):
272
+ """is_available() returns True when fetch_index does not raise."""
273
+ from skcapstone.registry_client import RegistryClient
274
+
275
+ mock_remote = MagicMock()
276
+ mock_remote.fetch_index.return_value = MagicMock(skills=[])
277
+ mock_module = MagicMock()
278
+ mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
279
+
280
+ with patch.dict("sys.modules", {"skskills.remote": mock_module}):
281
+ client = RegistryClient("https://test.example.com/api")
282
+ assert client.is_available() is True
283
+
284
+ def test_is_available_false_when_registry_unreachable(self):
285
+ """is_available() returns False when the registry is offline."""
286
+ from skcapstone.registry_client import RegistryClient
287
+
288
+ mock_remote = MagicMock()
289
+ mock_remote.fetch_index.side_effect = ConnectionError("network error")
290
+ mock_module = MagicMock()
291
+ mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
292
+
293
+ with patch.dict("sys.modules", {"skskills.remote": mock_module}):
294
+ client = RegistryClient("https://test.example.com/api")
295
+ assert client.is_available() is False
296
+
297
+ def test_install_returns_metadata_dict(self):
298
+ """install() should return dict with name/version/agent/install_path/status."""
299
+ from skcapstone.registry_client import RegistryClient
300
+
301
+ installed = MagicMock()
302
+ installed.manifest.name = "syncthing-setup"
303
+ installed.manifest.version = "1.0.0"
304
+ installed.agent = "global"
305
+ installed.install_path = "/home/user/.skskills/installed/syncthing-setup"
306
+ installed.status.value = "installed"
307
+
308
+ mock_remote = MagicMock()
309
+ mock_remote.pull.return_value = installed
310
+ mock_module = MagicMock()
311
+ mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
312
+
313
+ with patch.dict("sys.modules", {"skskills.remote": mock_module}):
314
+ client = RegistryClient("https://test.example.com/api")
315
+ result = client.install("syncthing-setup")
316
+
317
+ assert result["name"] == "syncthing-setup"
318
+ assert result["version"] == "1.0.0"
319
+ assert result["agent"] == "global"
320
+ assert "install_path" in result
321
+ assert result["status"] == "installed"
322
+
323
+ def test_get_skill_returns_none_when_not_found(self):
324
+ """get_skill() returns None when the remote has no matching skill."""
325
+ from skcapstone.registry_client import RegistryClient
326
+
327
+ mock_remote = MagicMock()
328
+ mock_remote.get_skill_info.return_value = None
329
+ mock_module = MagicMock()
330
+ mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
331
+
332
+ with patch.dict("sys.modules", {"skskills.remote": mock_module}):
333
+ client = RegistryClient("https://test.example.com/api")
334
+ result = client.get_skill("nonexistent")
335
+
336
+ assert result is None
337
+
338
+ def test_list_skills_returns_empty_when_registry_empty(self):
339
+ """list_skills() returns [] when no skills are in the registry."""
340
+ from skcapstone.registry_client import RegistryClient
341
+
342
+ mock_index = MagicMock()
343
+ mock_index.skills = []
344
+ mock_remote = MagicMock()
345
+ mock_remote.fetch_index.return_value = mock_index
346
+ mock_module = MagicMock()
347
+ mock_module.RemoteRegistry = MagicMock(return_value=mock_remote)
348
+
349
+ with patch.dict("sys.modules", {"skskills.remote": mock_module}):
350
+ client = RegistryClient("https://test.example.com/api")
351
+ result = client.list_skills()
352
+
353
+ assert result == []
354
+
355
+
356
+ # ---------------------------------------------------------------------------
357
+ # Helpers for MCP handler tests
358
+ # ---------------------------------------------------------------------------
359
+
360
+
361
+ def _make_aggregator_module(
362
+ tools: list[dict],
363
+ skills: list[dict],
364
+ skills_loaded: int,
365
+ call_result: Any = None,
366
+ ) -> MagicMock:
367
+ """Build a mock skskills.aggregator module."""
368
+ mock_loader = MagicMock()
369
+ mock_loader.all_tools.return_value = tools
370
+ mock_loader.call_tool = AsyncMock(return_value=call_result)
371
+
372
+ mock_agg = MagicMock()
373
+ mock_agg.load_all_skills.return_value = skills_loaded
374
+ mock_agg.loader = mock_loader
375
+ mock_agg.get_loaded_skills.return_value = skills
376
+
377
+ mock_module = MagicMock()
378
+ mock_module.SkillAggregator = MagicMock(return_value=mock_agg)
379
+ return mock_module
380
+
381
+
382
+ # ---------------------------------------------------------------------------
383
+ # TestSkillsListToolsMCP — skskills_list_tools handler
384
+ # ---------------------------------------------------------------------------
385
+
386
+
387
+ class TestSkillsListToolsMCP:
388
+ """Integration tests for the skskills_list_tools MCP handler."""
389
+
390
+ @pytest.mark.asyncio
391
+ async def test_returns_tools_list(self, tmp_path: Path):
392
+ """skskills_list_tools should return a 'tools' list in the JSON response."""
393
+ tools = [
394
+ {
395
+ "name": "syncthing-setup.check_status",
396
+ "description": "Check Syncthing status",
397
+ "inputSchema": {"type": "object", "properties": {}},
398
+ }
399
+ ]
400
+ mock_agg_module = _make_aggregator_module(tools=tools, skills=[], skills_loaded=1)
401
+
402
+ with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
403
+ with patch(
404
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
405
+ ):
406
+ with patch(
407
+ "skcapstone.mcp_tools.skills_tools._get_agent_name",
408
+ return_value="test-agent",
409
+ ):
410
+ result = await _handle_skskills_list_tools({"agent": "test-agent"})
411
+
412
+ assert len(result) == 1
413
+ data = json.loads(result[0].text)
414
+ assert "tools" in data
415
+ assert len(data["tools"]) == 1
416
+ assert data["tools"][0]["name"] == "syncthing-setup.check_status"
417
+
418
+ @pytest.mark.asyncio
419
+ async def test_returns_skills_metadata(self, tmp_path: Path):
420
+ """skskills_list_tools should include loaded skills metadata."""
421
+ skills = [{"name": "syncthing-setup", "version": "1.0.0"}]
422
+ mock_agg_module = _make_aggregator_module(tools=[], skills=skills, skills_loaded=1)
423
+
424
+ with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
425
+ with patch(
426
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
427
+ ):
428
+ with patch(
429
+ "skcapstone.mcp_tools.skills_tools._get_agent_name",
430
+ return_value="test-agent",
431
+ ):
432
+ result = await _handle_skskills_list_tools({})
433
+
434
+ data = json.loads(result[0].text)
435
+ assert data["skills_loaded"] == 1
436
+ assert data["skills"] == [{"name": "syncthing-setup", "version": "1.0.0"}]
437
+
438
+ @pytest.mark.asyncio
439
+ async def test_tools_have_required_fields(self, tmp_path: Path):
440
+ """Each tool entry must expose name, description, and inputSchema."""
441
+ tools = [
442
+ {
443
+ "name": "pgp-identity.show_key",
444
+ "description": "Show PGP public key",
445
+ "inputSchema": {
446
+ "type": "object",
447
+ "properties": {"format": {"type": "string"}},
448
+ },
449
+ }
450
+ ]
451
+ mock_agg_module = _make_aggregator_module(tools=tools, skills=[], skills_loaded=1)
452
+
453
+ with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
454
+ with patch(
455
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
456
+ ):
457
+ with patch(
458
+ "skcapstone.mcp_tools.skills_tools._get_agent_name",
459
+ return_value="opus",
460
+ ):
461
+ result = await _handle_skskills_list_tools({"agent": "opus"})
462
+
463
+ data = json.loads(result[0].text)
464
+ for tool in data["tools"]:
465
+ assert "name" in tool
466
+ assert "description" in tool
467
+ assert "inputSchema" in tool
468
+
469
+ @pytest.mark.asyncio
470
+ async def test_returns_error_when_skskills_missing(self, tmp_path: Path):
471
+ """Should return an error response when skskills is not installed.
472
+
473
+ Setting sys.modules["skskills.aggregator"] = None causes Python to
474
+ raise ImportError when the handler does `from skskills.aggregator import ...`.
475
+ """
476
+ with patch.dict("sys.modules", {"skskills.aggregator": None}):
477
+ with patch(
478
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
479
+ ):
480
+ result = await _handle_skskills_list_tools({})
481
+
482
+ data = json.loads(result[0].text)
483
+ assert "error" in data
484
+
485
+ @pytest.mark.asyncio
486
+ async def test_agent_name_in_response(self, tmp_path: Path):
487
+ """The response should include the agent namespace used."""
488
+ mock_agg_module = _make_aggregator_module(tools=[], skills=[], skills_loaded=0)
489
+
490
+ with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
491
+ with patch(
492
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
493
+ ):
494
+ with patch(
495
+ "skcapstone.mcp_tools.skills_tools._get_agent_name",
496
+ return_value="jarvis",
497
+ ):
498
+ result = await _handle_skskills_list_tools({"agent": "jarvis"})
499
+
500
+ data = json.loads(result[0].text)
501
+ assert data["agent"] == "jarvis"
502
+
503
+ @pytest.mark.asyncio
504
+ async def test_zero_skills_returns_empty_lists(self, tmp_path: Path):
505
+ """When no skills are installed, tools and skills lists should be empty."""
506
+ mock_agg_module = _make_aggregator_module(tools=[], skills=[], skills_loaded=0)
507
+
508
+ with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
509
+ with patch(
510
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
511
+ ):
512
+ with patch(
513
+ "skcapstone.mcp_tools.skills_tools._get_agent_name",
514
+ return_value="anonymous",
515
+ ):
516
+ result = await _handle_skskills_list_tools({})
517
+
518
+ data = json.loads(result[0].text)
519
+ assert data["skills_loaded"] == 0
520
+ assert data["tools"] == []
521
+ assert data["skills"] == []
522
+
523
+
524
+ # ---------------------------------------------------------------------------
525
+ # TestSkillsRunToolMCP — skskills_run_tool handler
526
+ # ---------------------------------------------------------------------------
527
+
528
+
529
+ class TestSkillsRunToolMCP:
530
+ """Integration tests for the skskills_run_tool MCP handler."""
531
+
532
+ @pytest.mark.asyncio
533
+ async def test_executes_tool_and_returns_json_result(self, tmp_path: Path):
534
+ """Should call the tool and return its dict result as JSON."""
535
+ mock_agg_module = _make_aggregator_module(
536
+ tools=[], skills=[], skills_loaded=1,
537
+ call_result={"status": "ok", "version": "1.9.3"},
538
+ )
539
+
540
+ with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
541
+ with patch(
542
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
543
+ ):
544
+ with patch(
545
+ "skcapstone.mcp_tools.skills_tools._get_agent_name",
546
+ return_value="test-agent",
547
+ ):
548
+ result = await _handle_skskills_run_tool(
549
+ {"tool": "syncthing-setup.check_status"}
550
+ )
551
+
552
+ data = json.loads(result[0].text)
553
+ assert data["status"] == "ok"
554
+ assert data["version"] == "1.9.3"
555
+
556
+ # Verify call_tool was invoked with the correct args
557
+ agg_instance = mock_agg_module.SkillAggregator.return_value
558
+ agg_instance.loader.call_tool.assert_awaited_once_with(
559
+ "syncthing-setup.check_status", {}
560
+ )
561
+
562
+ @pytest.mark.asyncio
563
+ async def test_passes_args_to_tool(self, tmp_path: Path):
564
+ """Tool arguments should be forwarded verbatim to call_tool."""
565
+ mock_agg_module = _make_aggregator_module(
566
+ tools=[], skills=[], skills_loaded=1, call_result={"result": "done"}
567
+ )
568
+
569
+ with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
570
+ with patch(
571
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
572
+ ):
573
+ with patch(
574
+ "skcapstone.mcp_tools.skills_tools._get_agent_name",
575
+ return_value="test-agent",
576
+ ):
577
+ await _handle_skskills_run_tool(
578
+ {
579
+ "tool": "pgp-identity.show_key",
580
+ "args": {"format": "armored"},
581
+ }
582
+ )
583
+
584
+ agg_instance = mock_agg_module.SkillAggregator.return_value
585
+ agg_instance.loader.call_tool.assert_awaited_once_with(
586
+ "pgp-identity.show_key", {"format": "armored"}
587
+ )
588
+
589
+ @pytest.mark.asyncio
590
+ async def test_missing_tool_arg_returns_error(self, tmp_path: Path):
591
+ """Should return an error when the 'tool' argument is absent."""
592
+ mock_agg_module = _make_aggregator_module(
593
+ tools=[], skills=[], skills_loaded=0, call_result=None
594
+ )
595
+
596
+ with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
597
+ with patch(
598
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
599
+ ):
600
+ result = await _handle_skskills_run_tool({})
601
+
602
+ data = json.loads(result[0].text)
603
+ assert "error" in data
604
+
605
+ @pytest.mark.asyncio
606
+ async def test_unknown_tool_returns_error(self, tmp_path: Path):
607
+ """Should return an error when call_tool raises KeyError (unknown tool)."""
608
+ mock_loader = MagicMock()
609
+ mock_loader.call_tool = AsyncMock(side_effect=KeyError("no such tool: xyz.run"))
610
+ mock_agg = MagicMock()
611
+ mock_agg.load_all_skills.return_value = 1
612
+ mock_agg.loader = mock_loader
613
+ mock_module = MagicMock()
614
+ mock_module.SkillAggregator = MagicMock(return_value=mock_agg)
615
+
616
+ with patch.dict("sys.modules", {"skskills.aggregator": mock_module}):
617
+ with patch(
618
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
619
+ ):
620
+ with patch(
621
+ "skcapstone.mcp_tools.skills_tools._get_agent_name",
622
+ return_value="test-agent",
623
+ ):
624
+ result = await _handle_skskills_run_tool({"tool": "xyz.run"})
625
+
626
+ data = json.loads(result[0].text)
627
+ assert "error" in data
628
+
629
+ @pytest.mark.asyncio
630
+ async def test_string_result_returned_as_plain_text(self, tmp_path: Path):
631
+ """When the tool returns a string, response should be plain text (not JSON)."""
632
+ mock_agg_module = _make_aggregator_module(
633
+ tools=[], skills=[], skills_loaded=1,
634
+ call_result="Syncthing is running.",
635
+ )
636
+
637
+ with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
638
+ with patch(
639
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
640
+ ):
641
+ with patch(
642
+ "skcapstone.mcp_tools.skills_tools._get_agent_name",
643
+ return_value="test-agent",
644
+ ):
645
+ result = await _handle_skskills_run_tool(
646
+ {"tool": "syncthing-setup.check_status"}
647
+ )
648
+
649
+ assert result[0].text == "Syncthing is running."
650
+
651
+ @pytest.mark.asyncio
652
+ async def test_tool_runtime_exception_returns_error(self, tmp_path: Path):
653
+ """Unexpected exceptions from call_tool should produce an error response."""
654
+ mock_loader = MagicMock()
655
+ mock_loader.call_tool = AsyncMock(side_effect=RuntimeError("daemon not running"))
656
+ mock_agg = MagicMock()
657
+ mock_agg.load_all_skills.return_value = 1
658
+ mock_agg.loader = mock_loader
659
+ mock_module = MagicMock()
660
+ mock_module.SkillAggregator = MagicMock(return_value=mock_agg)
661
+
662
+ with patch.dict("sys.modules", {"skskills.aggregator": mock_module}):
663
+ with patch(
664
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
665
+ ):
666
+ with patch(
667
+ "skcapstone.mcp_tools.skills_tools._get_agent_name",
668
+ return_value="test-agent",
669
+ ):
670
+ result = await _handle_skskills_run_tool(
671
+ {"tool": "syncthing-setup.check_status"}
672
+ )
673
+
674
+ data = json.loads(result[0].text)
675
+ assert "error" in data
676
+ assert "syncthing-setup.check_status" in data["error"]
677
+
678
+ @pytest.mark.asyncio
679
+ async def test_uses_specified_agent_namespace(self, tmp_path: Path):
680
+ """SkillAggregator should be constructed with the agent from args."""
681
+ mock_agg_module = _make_aggregator_module(
682
+ tools=[], skills=[], skills_loaded=1, call_result={"ok": True}
683
+ )
684
+
685
+ with patch.dict("sys.modules", {"skskills.aggregator": mock_agg_module}):
686
+ with patch(
687
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
688
+ ):
689
+ await _handle_skskills_run_tool(
690
+ {"tool": "syncthing-setup.check_status", "agent": "jarvis"}
691
+ )
692
+
693
+ mock_agg_module.SkillAggregator.assert_called_once_with(agent="jarvis")
694
+
695
+ @pytest.mark.asyncio
696
+ async def test_returns_error_when_skskills_missing(self, tmp_path: Path):
697
+ """Should return an error response when skskills is not installed."""
698
+ with patch.dict("sys.modules", {"skskills.aggregator": None}):
699
+ with patch(
700
+ "skcapstone.mcp_tools.skills_tools._home", return_value=tmp_path
701
+ ):
702
+ result = await _handle_skskills_run_tool({"tool": "syncthing-setup.run"})
703
+
704
+ data = json.loads(result[0].text)
705
+ assert "error" in data
706
+
707
+
708
+ # ---------------------------------------------------------------------------
709
+ # TestMCPToolRegistration — verify TOOLS and HANDLERS are wired correctly
710
+ # ---------------------------------------------------------------------------
711
+
712
+
713
+ class TestMCPToolRegistration:
714
+ """Verify the skills MCP tools are properly declared in TOOLS and HANDLERS."""
715
+
716
+ def test_tools_list_contains_expected_names(self):
717
+ """TOOLS list should declare both skskills tools."""
718
+ tool_names = {t.name for t in TOOLS}
719
+ assert "skskills_list_tools" in tool_names
720
+ assert "skskills_run_tool" in tool_names
721
+
722
+ def test_handlers_map_contains_expected_names(self):
723
+ """HANDLERS dict should map both tool names to callable handlers."""
724
+ assert "skskills_list_tools" in HANDLERS
725
+ assert "skskills_run_tool" in HANDLERS
726
+ assert callable(HANDLERS["skskills_list_tools"])
727
+ assert callable(HANDLERS["skskills_run_tool"])
728
+
729
+ def test_skskills_run_tool_requires_tool_arg(self):
730
+ """skskills_run_tool inputSchema must declare 'tool' as required."""
731
+ run_tool = next(t for t in TOOLS if t.name == "skskills_run_tool")
732
+ assert "tool" in run_tool.inputSchema["required"]
733
+
734
+ def test_skskills_list_tools_no_required_args(self):
735
+ """skskills_list_tools inputSchema should have no required arguments."""
736
+ list_tool = next(t for t in TOOLS if t.name == "skskills_list_tools")
737
+ assert list_tool.inputSchema.get("required", []) == []
738
+
739
+ def test_handlers_are_coroutines(self):
740
+ """Both handlers should be async coroutine functions."""
741
+ import asyncio
742
+
743
+ assert asyncio.iscoroutinefunction(HANDLERS["skskills_list_tools"])
744
+ assert asyncio.iscoroutinefunction(HANDLERS["skskills_run_tool"])