@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,360 @@
1
+ """Tests for session_recorder and session_replayer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # SessionRecorder
13
+ # ---------------------------------------------------------------------------
14
+
15
+
16
+ class TestSessionRecorder:
17
+ def test_auto_session_file_created(self, tmp_agent_home: Path) -> None:
18
+ from skcapstone.session_recorder import SessionRecorder
19
+
20
+ rec = SessionRecorder.start_session(tmp_agent_home)
21
+ assert rec.auto_path is not None
22
+ assert rec.auto_path.exists()
23
+ rec.close()
24
+
25
+ def test_record_writes_jsonl_line(self, tmp_agent_home: Path) -> None:
26
+ from skcapstone.session_recorder import SessionRecorder
27
+
28
+ rec = SessionRecorder.start_session(tmp_agent_home)
29
+ rec.record(
30
+ tool="memory_store",
31
+ arguments={"content": "hello", "tags": ["test"]},
32
+ result=[{"type": "text", "text": '{"ok": true}'}],
33
+ duration_ms=12,
34
+ )
35
+ rec.close()
36
+
37
+ lines = rec.auto_path.read_text(encoding="utf-8").strip().splitlines()
38
+ assert len(lines) == 1
39
+ entry = json.loads(lines[0])
40
+ assert entry["tool"] == "memory_store"
41
+ assert entry["duration_ms"] == 12
42
+ assert entry["arguments"]["content"] == "hello"
43
+ assert "ts" in entry
44
+
45
+ def test_explicit_output_file(self, tmp_agent_home: Path, tmp_path: Path) -> None:
46
+ from skcapstone.session_recorder import SessionRecorder
47
+
48
+ out = tmp_path / "explicit.jsonl"
49
+ rec = SessionRecorder.start_session(tmp_agent_home, output_path=out)
50
+ rec.record("coord_status", {}, [{"type": "text", "text": "ok"}], 5)
51
+ rec.close()
52
+
53
+ assert out.exists()
54
+ entry = json.loads(out.read_text(encoding="utf-8").strip())
55
+ assert entry["tool"] == "coord_status"
56
+
57
+ def test_env_var_output_file(
58
+ self, tmp_agent_home: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
59
+ ) -> None:
60
+ from skcapstone.session_recorder import SessionRecorder
61
+
62
+ out = tmp_path / "env_output.jsonl"
63
+ monkeypatch.setenv("SKCAPSTONE_RECORD_FILE", str(out))
64
+ rec = SessionRecorder.start_session(tmp_agent_home)
65
+ rec.record("agent_status", {}, [], 1)
66
+ rec.close()
67
+
68
+ assert out.exists()
69
+ entry = json.loads(out.read_text(encoding="utf-8").strip())
70
+ assert entry["tool"] == "agent_status"
71
+
72
+ def test_auto_rotate_keeps_last_five(self, tmp_agent_home: Path) -> None:
73
+ from skcapstone.session_recorder import SessionRecorder, list_sessions
74
+
75
+ # Create 7 sessions
76
+ for _ in range(7):
77
+ rec = SessionRecorder.start_session(tmp_agent_home)
78
+ rec.record("agent_status", {}, [], 1)
79
+ rec.close()
80
+
81
+ remaining = list_sessions(tmp_agent_home)
82
+ assert len(remaining) == 5
83
+
84
+ def test_count_tracks_records(self, tmp_agent_home: Path) -> None:
85
+ from skcapstone.session_recorder import SessionRecorder
86
+
87
+ rec = SessionRecorder.start_session(tmp_agent_home)
88
+ assert rec.count == 0
89
+ for i in range(3):
90
+ rec.record(f"tool_{i}", {}, [], i)
91
+ assert rec.count == 3
92
+ rec.close()
93
+
94
+ def test_multiple_records_multiple_lines(self, tmp_agent_home: Path) -> None:
95
+ from skcapstone.session_recorder import SessionRecorder, load_session
96
+
97
+ rec = SessionRecorder.start_session(tmp_agent_home)
98
+ tools = ["memory_store", "coord_status", "agent_status"]
99
+ for t in tools:
100
+ rec.record(t, {}, [], 1)
101
+ rec.close()
102
+
103
+ entries = load_session(rec.auto_path)
104
+ assert [e["tool"] for e in entries] == tools
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # load_session / list_sessions helpers
109
+ # ---------------------------------------------------------------------------
110
+
111
+
112
+ class TestSessionHelpers:
113
+ def test_load_session_empty_file(self, tmp_path: Path) -> None:
114
+ from skcapstone.session_recorder import load_session
115
+
116
+ f = tmp_path / "empty.jsonl"
117
+ f.write_text("")
118
+ assert load_session(f) == []
119
+
120
+ def test_load_session_skips_bad_lines(self, tmp_path: Path) -> None:
121
+ from skcapstone.session_recorder import load_session
122
+
123
+ f = tmp_path / "bad.jsonl"
124
+ f.write_text(
125
+ '{"tool": "ok", "arguments": {}, "result": [], "duration_ms": 1, "ts": "x"}\n'
126
+ "not-json\n"
127
+ '{"tool": "ok2", "arguments": {}, "result": [], "duration_ms": 2, "ts": "y"}\n'
128
+ )
129
+ entries = load_session(f)
130
+ assert len(entries) == 2
131
+ assert entries[0]["tool"] == "ok"
132
+ assert entries[1]["tool"] == "ok2"
133
+
134
+ def test_list_sessions_newest_first(self, tmp_agent_home: Path) -> None:
135
+ import time
136
+ from skcapstone.session_recorder import SessionRecorder, list_sessions
137
+
138
+ recs = []
139
+ for i in range(3):
140
+ rec = SessionRecorder.start_session(tmp_agent_home)
141
+ rec.record("t", {}, [], i)
142
+ rec.close()
143
+ recs.append(rec.auto_path)
144
+ time.sleep(0.01) # ensure distinct mtime
145
+
146
+ listed = list_sessions(tmp_agent_home)
147
+ assert len(listed) == 3
148
+ # newest first
149
+ assert listed[0].stat().st_mtime >= listed[1].stat().st_mtime
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # SessionReplayer — dry-run
154
+ # ---------------------------------------------------------------------------
155
+
156
+
157
+ class TestSessionReplayerDryRun:
158
+ def _write_session(self, path: Path, entries: list[dict]) -> None:
159
+ with path.open("w") as fh:
160
+ for e in entries:
161
+ fh.write(json.dumps(e) + "\n")
162
+
163
+ def test_dry_run_yields_results(self, tmp_path: Path) -> None:
164
+ from skcapstone.session_replayer import SessionReplayer
165
+
166
+ f = tmp_path / "s.jsonl"
167
+ self._write_session(f, [
168
+ {"ts": "t", "tool": "agent_status", "arguments": {}, "result": [], "duration_ms": 5},
169
+ {"ts": "t", "tool": "coord_status", "arguments": {}, "result": [], "duration_ms": 3},
170
+ ])
171
+
172
+ replayer = SessionReplayer(f, dry_run=True)
173
+ results = list(replayer.replay())
174
+ assert len(results) == 2
175
+ assert results[0].tool == "agent_status"
176
+ assert results[1].tool == "coord_status"
177
+
178
+ def test_dry_run_no_execution(self, tmp_path: Path) -> None:
179
+ from skcapstone.session_replayer import SessionReplayer
180
+
181
+ f = tmp_path / "s.jsonl"
182
+ self._write_session(f, [
183
+ {"ts": "t", "tool": "agent_status", "arguments": {}, "result": [], "duration_ms": 5},
184
+ ])
185
+
186
+ replayer = SessionReplayer(f, dry_run=True)
187
+ results = list(replayer.replay())
188
+ r = results[0]
189
+ assert r.replayed_result is None
190
+ assert r.match is None
191
+ assert r.duration_ms == 0
192
+ assert r.error is None
193
+
194
+ def test_dry_run_empty_file(self, tmp_path: Path) -> None:
195
+ from skcapstone.session_replayer import SessionReplayer
196
+
197
+ f = tmp_path / "empty.jsonl"
198
+ f.write_text("")
199
+
200
+ results = list(SessionReplayer(f, dry_run=True).replay())
201
+ assert results == []
202
+
203
+ def test_dry_run_arguments_preserved(self, tmp_path: Path) -> None:
204
+ from skcapstone.session_replayer import SessionReplayer
205
+
206
+ args = {"content": "test memory", "tags": ["debug"]}
207
+ f = tmp_path / "s.jsonl"
208
+ self._write_session(f, [
209
+ {"ts": "t", "tool": "memory_store", "arguments": args,
210
+ "result": [{"type": "text", "text": "{}"}], "duration_ms": 10},
211
+ ])
212
+
213
+ results = list(SessionReplayer(f, dry_run=True).replay())
214
+ assert results[0].arguments == args
215
+ assert results[0].recorded_result == [{"type": "text", "text": "{}"}]
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # MockMCPServer
220
+ # ---------------------------------------------------------------------------
221
+
222
+
223
+ class TestMockMCPServer:
224
+ def test_call_returns_matching_recorded_result(self, tmp_path: Path) -> None:
225
+ from skcapstone.session_replayer import MockMCPServer
226
+
227
+ f = tmp_path / "s.jsonl"
228
+ result_data = [{"type": "text", "text": '{"status": "ok"}'}]
229
+ f.write_text(
230
+ json.dumps({
231
+ "ts": "t", "tool": "agent_status", "arguments": {},
232
+ "result": result_data, "duration_ms": 5,
233
+ }) + "\n"
234
+ )
235
+
236
+ mock = MockMCPServer(f)
237
+ result = mock.call("agent_status", {})
238
+ assert result == result_data
239
+
240
+ def test_call_returns_none_for_unknown_tool(self, tmp_path: Path) -> None:
241
+ from skcapstone.session_replayer import MockMCPServer
242
+
243
+ f = tmp_path / "s.jsonl"
244
+ f.write_text(
245
+ json.dumps({
246
+ "ts": "t", "tool": "memory_store", "arguments": {},
247
+ "result": [], "duration_ms": 1,
248
+ }) + "\n"
249
+ )
250
+
251
+ mock = MockMCPServer(f)
252
+ result = mock.call("no_such_tool", {})
253
+ assert result is None
254
+
255
+ def test_sequential_calls_advance_index(self, tmp_path: Path) -> None:
256
+ from skcapstone.session_replayer import MockMCPServer
257
+
258
+ f = tmp_path / "s.jsonl"
259
+ lines = [
260
+ {"ts": "t", "tool": "tool_a", "arguments": {}, "result": [{"t": "a"}], "duration_ms": 1},
261
+ {"ts": "t", "tool": "tool_b", "arguments": {}, "result": [{"t": "b"}], "duration_ms": 2},
262
+ ]
263
+ f.write_text("\n".join(json.dumps(l) for l in lines) + "\n")
264
+
265
+ mock = MockMCPServer(f)
266
+ r1 = mock.call("tool_a", {})
267
+ r2 = mock.call("tool_b", {})
268
+ assert r1 == [{"t": "a"}]
269
+ assert r2 == [{"t": "b"}]
270
+
271
+
272
+ # ---------------------------------------------------------------------------
273
+ # CLI smoke tests
274
+ # ---------------------------------------------------------------------------
275
+
276
+
277
+ class TestCLIRecordCommands:
278
+ def test_sessions_list_empty(
279
+ self, tmp_agent_home: Path, monkeypatch: pytest.MonkeyPatch
280
+ ) -> None:
281
+ from click.testing import CliRunner
282
+ from skcapstone.cli import main
283
+
284
+ monkeypatch.setenv("SKCAPSTONE_ROOT", str(tmp_agent_home.parent))
285
+ runner = CliRunner()
286
+ result = runner.invoke(main, ["sessions", "list", "--home", str(tmp_agent_home)])
287
+ assert result.exit_code == 0
288
+ assert "No sessions" in result.output
289
+
290
+ def test_sessions_list_with_sessions(
291
+ self, tmp_agent_home: Path, monkeypatch: pytest.MonkeyPatch
292
+ ) -> None:
293
+ from click.testing import CliRunner
294
+ from skcapstone.cli import main
295
+ from skcapstone.session_recorder import SessionRecorder
296
+
297
+ rec = SessionRecorder.start_session(tmp_agent_home)
298
+ rec.record("agent_status", {}, [], 5)
299
+ rec.close()
300
+
301
+ runner = CliRunner()
302
+ result = runner.invoke(main, ["sessions", "list", "--home", str(tmp_agent_home)])
303
+ assert result.exit_code == 0
304
+ assert "session-" in result.output
305
+
306
+ def test_replay_dry_run_cli(
307
+ self, tmp_agent_home: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
308
+ ) -> None:
309
+ import json as _json
310
+ from click.testing import CliRunner
311
+ from skcapstone.cli import main
312
+
313
+ f = tmp_path / "test_session.jsonl"
314
+ f.write_text(
315
+ _json.dumps({
316
+ "ts": "2026-01-01T00:00:00+00:00",
317
+ "tool": "agent_status",
318
+ "arguments": {},
319
+ "result": [],
320
+ "duration_ms": 3,
321
+ }) + "\n"
322
+ )
323
+
324
+ runner = CliRunner()
325
+ result = runner.invoke(
326
+ main,
327
+ ["replay", str(f), "--dry-run", "--home", str(tmp_agent_home)],
328
+ )
329
+ assert result.exit_code == 0
330
+ assert "agent_status" in result.output
331
+ assert "SKIP" in result.output or "dry" in result.output.lower()
332
+
333
+ def test_replay_json_format(
334
+ self, tmp_agent_home: Path, tmp_path: Path
335
+ ) -> None:
336
+ import json as _json
337
+ from click.testing import CliRunner
338
+ from skcapstone.cli import main
339
+
340
+ f = tmp_path / "test_session.jsonl"
341
+ f.write_text(
342
+ _json.dumps({
343
+ "ts": "2026-01-01T00:00:00+00:00",
344
+ "tool": "coord_status",
345
+ "arguments": {},
346
+ "result": [{"type": "text", "text": "{}"}],
347
+ "duration_ms": 7,
348
+ }) + "\n"
349
+ )
350
+
351
+ runner = CliRunner()
352
+ result = runner.invoke(
353
+ main,
354
+ ["replay", str(f), "--dry-run", "--format", "json",
355
+ "--home", str(tmp_agent_home)],
356
+ )
357
+ assert result.exit_code == 0
358
+ rows = _json.loads(result.output)
359
+ assert len(rows) == 1
360
+ assert rows[0]["tool"] == "coord_status"
@@ -0,0 +1,235 @@
1
+ """Tests for the session skills bridge — wiring SKSkills into agent runtime sessions."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from textwrap import dedent
7
+ from typing import Dict
8
+ from unittest.mock import patch
9
+
10
+ import pytest
11
+
12
+
13
+ @pytest.fixture
14
+ def skskills_home(tmp_path: Path) -> Path:
15
+ """Set up a mock SKSkills home with installed skills."""
16
+ home = tmp_path / "skskills"
17
+ installed = home / "installed"
18
+ agents = home / "agents"
19
+
20
+ # Global skill
21
+ global_skill = installed / "syncthing-setup"
22
+ global_skill.mkdir(parents=True)
23
+ (global_skill / "skill.yaml").write_text(dedent("""\
24
+ name: syncthing-setup
25
+ version: "1.0.0"
26
+ description: Auto-configure Syncthing
27
+ author:
28
+ name: tester
29
+ """))
30
+
31
+ # Agent-specific skill
32
+ agent_skill = agents / "jarvis" / "code-review"
33
+ agent_skill.mkdir(parents=True)
34
+ (agent_skill / "skill.yaml").write_text(dedent("""\
35
+ name: code-review
36
+ version: "0.1.0"
37
+ description: Code review skill
38
+ author:
39
+ name: tester
40
+ """))
41
+
42
+ return home
43
+
44
+
45
+ @pytest.fixture
46
+ def repo_root(tmp_path: Path) -> Path:
47
+ """Set up a mock repo root."""
48
+ root = tmp_path / "repo"
49
+ root.mkdir(parents=True)
50
+ return root
51
+
52
+
53
+ class TestResolveSkillPaths:
54
+ """Test skill resolution with SKSkills registry integration."""
55
+
56
+ def test_resolve_from_skskills_global(self, skskills_home: Path):
57
+ """Should resolve skill from global SKSkills registry."""
58
+ from skcapstone.session_skills import resolve_skill_paths_with_skskills
59
+
60
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
61
+ result = resolve_skill_paths_with_skskills(["syncthing-setup"])
62
+ assert len(result) == 1
63
+ assert "syncthing-setup" in result[0]
64
+ assert Path(result[0]).exists()
65
+
66
+ def test_resolve_from_skskills_agent(self, skskills_home: Path):
67
+ """Should resolve agent-specific skills first."""
68
+ from skcapstone.session_skills import resolve_skill_paths_with_skskills
69
+
70
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
71
+ result = resolve_skill_paths_with_skskills(
72
+ ["code-review"], agent="jarvis"
73
+ )
74
+ assert len(result) == 1
75
+ assert "jarvis" in result[0]
76
+ assert "code-review" in result[0]
77
+
78
+ def test_resolve_passthrough_when_not_in_skskills(self, skskills_home: Path, repo_root: Path):
79
+ """Skills not in SKSkills should pass through as-is."""
80
+ from skcapstone.session_skills import resolve_skill_paths_with_skskills
81
+
82
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
83
+ result = resolve_skill_paths_with_skskills(
84
+ ["legacy"], repo_root=repo_root
85
+ )
86
+ assert result == ["legacy"]
87
+
88
+ def test_resolve_passthrough_for_unknown(self, skskills_home: Path):
89
+ """Unknown skills should pass through as-is."""
90
+ from skcapstone.session_skills import resolve_skill_paths_with_skskills
91
+
92
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
93
+ result = resolve_skill_paths_with_skskills(["unknown-skill"])
94
+ assert result == ["unknown-skill"]
95
+
96
+ def test_resolve_absolute_path(self, skskills_home: Path, tmp_path: Path):
97
+ """Absolute paths should be used as-is if they exist."""
98
+ from skcapstone.session_skills import resolve_skill_paths_with_skskills
99
+
100
+ abs_path = tmp_path / "my-skill"
101
+ abs_path.mkdir()
102
+
103
+ with patch.dict(os.environ, {"SKSKILLS_HOME": str(skskills_home)}):
104
+ result = resolve_skill_paths_with_skskills([str(abs_path)])
105
+ assert result == [str(abs_path)]
106
+
107
+
108
+ class TestPrepareSessionSkills:
109
+ """Test session skill preparation."""
110
+
111
+ def test_prepare_with_skskills_dir(self, skskills_home: Path, tmp_path: Path):
112
+ """Should load skills that have skill.yaml."""
113
+ from skcapstone.session_skills import prepare_session_skills
114
+
115
+ work_dir = tmp_path / "agent-work"
116
+ work_dir.mkdir()
117
+
118
+ skill_path = str(skskills_home / "installed" / "syncthing-setup")
119
+ result = prepare_session_skills("test-agent", [skill_path], work_dir)
120
+
121
+ assert result["skills_loaded"] == 1
122
+ assert "syncthing-setup" in result["skill_names"]
123
+
124
+ def test_prepare_skips_non_skskills(self, tmp_path: Path):
125
+ """Should skip paths that don't have skill.yaml."""
126
+ from skcapstone.session_skills import prepare_session_skills
127
+
128
+ work_dir = tmp_path / "agent-work"
129
+ work_dir.mkdir()
130
+ non_skill = tmp_path / "not-a-skill"
131
+ non_skill.mkdir()
132
+
133
+ result = prepare_session_skills("test-agent", [str(non_skill)], work_dir)
134
+ assert result["skills_loaded"] == 0
135
+
136
+ def test_prepare_writes_mcp_config(self, skskills_home: Path, tmp_path: Path):
137
+ """Should write MCP config when skills are loaded."""
138
+ from skcapstone.session_skills import prepare_session_skills
139
+
140
+ work_dir = tmp_path / "agent-work"
141
+ work_dir.mkdir()
142
+
143
+ skill_path = str(skskills_home / "installed" / "syncthing-setup")
144
+ result = prepare_session_skills("test-agent", [skill_path], work_dir)
145
+
146
+ assert result["mcp_config_path"] is not None
147
+ config_path = Path(result["mcp_config_path"])
148
+ assert config_path.exists()
149
+ config = json.loads(config_path.read_text())
150
+ assert "mcpServers" in config
151
+ assert "skskills" in config["mcpServers"]
152
+
153
+
154
+ class TestEnrichConfigs:
155
+ """Test session and crush config enrichment."""
156
+
157
+ def test_enrich_session_config(self):
158
+ """Should add skskills metadata to session config."""
159
+ from skcapstone.session_skills import enrich_session_config
160
+
161
+ session_config: Dict = {"agent_name": "test", "skills": []}
162
+ skill_result = {
163
+ "skills_loaded": 2,
164
+ "skill_names": ["skill-a", "skill-b"],
165
+ "tools_available": ["skill-a.tool1", "skill-b.tool2"],
166
+ "mcp_config_path": "/tmp/test.json",
167
+ }
168
+
169
+ enriched = enrich_session_config(session_config, skill_result)
170
+ assert "skskills" in enriched
171
+ assert enriched["skskills"]["loaded"] == 2
172
+ assert enriched["skskills"]["tools_available"] == ["skill-a.tool1", "skill-b.tool2"]
173
+
174
+ def test_enrich_session_config_no_skills(self):
175
+ """Should not add skskills section when no skills loaded."""
176
+ from skcapstone.session_skills import enrich_session_config
177
+
178
+ session_config: Dict = {"agent_name": "test"}
179
+ skill_result = {"skills_loaded": 0, "skill_names": [], "tools_available": []}
180
+
181
+ enriched = enrich_session_config(session_config, skill_result)
182
+ assert "skskills" not in enriched
183
+
184
+ def test_enrich_crush_config(self):
185
+ """Should add skskills MCP server to crush config."""
186
+ from skcapstone.session_skills import enrich_crush_config
187
+
188
+ crush_config: Dict = {
189
+ "options": {},
190
+ "permissions": {"allowed_tools": ["view", "edit"]},
191
+ }
192
+ skill_result = {
193
+ "skills_loaded": 1,
194
+ "tools_available": ["my-skill.deploy"],
195
+ }
196
+
197
+ enriched = enrich_crush_config(crush_config, skill_result)
198
+ assert "skskills" in enriched["mcpServers"]
199
+ assert "mcp_skskills_my_skill_deploy" in enriched["permissions"]["allowed_tools"]
200
+
201
+ def test_enrich_crush_config_no_skills(self):
202
+ """Should not modify crush config when no skills loaded."""
203
+ from skcapstone.session_skills import enrich_crush_config
204
+
205
+ crush_config: Dict = {"options": {}}
206
+ skill_result = {"skills_loaded": 0, "tools_available": []}
207
+
208
+ enriched = enrich_crush_config(crush_config, skill_result)
209
+ assert "mcpServers" not in enriched
210
+
211
+
212
+ class TestCleanup:
213
+ """Test session skills cleanup."""
214
+
215
+ def test_cleanup_removes_mcp_config(self, tmp_path: Path):
216
+ """Should remove the MCP config file."""
217
+ from skcapstone.session_skills import cleanup_session_skills
218
+
219
+ work_dir = tmp_path / "agent-work"
220
+ work_dir.mkdir()
221
+ mcp_config = work_dir / "skskills_mcp.json"
222
+ mcp_config.write_text("{}")
223
+
224
+ cleanup_session_skills("test-agent", work_dir)
225
+ assert not mcp_config.exists()
226
+
227
+ def test_cleanup_handles_missing_config(self, tmp_path: Path):
228
+ """Should not fail when MCP config doesn't exist."""
229
+ from skcapstone.session_skills import cleanup_session_skills
230
+
231
+ work_dir = tmp_path / "agent-work"
232
+ work_dir.mkdir()
233
+
234
+ # Should not raise
235
+ cleanup_session_skills("test-agent", work_dir)