@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,391 @@
1
+ """Tests for ConversationStore and chat history CLI.
2
+
3
+ Covers:
4
+ - ConversationStore.append / get_last / load / all_peers / clear
5
+ - Path-traversal sanitization
6
+ - ConsciousnessLoop integrates ConversationStore (last-10 context)
7
+ - `skcapstone chat history PEER` CLI command
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from pathlib import Path
14
+ from unittest.mock import MagicMock, patch
15
+
16
+ import pytest
17
+ from click.testing import CliRunner
18
+
19
+ from skcapstone.conversation_store import ConversationStore, _sanitize_peer_name
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Fixtures
24
+ # ---------------------------------------------------------------------------
25
+
26
+
27
+ @pytest.fixture
28
+ def store(tmp_path):
29
+ """ConversationStore rooted in a temp directory."""
30
+ home = tmp_path / ".skcapstone"
31
+ home.mkdir()
32
+ return ConversationStore(home)
33
+
34
+
35
+ @pytest.fixture
36
+ def populated_store(store, tmp_path):
37
+ """Store with two peers pre-seeded."""
38
+ store.append("alice", "user", "hello alice")
39
+ store.append("alice", "assistant", "hi there!")
40
+ store.append("bob", "user", "hey bob")
41
+ return store
42
+
43
+
44
+ @pytest.fixture
45
+ def agent_home(tmp_path):
46
+ """Minimal agent home for CLI tests."""
47
+ home = tmp_path / ".skcapstone"
48
+ (home / "identity").mkdir(parents=True)
49
+ (home / "config").mkdir(parents=True)
50
+ identity = {"name": "TestAgent", "fingerprint": "AABB1234", "capauth_managed": False}
51
+ (home / "identity" / "identity.json").write_text(json.dumps(identity))
52
+ (home / "manifest.json").write_text(json.dumps({"name": "TestAgent", "version": "0.1.0"}))
53
+ import yaml
54
+ (home / "config" / "config.yaml").write_text(yaml.dump({"agent_name": "TestAgent"}))
55
+ return home
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # _sanitize_peer_name
60
+ # ---------------------------------------------------------------------------
61
+
62
+
63
+ class TestSanitizePeerName:
64
+ def test_normal_name_unchanged(self):
65
+ assert _sanitize_peer_name("lumina") == "lumina"
66
+
67
+ def test_strips_path_separators(self):
68
+ result = _sanitize_peer_name("../../etc/passwd")
69
+ assert "/" not in result
70
+ assert ".." not in result
71
+
72
+ def test_strips_null_bytes(self):
73
+ assert "\x00" not in _sanitize_peer_name("evil\x00peer")
74
+
75
+ def test_empty_returns_unknown(self):
76
+ assert _sanitize_peer_name("") == "unknown"
77
+
78
+ def test_none_returns_unknown(self):
79
+ assert _sanitize_peer_name(None) == "unknown" # type: ignore[arg-type]
80
+
81
+ def test_max_length_64(self):
82
+ assert len(_sanitize_peer_name("a" * 100)) == 64
83
+
84
+ def test_allowed_chars_preserved(self):
85
+ assert _sanitize_peer_name("user@domain.io") == "user@domain.io"
86
+ assert _sanitize_peer_name("my-peer_01") == "my-peer_01"
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # ConversationStore — basic operations
91
+ # ---------------------------------------------------------------------------
92
+
93
+
94
+ class TestConversationStoreAppend:
95
+ def test_append_creates_file(self, store, tmp_path):
96
+ store.append("alice", "user", "hello")
97
+ assert (tmp_path / ".skcapstone" / "conversations" / "alice.json").exists()
98
+
99
+ def test_append_returns_message_dict(self, store):
100
+ msg = store.append("alice", "user", "hello")
101
+ assert msg["role"] == "user"
102
+ assert msg["content"] == "hello"
103
+ assert "timestamp" in msg
104
+
105
+ def test_append_multiple_messages(self, store):
106
+ store.append("alice", "user", "msg1")
107
+ store.append("alice", "assistant", "msg2")
108
+ history = store.load("alice")
109
+ assert len(history) == 2
110
+ assert history[0]["content"] == "msg1"
111
+ assert history[1]["content"] == "msg2"
112
+
113
+ def test_append_stores_thread_id(self, store):
114
+ store.append("alice", "user", "threaded", thread_id="t-01")
115
+ history = store.load("alice")
116
+ assert history[0].get("thread_id") == "t-01"
117
+
118
+ def test_append_stores_in_reply_to(self, store):
119
+ store.append("alice", "assistant", "reply", in_reply_to="msg-123")
120
+ history = store.load("alice")
121
+ assert history[0].get("in_reply_to") == "msg-123"
122
+
123
+ def test_append_path_traversal_sanitized(self, store, tmp_path):
124
+ """Malicious peer name is sanitized; no file written outside conversations/."""
125
+ store.append("../../evil", "user", "attack")
126
+ conv_dir = tmp_path / ".skcapstone" / "conversations"
127
+ files = list(conv_dir.glob("*.json"))
128
+ # The sanitized name must not contain path separators
129
+ for f in files:
130
+ assert "/" not in f.name
131
+ assert ".." not in f.name
132
+
133
+
134
+ class TestConversationStoreGetLast:
135
+ def test_returns_last_n(self, store):
136
+ for i in range(15):
137
+ store.append("alice", "user", f"msg{i}")
138
+ last5 = store.get_last("alice", 5)
139
+ assert len(last5) == 5
140
+ assert last5[-1]["content"] == "msg14"
141
+
142
+ def test_returns_all_when_n_larger(self, store):
143
+ store.append("alice", "user", "only one")
144
+ result = store.get_last("alice", 10)
145
+ assert len(result) == 1
146
+
147
+ def test_empty_for_unknown_peer(self, store):
148
+ assert store.get_last("nobody", 10) == []
149
+
150
+ def test_n_zero_returns_empty(self, store):
151
+ store.append("alice", "user", "hi")
152
+ assert store.get_last("alice", 0) == []
153
+
154
+ def test_default_n_is_10(self, store):
155
+ for i in range(20):
156
+ store.append("alice", "user", f"msg{i}")
157
+ assert len(store.get_last("alice")) == 10
158
+
159
+
160
+ class TestConversationStoreAllPeers:
161
+ def test_empty_when_no_dir(self, tmp_path):
162
+ store = ConversationStore(tmp_path / "empty")
163
+ assert store.all_peers() == []
164
+
165
+ def test_lists_all_peers(self, populated_store):
166
+ peers = populated_store.all_peers()
167
+ assert "alice" in peers
168
+ assert "bob" in peers
169
+
170
+ def test_sorted_alphabetically(self, store):
171
+ store.append("zara", "user", "hi")
172
+ store.append("anna", "user", "hi")
173
+ peers = store.all_peers()
174
+ assert peers == sorted(peers)
175
+
176
+
177
+ class TestConversationStoreClear:
178
+ def test_clear_removes_file(self, populated_store, tmp_path):
179
+ populated_store.clear("bob")
180
+ assert not (tmp_path / ".skcapstone" / "conversations" / "bob.json").exists()
181
+
182
+ def test_clear_returns_true_when_existed(self, populated_store):
183
+ assert populated_store.clear("alice") is True
184
+
185
+ def test_clear_returns_false_when_missing(self, store):
186
+ assert store.clear("nobody") is False
187
+
188
+ def test_clear_does_not_affect_other_peers(self, populated_store):
189
+ populated_store.clear("bob")
190
+ assert populated_store.load("alice") != []
191
+
192
+
193
+ class TestConversationStoreFormatForPrompt:
194
+ def test_returns_empty_for_unknown_peer(self, store):
195
+ assert store.format_for_prompt("nobody") == ""
196
+
197
+ def test_includes_peer_name_header(self, store):
198
+ store.append("alice", "user", "hi")
199
+ result = store.format_for_prompt("alice")
200
+ assert "alice" in result
201
+
202
+ def test_includes_role_and_content(self, store):
203
+ store.append("alice", "user", "how are you?")
204
+ store.append("alice", "assistant", "doing great!")
205
+ result = store.format_for_prompt("alice")
206
+ assert "[user]" in result
207
+ assert "[assistant]" in result
208
+ assert "how are you?" in result
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # ConsciousnessLoop integration — uses ConversationStore for context
213
+ # ---------------------------------------------------------------------------
214
+
215
+
216
+ class TestConsciousnessLoopUsesConversationStore:
217
+ """Verify ConsciousnessLoop wires ConversationStore into SystemPromptBuilder."""
218
+
219
+ def test_loop_creates_conv_store(self, tmp_path):
220
+ from skcapstone.consciousness_loop import ConsciousnessConfig, ConsciousnessLoop
221
+ from skcapstone.conversation_store import ConversationStore
222
+
223
+ home = tmp_path / ".skcapstone"
224
+ home.mkdir()
225
+ config = ConsciousnessConfig()
226
+ loop = ConsciousnessLoop(config, home=home, shared_root=home)
227
+ assert isinstance(loop._conv_store, ConversationStore)
228
+
229
+ def test_prompt_builder_has_conv_store(self, tmp_path):
230
+ from skcapstone.consciousness_loop import ConsciousnessConfig, ConsciousnessLoop
231
+
232
+ home = tmp_path / ".skcapstone"
233
+ home.mkdir()
234
+ config = ConsciousnessConfig()
235
+ loop = ConsciousnessLoop(config, home=home, shared_root=home)
236
+ assert loop._prompt_builder._conv_store is loop._conv_store
237
+
238
+ def test_add_to_history_writes_via_conv_store(self, tmp_path):
239
+ """add_to_history via ConversationStore creates the JSON file."""
240
+ from skcapstone.consciousness_loop import SystemPromptBuilder
241
+ from skcapstone.conversation_store import ConversationStore
242
+
243
+ home = tmp_path / ".skcapstone"
244
+ home.mkdir()
245
+ store = ConversationStore(home)
246
+ builder = SystemPromptBuilder(home=home, conv_store=store)
247
+
248
+ builder.add_to_history("testpeer", "user", "hello world")
249
+
250
+ history = store.load("testpeer")
251
+ assert len(history) == 1
252
+ assert history[0]["role"] == "user"
253
+ assert history[0]["content"] == "hello world"
254
+
255
+ def test_get_peer_history_reads_from_store(self, tmp_path):
256
+ """_get_peer_history returns content written directly to the store."""
257
+ from skcapstone.consciousness_loop import SystemPromptBuilder
258
+ from skcapstone.conversation_store import ConversationStore
259
+
260
+ home = tmp_path / ".skcapstone"
261
+ home.mkdir()
262
+ store = ConversationStore(home)
263
+ # Write directly to the store (bypassing prompt builder)
264
+ store.append("opus", "user", "direct write")
265
+ store.append("opus", "assistant", "got it")
266
+
267
+ # Build prompt builder with the same store
268
+ builder = SystemPromptBuilder(home=home, conv_store=store)
269
+ history_text = builder._get_peer_history("opus")
270
+
271
+ assert "opus" in history_text
272
+ assert "direct write" in history_text
273
+ assert "got it" in history_text
274
+
275
+ def test_loads_last_10_messages_for_context(self, tmp_path):
276
+ """Context includes at most max_history_messages (10) entries."""
277
+ from skcapstone.consciousness_loop import SystemPromptBuilder
278
+ from skcapstone.conversation_store import ConversationStore
279
+
280
+ home = tmp_path / ".skcapstone"
281
+ home.mkdir()
282
+ store = ConversationStore(home)
283
+ for i in range(20):
284
+ store.append("lumina", "user", f"msg{i}")
285
+
286
+ builder = SystemPromptBuilder(home=home, conv_store=store, max_history_messages=10)
287
+ history_text = builder._get_peer_history("lumina")
288
+
289
+ # Only messages 10–19 should appear
290
+ assert "msg19" in history_text
291
+ assert "msg0" not in history_text
292
+
293
+
294
+ # ---------------------------------------------------------------------------
295
+ # `skcapstone chat history PEER` CLI
296
+ # ---------------------------------------------------------------------------
297
+
298
+
299
+ class TestChatHistoryCLI:
300
+ """Tests for `skcapstone chat history PEER`."""
301
+
302
+ @patch("skcapstone.cli.chat.get_runtime")
303
+ def test_history_help(self, _mock_rt):
304
+ from skcapstone.cli import main
305
+ runner = CliRunner()
306
+ result = runner.invoke(main, ["chat", "history", "--help"])
307
+ assert result.exit_code == 0
308
+ assert "PEER" in result.output
309
+
310
+ @patch("skcapstone.cli.chat.get_runtime")
311
+ def test_history_empty(self, _mock_rt, agent_home):
312
+ """No conversation → 'No conversation history' message."""
313
+ from skcapstone.cli import main
314
+ runner = CliRunner()
315
+ result = runner.invoke(
316
+ main, ["chat", "history", "nobody", "--home", str(agent_home)]
317
+ )
318
+ assert result.exit_code == 0
319
+ assert "No conversation history" in result.output
320
+
321
+ @patch("skcapstone.cli.chat.get_runtime")
322
+ def test_history_shows_messages(self, _mock_rt, agent_home):
323
+ """Messages written to store appear in history output."""
324
+ from skcapstone.cli import main
325
+ from skcapstone.conversation_store import ConversationStore
326
+
327
+ store = ConversationStore(agent_home)
328
+ store.append("lumina", "user", "Hello Lumina!")
329
+ store.append("lumina", "assistant", "Hello! How can I help?")
330
+
331
+ runner = CliRunner()
332
+ result = runner.invoke(
333
+ main, ["chat", "history", "lumina", "--home", str(agent_home)]
334
+ )
335
+ assert result.exit_code == 0
336
+ assert "Hello Lumina!" in result.output
337
+ assert "Hello! How can I help?" in result.output
338
+
339
+ @patch("skcapstone.cli.chat.get_runtime")
340
+ def test_history_limit(self, _mock_rt, agent_home):
341
+ """--limit N restricts output to last N messages."""
342
+ from skcapstone.cli import main
343
+ from skcapstone.conversation_store import ConversationStore
344
+
345
+ store = ConversationStore(agent_home)
346
+ for i in range(10):
347
+ store.append("lumina", "user", f"msg{i}")
348
+
349
+ runner = CliRunner()
350
+ result = runner.invoke(
351
+ main, ["chat", "history", "lumina", "--limit", "3", "--home", str(agent_home)]
352
+ )
353
+ assert result.exit_code == 0
354
+ assert "msg9" in result.output
355
+ assert "msg0" not in result.output
356
+
357
+ @patch("skcapstone.cli.chat.get_runtime")
358
+ def test_history_json_output(self, _mock_rt, agent_home):
359
+ """--json flag outputs valid JSON list."""
360
+ from skcapstone.cli import main
361
+ from skcapstone.conversation_store import ConversationStore
362
+
363
+ store = ConversationStore(agent_home)
364
+ store.append("opus", "user", "test message")
365
+
366
+ runner = CliRunner()
367
+ result = runner.invoke(
368
+ main, ["chat", "history", "opus", "--json", "--home", str(agent_home)]
369
+ )
370
+ assert result.exit_code == 0
371
+ data = json.loads(result.output.strip())
372
+ assert isinstance(data, list)
373
+ assert data[0]["content"] == "test message"
374
+
375
+ @patch("skcapstone.cli.chat.get_runtime")
376
+ def test_history_role_labels(self, _mock_rt, agent_home):
377
+ """Both user and assistant roles appear in the formatted output."""
378
+ from skcapstone.cli import main
379
+ from skcapstone.conversation_store import ConversationStore
380
+
381
+ store = ConversationStore(agent_home)
382
+ store.append("jarvis", "user", "status?")
383
+ store.append("jarvis", "assistant", "all systems nominal")
384
+
385
+ runner = CliRunner()
386
+ result = runner.invoke(
387
+ main, ["chat", "history", "jarvis", "--home", str(agent_home)]
388
+ )
389
+ assert result.exit_code == 0
390
+ assert "user" in result.output
391
+ assert "assistant" in result.output
@@ -0,0 +1,302 @@
1
+ """Tests for ConversationSummarizer and the `skcapstone chat summary` CLI.
2
+
3
+ Test coverage:
4
+ - Happy path: summarize() calls LLM and returns a ConversationSummary
5
+ - Empty conversation raises ValueError
6
+ - Summary is persisted to {home}/summaries/{peer}.json
7
+ - load_summary() retrieves stored summary
8
+ - Peer name sanitization (path traversal attempt)
9
+ - CLI: chat summary renders summary text
10
+ - CLI: chat summary --show-stored shows stored summary
11
+ - CLI: chat summary --show-stored with no stored summary shows helpful message
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from pathlib import Path
18
+ from unittest.mock import MagicMock, patch
19
+
20
+ import pytest
21
+ from click.testing import CliRunner
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Fixtures
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ @pytest.fixture
30
+ def agent_home(tmp_path):
31
+ """Minimal agent home with identity manifest."""
32
+ home = tmp_path / ".skcapstone"
33
+ (home / "identity").mkdir(parents=True)
34
+ (home / "config").mkdir(parents=True)
35
+ identity = {"name": "TestAgent", "fingerprint": "AABB1234", "capauth_managed": False}
36
+ (home / "identity" / "identity.json").write_text(json.dumps(identity))
37
+ (home / "manifest.json").write_text(json.dumps({"name": "TestAgent", "version": "0.1.0"}))
38
+ import yaml
39
+ (home / "config" / "config.yaml").write_text(yaml.dump({"agent_name": "TestAgent"}))
40
+ return home
41
+
42
+
43
+ @pytest.fixture
44
+ def agent_home_with_conv(agent_home):
45
+ """Agent home with a lumina conversation file."""
46
+ convs = agent_home / "conversations"
47
+ convs.mkdir(parents=True)
48
+ messages = [
49
+ {"role": "user", "content": "Hello Lumina!", "timestamp": "2026-03-01T10:00:00Z"},
50
+ {"role": "assistant", "content": "Hi! I'm here.", "timestamp": "2026-03-01T10:00:01Z"},
51
+ {"role": "user", "content": "Can you deploy the update?", "timestamp": "2026-03-01T10:01:00Z"},
52
+ {"role": "assistant", "content": "Sure, initiating deployment now.", "timestamp": "2026-03-01T10:01:05Z"},
53
+ ]
54
+ (convs / "lumina.json").write_text(json.dumps(messages))
55
+ return agent_home
56
+
57
+
58
+ def _make_bridge(response: str = "Two agents discussed deployment of the update. The task was agreed upon and initiated."):
59
+ """Return a mock LLMBridge with a canned generate() return value."""
60
+ bridge = MagicMock()
61
+ bridge.generate.return_value = response
62
+ return bridge
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # ConversationSummarizer unit tests
67
+ # ---------------------------------------------------------------------------
68
+
69
+
70
+ class TestConversationSummarizer:
71
+ """Unit tests for ConversationSummarizer."""
72
+
73
+ def test_summarize_happy_path(self, agent_home_with_conv):
74
+ """summarize() calls LLMBridge and returns a ConversationSummary."""
75
+ from skcapstone.conversation_summarizer import ConversationSummarizer
76
+
77
+ bridge = _make_bridge("Lumina and agent discussed deployment. The update was initiated.")
78
+ summarizer = ConversationSummarizer(home=agent_home_with_conv)
79
+ result = summarizer.summarize("lumina", n=20, bridge=bridge)
80
+
81
+ assert result.peer == "lumina"
82
+ assert "deployment" in result.text.lower() or result.text # LLM returned something
83
+ assert result.message_count == 4
84
+ assert result.generated_at # non-empty timestamp
85
+ bridge.generate.assert_called_once()
86
+
87
+ def test_summarize_empty_conversation_raises(self, agent_home):
88
+ """summarize() raises ValueError when there is no conversation history."""
89
+ from skcapstone.conversation_summarizer import ConversationSummarizer
90
+
91
+ summarizer = ConversationSummarizer(home=agent_home)
92
+ with pytest.raises(ValueError, match="No conversation history"):
93
+ summarizer.summarize("nobody", bridge=_make_bridge())
94
+
95
+ def test_summarize_persists_to_disk(self, agent_home_with_conv):
96
+ """summarize() writes the summary JSON to {home}/summaries/{peer}.json."""
97
+ from skcapstone.conversation_summarizer import ConversationSummarizer
98
+
99
+ summarizer = ConversationSummarizer(home=agent_home_with_conv)
100
+ result = summarizer.summarize("lumina", bridge=_make_bridge("Summary text here."))
101
+
102
+ summary_file = agent_home_with_conv / "summaries" / "lumina.json"
103
+ assert summary_file.exists(), "summaries/lumina.json should be created"
104
+
105
+ data = json.loads(summary_file.read_text())
106
+ assert data["peer"] == "lumina"
107
+ assert data["text"] == "Summary text here."
108
+ assert data["message_count"] == 4
109
+
110
+ def test_load_summary_returns_stored(self, agent_home_with_conv):
111
+ """load_summary() retrieves the previously stored summary."""
112
+ from skcapstone.conversation_summarizer import ConversationSummarizer
113
+
114
+ summarizer = ConversationSummarizer(home=agent_home_with_conv)
115
+ summarizer.summarize("lumina", bridge=_make_bridge("Stored summary content."))
116
+
117
+ loaded = summarizer.load_summary("lumina")
118
+ assert loaded is not None
119
+ assert loaded.peer == "lumina"
120
+ assert loaded.text == "Stored summary content."
121
+
122
+ def test_load_summary_returns_none_when_missing(self, agent_home):
123
+ """load_summary() returns None when no summary has been stored yet."""
124
+ from skcapstone.conversation_summarizer import ConversationSummarizer
125
+
126
+ summarizer = ConversationSummarizer(home=agent_home)
127
+ assert summarizer.load_summary("nobody") is None
128
+
129
+ def test_summarize_respects_n_limit(self, agent_home):
130
+ """summarize() only includes the last n messages."""
131
+ from skcapstone.conversation_summarizer import ConversationSummarizer
132
+
133
+ convs = agent_home / "conversations"
134
+ convs.mkdir(parents=True)
135
+ messages = [
136
+ {"role": "user", "content": f"Message {i}", "timestamp": "2026-03-01T10:00:00Z"}
137
+ for i in range(30)
138
+ ]
139
+ (convs / "peer.json").write_text(json.dumps(messages))
140
+
141
+ bridge = _make_bridge("Summary of last 5.")
142
+ summarizer = ConversationSummarizer(home=agent_home)
143
+ result = summarizer.summarize("peer", n=5, bridge=bridge)
144
+
145
+ assert result.message_count == 5
146
+
147
+ def test_summarize_sanitizes_peer_name(self, agent_home):
148
+ """Path traversal in peer name is stripped, not stored as-is."""
149
+ from skcapstone.conversation_summarizer import ConversationSummarizer
150
+
151
+ convs = agent_home / "conversations"
152
+ convs.mkdir(parents=True)
153
+ messages = [{"role": "user", "content": "hi", "timestamp": "2026-03-01T10:00:00Z"}]
154
+ # The sanitizer will strip path separators; "etcpasswd" will be the key
155
+ (convs / "etcpasswd.json").write_text(json.dumps(messages))
156
+
157
+ summarizer = ConversationSummarizer(home=agent_home)
158
+ result = summarizer.summarize("../../../etc/passwd", bridge=_make_bridge("Safe."))
159
+
160
+ assert result.peer == "etcpasswd"
161
+ summary_file = agent_home / "summaries" / "etcpasswd.json"
162
+ assert summary_file.exists()
163
+
164
+ def test_summarize_llm_error_returns_error_text(self, agent_home_with_conv):
165
+ """If the LLM fails, summarize() stores an error placeholder instead of raising."""
166
+ from skcapstone.conversation_summarizer import ConversationSummarizer
167
+
168
+ bridge = MagicMock()
169
+ bridge.generate.side_effect = RuntimeError("LLM offline")
170
+
171
+ summarizer = ConversationSummarizer(home=agent_home_with_conv)
172
+ result = summarizer.summarize("lumina", bridge=bridge)
173
+
174
+ assert "[Summary unavailable" in result.text
175
+ assert result.message_count == 4
176
+
177
+ def test_summary_to_dict_roundtrip(self):
178
+ """ConversationSummary serializes and deserializes correctly."""
179
+ from skcapstone.conversation_summarizer import ConversationSummary
180
+
181
+ original = ConversationSummary(
182
+ peer="opus",
183
+ text="A concise summary.",
184
+ message_count=10,
185
+ generated_at="2026-03-01T12:00:00+00:00",
186
+ )
187
+ data = original.to_dict()
188
+ restored = ConversationSummary.from_dict(data)
189
+
190
+ assert restored.peer == original.peer
191
+ assert restored.text == original.text
192
+ assert restored.message_count == original.message_count
193
+ assert restored.generated_at == original.generated_at
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # CLI integration tests
198
+ # ---------------------------------------------------------------------------
199
+
200
+
201
+ @pytest.fixture
202
+ def agent_home_cli(agent_home_with_conv):
203
+ """Agent home suitable for CLI tests (has runtime files)."""
204
+ return agent_home_with_conv
205
+
206
+
207
+ class TestChatSummaryCLI:
208
+ """Tests for `skcapstone chat summary`."""
209
+
210
+ def _make_runtime(self, name="TestAgent"):
211
+ rt = MagicMock()
212
+ rt.manifest.name = name
213
+ return rt
214
+
215
+ @patch("skcapstone.cli.chat.get_runtime")
216
+ def test_chat_summary_help(self, _mock_rt):
217
+ """chat summary --help exits cleanly and mentions PEER."""
218
+ from skcapstone.cli import main
219
+ runner = CliRunner()
220
+ result = runner.invoke(main, ["chat", "summary", "--help"])
221
+ assert result.exit_code == 0
222
+ assert "PEER" in result.output
223
+
224
+ @patch("skcapstone.cli.chat.get_runtime")
225
+ @patch("skcapstone.conversation_summarizer.ConversationSummarizer.summarize")
226
+ def test_chat_summary_prints_result(self, mock_summarize, mock_rt, agent_home_cli):
227
+ """chat summary prints the generated summary text."""
228
+ from skcapstone.conversation_summarizer import ConversationSummary
229
+ from skcapstone.cli import main
230
+
231
+ mock_rt.return_value = self._make_runtime()
232
+ mock_summarize.return_value = ConversationSummary(
233
+ peer="lumina",
234
+ text="Two agents talked about deployment. The update was shipped.",
235
+ message_count=4,
236
+ generated_at="2026-03-01T10:00:00+00:00",
237
+ )
238
+
239
+ runner = CliRunner()
240
+ result = runner.invoke(
241
+ main, ["chat", "summary", "lumina", "--home", str(agent_home_cli)]
242
+ )
243
+
244
+ assert result.exit_code == 0
245
+ assert "Two agents talked about deployment" in result.output
246
+
247
+ @patch("skcapstone.cli.chat.get_runtime")
248
+ @patch("skcapstone.conversation_summarizer.ConversationSummarizer.load_summary")
249
+ def test_chat_summary_show_stored(self, mock_load, mock_rt, agent_home_cli):
250
+ """chat summary --show-stored displays previously stored summary."""
251
+ from skcapstone.conversation_summarizer import ConversationSummary
252
+ from skcapstone.cli import main
253
+
254
+ mock_rt.return_value = self._make_runtime()
255
+ mock_load.return_value = ConversationSummary(
256
+ peer="lumina",
257
+ text="Stored summary about prior work.",
258
+ message_count=8,
259
+ generated_at="2026-03-01T09:00:00+00:00",
260
+ )
261
+
262
+ runner = CliRunner()
263
+ result = runner.invoke(
264
+ main, ["chat", "summary", "lumina", "--home", str(agent_home_cli), "--show-stored"]
265
+ )
266
+
267
+ assert result.exit_code == 0
268
+ assert "Stored summary about prior work" in result.output
269
+
270
+ @patch("skcapstone.cli.chat.get_runtime")
271
+ @patch("skcapstone.conversation_summarizer.ConversationSummarizer.load_summary")
272
+ def test_chat_summary_show_stored_missing(self, mock_load, mock_rt, agent_home_cli):
273
+ """chat summary --show-stored with no stored summary shows helpful message."""
274
+ from skcapstone.cli import main
275
+
276
+ mock_rt.return_value = self._make_runtime()
277
+ mock_load.return_value = None
278
+
279
+ runner = CliRunner()
280
+ result = runner.invoke(
281
+ main, ["chat", "summary", "lumina", "--home", str(agent_home_cli), "--show-stored"]
282
+ )
283
+
284
+ assert result.exit_code == 0
285
+ assert "No stored summary" in result.output
286
+
287
+ @patch("skcapstone.cli.chat.get_runtime")
288
+ @patch("skcapstone.conversation_summarizer.ConversationSummarizer.summarize")
289
+ def test_chat_summary_no_history(self, mock_summarize, mock_rt, agent_home):
290
+ """chat summary prints an error message when the peer has no conversation."""
291
+ from skcapstone.cli import main
292
+
293
+ mock_rt.return_value = self._make_runtime()
294
+ mock_summarize.side_effect = ValueError("No conversation history found for peer 'nobody'.")
295
+
296
+ runner = CliRunner()
297
+ result = runner.invoke(
298
+ main, ["chat", "summary", "nobody", "--home", str(agent_home)]
299
+ )
300
+
301
+ assert result.exit_code == 0 # CLI handles the ValueError gracefully
302
+ assert "Error" in result.output or "No conversation" in result.output