@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,394 @@
1
+ """Tests for the agent mood tracker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import threading
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from skcapstone.mood import (
12
+ MoodSnapshot,
13
+ MoodTracker,
14
+ _classify_social,
15
+ _classify_stress,
16
+ _classify_success,
17
+ _compute_summary,
18
+ )
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Axis classifier unit tests
23
+ # ---------------------------------------------------------------------------
24
+
25
+
26
+ class TestClassifySuccess:
27
+ """Unit tests for _classify_success."""
28
+
29
+ def test_high_rate_is_happy(self) -> None:
30
+ """>=90% success rate maps to 'happy'."""
31
+ assert _classify_success(0.95) == "happy"
32
+ assert _classify_success(1.0) == "happy"
33
+ assert _classify_success(0.9) == "happy"
34
+
35
+ def test_moderate_rate_is_content(self) -> None:
36
+ """70–89% maps to 'content'."""
37
+ assert _classify_success(0.80) == "content"
38
+ assert _classify_success(0.70) == "content"
39
+
40
+ def test_borderline_rate_is_neutral(self) -> None:
41
+ """50–69% maps to 'neutral'."""
42
+ assert _classify_success(0.60) == "neutral"
43
+ assert _classify_success(0.50) == "neutral"
44
+
45
+ def test_low_rate_is_frustrated(self) -> None:
46
+ """<50% maps to 'frustrated'."""
47
+ assert _classify_success(0.49) == "frustrated"
48
+ assert _classify_success(0.0) == "frustrated"
49
+
50
+
51
+ class TestClassifySocial:
52
+ """Unit tests for _classify_social."""
53
+
54
+ def test_high_frequency_is_social(self) -> None:
55
+ """>=10 msgs/hr maps to 'social'."""
56
+ assert _classify_social(10.0) == "social"
57
+ assert _classify_social(20.0) == "social"
58
+
59
+ def test_medium_frequency_is_active(self) -> None:
60
+ """3–9 msgs/hr maps to 'active'."""
61
+ assert _classify_social(5.0) == "active"
62
+ assert _classify_social(3.0) == "active"
63
+
64
+ def test_low_frequency_is_quiet(self) -> None:
65
+ """0.5–2 msgs/hr maps to 'quiet'."""
66
+ assert _classify_social(1.0) == "quiet"
67
+ assert _classify_social(0.5) == "quiet"
68
+
69
+ def test_no_activity_is_isolated(self) -> None:
70
+ """<0.5 msgs/hr maps to 'isolated'."""
71
+ assert _classify_social(0.1) == "isolated"
72
+ assert _classify_social(0.0) == "isolated"
73
+
74
+
75
+ class TestClassifyStress:
76
+ """Unit tests for _classify_stress."""
77
+
78
+ def test_very_low_errors_are_calm(self) -> None:
79
+ """<5% error rate maps to 'calm'."""
80
+ assert _classify_stress(0.0) == "calm"
81
+ assert _classify_stress(0.04) == "calm"
82
+
83
+ def test_low_errors_are_relaxed(self) -> None:
84
+ """5–14% maps to 'relaxed'."""
85
+ assert _classify_stress(0.10) == "relaxed"
86
+ assert _classify_stress(0.05) == "relaxed"
87
+
88
+ def test_moderate_errors_are_tense(self) -> None:
89
+ """15–29% maps to 'tense'."""
90
+ assert _classify_stress(0.20) == "tense"
91
+ assert _classify_stress(0.15) == "tense"
92
+
93
+ def test_high_errors_are_stressed(self) -> None:
94
+ """>=30% maps to 'stressed'."""
95
+ assert _classify_stress(0.30) == "stressed"
96
+ assert _classify_stress(1.0) == "stressed"
97
+
98
+
99
+ class TestComputeSummary:
100
+ """Unit tests for _compute_summary."""
101
+
102
+ def test_stressed_overrides_all(self) -> None:
103
+ """'stressed' dominates regardless of other axes."""
104
+ assert _compute_summary("happy", "social", "stressed") == "stressed"
105
+
106
+ def test_frustrated_overrides_non_stressed(self) -> None:
107
+ """'frustrated' wins when stress is not 'stressed'."""
108
+ assert _compute_summary("frustrated", "social", "calm") == "frustrated"
109
+ assert _compute_summary("frustrated", "active", "relaxed") == "frustrated"
110
+
111
+ def test_tense_follows_frustrated(self) -> None:
112
+ """'tense' wins when not frustrated."""
113
+ assert _compute_summary("content", "active", "tense") == "tense"
114
+
115
+ def test_isolated_when_not_engaged(self) -> None:
116
+ """Isolation surfaces when not otherwise stressed or frustrated."""
117
+ assert _compute_summary("neutral", "isolated", "calm") == "isolated"
118
+
119
+ def test_flourishing_when_happy_and_active(self) -> None:
120
+ """Happy + socially active → 'flourishing'."""
121
+ assert _compute_summary("happy", "social", "calm") == "flourishing"
122
+ assert _compute_summary("happy", "active", "calm") == "flourishing"
123
+
124
+ def test_happy_without_social(self) -> None:
125
+ """Happy but quiet stays 'happy'."""
126
+ assert _compute_summary("happy", "quiet", "calm") == "happy"
127
+
128
+ def test_content_maps_to_content(self) -> None:
129
+ """content + quiet + calm → 'content'."""
130
+ assert _compute_summary("content", "quiet", "calm") == "content"
131
+
132
+ def test_fallback_is_neutral(self) -> None:
133
+ """neutral + quiet + calm → 'neutral'."""
134
+ assert _compute_summary("neutral", "quiet", "calm") == "neutral"
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # MoodTracker integration tests
139
+ # ---------------------------------------------------------------------------
140
+
141
+
142
+ @pytest.fixture
143
+ def tracker(tmp_path: Path) -> MoodTracker:
144
+ """MoodTracker using a temp home directory."""
145
+ return MoodTracker(home=tmp_path)
146
+
147
+
148
+ class TestMoodTrackerUpdate:
149
+ """Tests for MoodTracker.update()."""
150
+
151
+ def test_happy_high_success(self, tracker: MoodTracker) -> None:
152
+ """High response rate produces 'happy' success_mood."""
153
+ snap = tracker.update(messages=100, responses=95, errors=0)
154
+ assert snap.success_mood == "happy"
155
+ assert snap.summary in ("happy", "flourishing")
156
+
157
+ def test_frustrated_low_success(self, tracker: MoodTracker) -> None:
158
+ """Low response rate produces 'frustrated' success_mood and summary."""
159
+ snap = tracker.update(messages=100, responses=10, errors=0)
160
+ assert snap.success_mood == "frustrated"
161
+ assert snap.summary == "frustrated"
162
+
163
+ def test_stressed_high_errors(self, tracker: MoodTracker) -> None:
164
+ """High error rate produces 'stressed' and overrides success in summary."""
165
+ snap = tracker.update(messages=100, responses=90, errors=40)
166
+ assert snap.stress_mood == "stressed"
167
+ assert snap.summary == "stressed"
168
+
169
+ def test_calm_low_errors(self, tracker: MoodTracker) -> None:
170
+ """Near-zero errors produce 'calm' stress mood."""
171
+ snap = tracker.update(messages=50, responses=49, errors=1)
172
+ assert snap.stress_mood == "calm"
173
+
174
+ def test_social_high_frequency(self, tracker: MoodTracker) -> None:
175
+ """Many messages in a short window → 'social'."""
176
+ # 100 messages over 1 hour = 100 msgs/hr
177
+ snap = tracker.update(messages=100, responses=90, errors=0, window_hours=1)
178
+ assert snap.social_mood == "social"
179
+
180
+ def test_isolated_no_messages(self, tracker: MoodTracker) -> None:
181
+ """Few messages in a long window → 'isolated'."""
182
+ snap = tracker.update(messages=2, responses=2, errors=0, window_hours=24)
183
+ assert snap.social_mood == "isolated"
184
+
185
+ def test_zero_messages_defaults_to_neutral(self, tracker: MoodTracker) -> None:
186
+ """Zero messages produce safe default rates (no division by zero)."""
187
+ snap = tracker.update(messages=0, responses=0, errors=0)
188
+ assert snap.success_rate == 1.0
189
+ assert snap.error_rate == 0.0
190
+ assert snap.summary in ("neutral", "isolated") # isolated because 0 msgs/hr
191
+
192
+ def test_rates_are_clamped_to_four_decimals(self, tracker: MoodTracker) -> None:
193
+ """success_rate and error_rate are rounded to 4 decimal places."""
194
+ snap = tracker.update(messages=3, responses=2, errors=1)
195
+ # 2/3 ≈ 0.6667, 1/3 ≈ 0.3333
196
+ assert snap.success_rate == round(2 / 3, 4)
197
+ assert snap.error_rate == round(1 / 3, 4)
198
+
199
+ def test_updated_at_is_set(self, tracker: MoodTracker) -> None:
200
+ """updated_at is populated after update."""
201
+ snap = tracker.update(messages=5, responses=5, errors=0)
202
+ assert snap.updated_at != ""
203
+ assert "T" in snap.updated_at # ISO-8601 format
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # Persistence
208
+ # ---------------------------------------------------------------------------
209
+
210
+
211
+ class TestMoodPersistence:
212
+ """Tests for save / load round-trip."""
213
+
214
+ def test_update_persists_file(self, tmp_path: Path) -> None:
215
+ """update() writes mood.json to the home directory."""
216
+ tracker = MoodTracker(home=tmp_path)
217
+ tracker.update(messages=10, responses=9, errors=0)
218
+ assert (tmp_path / "mood.json").exists()
219
+
220
+ def test_reload_recovers_state(self, tmp_path: Path) -> None:
221
+ """A second MoodTracker in the same home loads the saved snapshot."""
222
+ t1 = MoodTracker(home=tmp_path)
223
+ t1.update(messages=20, responses=18, errors=1)
224
+
225
+ t2 = MoodTracker(home=tmp_path)
226
+ snap = t2.snapshot
227
+ assert snap.messages_processed == 20
228
+ assert snap.responses_sent == 18
229
+ assert snap.errors == 1
230
+
231
+ def test_corrupt_file_yields_neutral(self, tmp_path: Path) -> None:
232
+ """Corrupt mood.json is silently ignored; tracker starts neutral."""
233
+ mood_path = tmp_path / "mood.json"
234
+ mood_path.write_text("not valid json {{{", encoding="utf-8")
235
+ tracker = MoodTracker(home=tmp_path)
236
+ snap = tracker.snapshot
237
+ assert snap.summary == "neutral"
238
+
239
+ def test_missing_file_yields_neutral(self, tmp_path: Path) -> None:
240
+ """Absent mood.json yields a neutral default snapshot."""
241
+ tracker = MoodTracker(home=tmp_path / "nonexistent")
242
+ snap = tracker.snapshot
243
+ assert snap.summary == "neutral"
244
+
245
+
246
+ # ---------------------------------------------------------------------------
247
+ # update_from_metrics
248
+ # ---------------------------------------------------------------------------
249
+
250
+
251
+ class TestUpdateFromMetrics:
252
+ """Tests for MoodTracker.update_from_metrics()."""
253
+
254
+ def test_reads_consciousness_metrics(self, tmp_path: Path) -> None:
255
+ """update_from_metrics reads from ConsciousnessMetrics.to_dict()."""
256
+ from skcapstone.metrics import ConsciousnessMetrics
257
+
258
+ cm = ConsciousnessMetrics(home=tmp_path, persist_interval=0)
259
+ for _ in range(5):
260
+ cm.record_message("peer-a")
261
+ for _ in range(4):
262
+ cm.record_response(50.0, "ollama", "fast")
263
+ cm.record_error()
264
+
265
+ tracker = MoodTracker(home=tmp_path)
266
+ snap = tracker.update_from_metrics(cm)
267
+ assert snap.messages_processed == 5
268
+ assert snap.responses_sent == 4
269
+ assert snap.errors == 1
270
+
271
+ def test_bad_metrics_object_returns_current_snapshot(self, tmp_path: Path) -> None:
272
+ """update_from_metrics with a broken object returns existing snapshot."""
273
+
274
+ class _BrokenMetrics:
275
+ def to_dict(self):
276
+ raise RuntimeError("broken")
277
+
278
+ tracker = MoodTracker(home=tmp_path)
279
+ snap_before = tracker.snapshot
280
+ snap_after = tracker.update_from_metrics(_BrokenMetrics())
281
+ assert snap_after.summary == snap_before.summary
282
+
283
+
284
+ # ---------------------------------------------------------------------------
285
+ # load_snapshot classmethod
286
+ # ---------------------------------------------------------------------------
287
+
288
+
289
+ class TestLoadSnapshot:
290
+ """Tests for MoodTracker.load_snapshot()."""
291
+
292
+ def test_returns_default_when_no_file(self, tmp_path: Path) -> None:
293
+ """Returns a neutral MoodSnapshot when no file exists."""
294
+ snap = MoodTracker.load_snapshot(home=tmp_path)
295
+ assert isinstance(snap, MoodSnapshot)
296
+ assert snap.summary == "neutral"
297
+
298
+ def test_returns_saved_snapshot(self, tmp_path: Path) -> None:
299
+ """Returns the persisted snapshot when mood.json exists."""
300
+ t = MoodTracker(home=tmp_path)
301
+ t.update(messages=30, responses=28, errors=0)
302
+ snap = MoodTracker.load_snapshot(home=tmp_path)
303
+ assert snap.messages_processed == 30
304
+
305
+
306
+ # ---------------------------------------------------------------------------
307
+ # describe()
308
+ # ---------------------------------------------------------------------------
309
+
310
+
311
+ class TestDescribe:
312
+ """Tests for MoodTracker.describe()."""
313
+
314
+ def test_describe_contains_summary(self, tracker: MoodTracker) -> None:
315
+ """describe() includes the summary word."""
316
+ tracker.update(messages=10, responses=10, errors=0)
317
+ text = tracker.describe()
318
+ assert "Mood summary" in text
319
+
320
+ def test_describe_contains_all_axes(self, tracker: MoodTracker) -> None:
321
+ """describe() mentions all three mood axes."""
322
+ tracker.update(messages=10, responses=9, errors=0)
323
+ text = tracker.describe()
324
+ assert "Success" in text
325
+ assert "Social" in text
326
+ assert "Stress" in text
327
+
328
+ def test_describe_contains_updated_at(self, tracker: MoodTracker) -> None:
329
+ """describe() includes the updated_at timestamp."""
330
+ tracker.update(messages=5, responses=5, errors=0)
331
+ text = tracker.describe()
332
+ assert "Updated" in text
333
+
334
+
335
+ # ---------------------------------------------------------------------------
336
+ # Thread safety
337
+ # ---------------------------------------------------------------------------
338
+
339
+
340
+ class TestThreadSafety:
341
+ """Tests for concurrent MoodTracker access."""
342
+
343
+ def test_concurrent_updates_are_safe(self, tmp_path: Path) -> None:
344
+ """Concurrent update() calls do not raise exceptions."""
345
+ tracker = MoodTracker(home=tmp_path)
346
+ errors: list[Exception] = []
347
+
348
+ def _work(i: int) -> None:
349
+ try:
350
+ tracker.update(messages=i + 1, responses=i, errors=0)
351
+ except Exception as exc:
352
+ errors.append(exc)
353
+
354
+ threads = [threading.Thread(target=_work, args=(i,)) for i in range(20)]
355
+ for t in threads:
356
+ t.start()
357
+ for t in threads:
358
+ t.join()
359
+
360
+ assert errors == [], f"Unexpected errors: {errors}"
361
+ # Snapshot must still be a valid MoodSnapshot
362
+ snap = tracker.snapshot
363
+ assert isinstance(snap, MoodSnapshot)
364
+
365
+
366
+ # ---------------------------------------------------------------------------
367
+ # MoodSnapshot model
368
+ # ---------------------------------------------------------------------------
369
+
370
+
371
+ class TestMoodSnapshot:
372
+ """Tests for MoodSnapshot model."""
373
+
374
+ def test_defaults_are_neutral(self) -> None:
375
+ """Default snapshot is neutral / quiet / calm."""
376
+ snap = MoodSnapshot()
377
+ assert snap.summary == "neutral"
378
+ assert snap.success_mood == "neutral"
379
+ assert snap.social_mood == "quiet"
380
+ assert snap.stress_mood == "calm"
381
+
382
+ def test_json_serializable(self) -> None:
383
+ """MoodSnapshot serializes to valid JSON."""
384
+ snap = MoodSnapshot(
385
+ messages_processed=5,
386
+ responses_sent=5,
387
+ errors=0,
388
+ summary="happy",
389
+ updated_at="2026-03-02T12:00:00+00:00",
390
+ )
391
+ data = snap.model_dump_json()
392
+ parsed = json.loads(data)
393
+ assert parsed["summary"] == "happy"
394
+ assert parsed["messages_processed"] == 5
@@ -0,0 +1,269 @@
1
+ """Tests for multi-agent daemon isolation.
2
+
3
+ Covers:
4
+ - Per-agent home directory resolution (opus → agents/opus/, jarvis → agents/jarvis/)
5
+ - Per-agent port assignment (opus=7777, jarvis=7778, unknown → next available)
6
+ - Default (no-agent) mode keeps backward-compatible home and port
7
+ - SKCAPSTONE_AGENT env var propagation
8
+ - DaemonConfig accepts distinct homes and ports for simultaneous agents
9
+ - PID files are isolated per agent home
10
+ - is_running / read_pid are home-scoped (no cross-agent interference)
11
+ - CLI --agent option resolves correct home path
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ from pathlib import Path
19
+ from unittest.mock import MagicMock, patch
20
+
21
+ import pytest
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Helpers
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ def _make_agent_home(tmp_path: Path, agent: str) -> Path:
30
+ """Create a minimal agent home inside tmp_path/agents/<agent>/."""
31
+ home = tmp_path / "agents" / agent
32
+ home.mkdir(parents=True)
33
+ return home
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # 1. _resolve_agent_home — home directory isolation
38
+ # ---------------------------------------------------------------------------
39
+
40
+
41
+ class TestResolveAgentHome:
42
+ def test_named_agent_uses_agents_subdir(self, tmp_path: Path):
43
+ """--agent opus → ~/.skcapstone/agents/opus/"""
44
+ from skcapstone.cli.daemon import _resolve_agent_home
45
+
46
+ with patch("skcapstone.cli.daemon.SKCAPSTONE_ROOT", str(tmp_path)):
47
+ result = _resolve_agent_home("opus", str(tmp_path))
48
+
49
+ assert result == (tmp_path / "agents" / "opus").expanduser()
50
+
51
+ def test_jarvis_uses_own_subdir(self, tmp_path: Path):
52
+ """--agent jarvis → ~/.skcapstone/agents/jarvis/"""
53
+ from skcapstone.cli.daemon import _resolve_agent_home
54
+
55
+ with patch("skcapstone.cli.daemon.SKCAPSTONE_ROOT", str(tmp_path)):
56
+ result = _resolve_agent_home("jarvis", str(tmp_path))
57
+
58
+ assert result == (tmp_path / "agents" / "jarvis").expanduser()
59
+
60
+ def test_no_agent_uses_home_arg(self, tmp_path: Path):
61
+ """No --agent flag → use the --home value directly (backward compat)."""
62
+ from skcapstone.cli.daemon import _resolve_agent_home
63
+
64
+ custom_home = str(tmp_path / "custom")
65
+ result = _resolve_agent_home(None, custom_home)
66
+ assert result == Path(custom_home).expanduser()
67
+
68
+ def test_opus_and_jarvis_homes_are_distinct(self, tmp_path: Path):
69
+ """Opus and Jarvis home paths must not overlap."""
70
+ from skcapstone.cli.daemon import _resolve_agent_home
71
+
72
+ with patch("skcapstone.cli.daemon.SKCAPSTONE_ROOT", str(tmp_path)):
73
+ opus_home = _resolve_agent_home("opus", str(tmp_path))
74
+ jarvis_home = _resolve_agent_home("jarvis", str(tmp_path))
75
+
76
+ assert opus_home != jarvis_home
77
+ assert "opus" in str(opus_home)
78
+ assert "jarvis" in str(jarvis_home)
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # 2. _resolve_agent_port — port isolation
83
+ # ---------------------------------------------------------------------------
84
+
85
+
86
+ class TestResolveAgentPort:
87
+ def test_opus_gets_7777(self):
88
+ """opus always gets port 7777."""
89
+ from skcapstone.cli.daemon import _resolve_agent_port
90
+
91
+ assert _resolve_agent_port("opus", None) == 7777
92
+
93
+ def test_jarvis_gets_7778(self):
94
+ """jarvis always gets port 7778."""
95
+ from skcapstone.cli.daemon import _resolve_agent_port
96
+
97
+ assert _resolve_agent_port("jarvis", None) == 7778
98
+
99
+ def test_explicit_port_overrides_agent_default(self):
100
+ """Explicit --port always wins over the agent default."""
101
+ from skcapstone.cli.daemon import _resolve_agent_port
102
+
103
+ assert _resolve_agent_port("opus", 9999) == 9999
104
+ assert _resolve_agent_port("jarvis", 8000) == 8000
105
+
106
+ def test_no_agent_defaults_to_7777(self):
107
+ """Single-agent / no-flag mode uses 7777."""
108
+ from skcapstone.cli.daemon import _resolve_agent_port
109
+
110
+ assert _resolve_agent_port(None, None) == 7777
111
+
112
+ def test_unknown_agent_gets_next_port(self):
113
+ """An agent not in AGENT_PORTS gets max(ports)+1."""
114
+ from skcapstone import AGENT_PORTS
115
+ from skcapstone.cli.daemon import _resolve_agent_port
116
+
117
+ expected = max(AGENT_PORTS.values()) + 1
118
+ result = _resolve_agent_port("brandnew", None)
119
+ assert result == expected
120
+
121
+ def test_opus_and_jarvis_ports_differ(self):
122
+ """Opus and Jarvis must listen on different ports."""
123
+ from skcapstone.cli.daemon import _resolve_agent_port
124
+
125
+ assert _resolve_agent_port("opus", None) != _resolve_agent_port("jarvis", None)
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # 3. AGENT_PORTS registry in __init__
130
+ # ---------------------------------------------------------------------------
131
+
132
+
133
+ class TestAgentPortsRegistry:
134
+ def test_opus_registered(self):
135
+ from skcapstone import AGENT_PORTS
136
+
137
+ assert "opus" in AGENT_PORTS
138
+ assert AGENT_PORTS["opus"] == 7777
139
+
140
+ def test_jarvis_registered(self):
141
+ from skcapstone import AGENT_PORTS
142
+
143
+ assert "jarvis" in AGENT_PORTS
144
+ assert AGENT_PORTS["jarvis"] == 7778
145
+
146
+ def test_all_ports_unique(self):
147
+ from skcapstone import AGENT_PORTS
148
+
149
+ ports = list(AGENT_PORTS.values())
150
+ assert len(ports) == len(set(ports)), "Duplicate ports in AGENT_PORTS"
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # 4. PID-file isolation — is_running / read_pid are home-scoped
155
+ # ---------------------------------------------------------------------------
156
+
157
+
158
+ class TestPidIsolation:
159
+ def test_pid_file_written_to_agent_home(self, tmp_path: Path):
160
+ """PID file is created inside the agent's own home directory."""
161
+ from skcapstone.daemon import DaemonConfig, DaemonService
162
+
163
+ opus_home = _make_agent_home(tmp_path, "opus")
164
+ config = DaemonConfig(home=opus_home, port=7777)
165
+
166
+ svc = DaemonService(config)
167
+ # Call _write_pid directly without starting the full daemon.
168
+ svc._write_pid()
169
+
170
+ pid_file = opus_home / "daemon.pid"
171
+ assert pid_file.exists()
172
+ assert int(pid_file.read_text().strip()) == os.getpid()
173
+
174
+ def test_pid_files_are_isolated_between_agents(self, tmp_path: Path):
175
+ """Writing opus PID does not affect jarvis PID file."""
176
+ from skcapstone.daemon import DaemonConfig, DaemonService, read_pid
177
+
178
+ opus_home = _make_agent_home(tmp_path, "opus")
179
+ jarvis_home = _make_agent_home(tmp_path, "jarvis")
180
+
181
+ opus_svc = DaemonService(DaemonConfig(home=opus_home, port=7777))
182
+ opus_svc._write_pid()
183
+
184
+ # Jarvis home has no PID file → read_pid returns None.
185
+ assert read_pid(jarvis_home) is None
186
+
187
+ def test_is_running_false_without_pid_file(self, tmp_path: Path):
188
+ """is_running returns False when no PID file exists."""
189
+ from skcapstone.daemon import is_running
190
+
191
+ empty_home = _make_agent_home(tmp_path, "nobody")
192
+ assert is_running(empty_home) is False
193
+
194
+ def test_read_pid_returns_current_pid_after_write(self, tmp_path: Path):
195
+ """read_pid returns the PID we just wrote."""
196
+ from skcapstone.daemon import DaemonConfig, DaemonService, read_pid
197
+
198
+ home = _make_agent_home(tmp_path, "opus")
199
+ svc = DaemonService(DaemonConfig(home=home, port=7777))
200
+ svc._write_pid()
201
+
202
+ assert read_pid(home) == os.getpid()
203
+
204
+
205
+ # ---------------------------------------------------------------------------
206
+ # 5. DaemonConfig — simultaneous distinct configs
207
+ # ---------------------------------------------------------------------------
208
+
209
+
210
+ class TestDaemonConfigMultiAgent:
211
+ def test_two_configs_have_distinct_homes_and_ports(self, tmp_path: Path):
212
+ """Two DaemonConfig instances for opus/jarvis stay isolated."""
213
+ from skcapstone.daemon import DaemonConfig
214
+
215
+ opus_home = _make_agent_home(tmp_path, "opus")
216
+ jarvis_home = _make_agent_home(tmp_path, "jarvis")
217
+
218
+ opus_cfg = DaemonConfig(home=opus_home, port=7777)
219
+ jarvis_cfg = DaemonConfig(home=jarvis_home, port=7778)
220
+
221
+ assert opus_cfg.home != jarvis_cfg.home
222
+ assert opus_cfg.port != jarvis_cfg.port
223
+ assert opus_cfg.port == 7777
224
+ assert jarvis_cfg.port == 7778
225
+
226
+ def test_log_files_are_in_respective_homes(self, tmp_path: Path):
227
+ """Each agent's log file lives under its own home."""
228
+ from skcapstone.daemon import DaemonConfig
229
+
230
+ opus_home = _make_agent_home(tmp_path, "opus")
231
+ jarvis_home = _make_agent_home(tmp_path, "jarvis")
232
+
233
+ opus_cfg = DaemonConfig(home=opus_home, port=7777)
234
+ jarvis_cfg = DaemonConfig(home=jarvis_home, port=7778)
235
+
236
+ assert str(opus_cfg.log_file).startswith(str(opus_home))
237
+ assert str(jarvis_cfg.log_file).startswith(str(jarvis_home))
238
+ assert opus_cfg.log_file != jarvis_cfg.log_file
239
+
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # 6. SKCAPSTONE_AGENT env-var path derivation in __init__
243
+ # ---------------------------------------------------------------------------
244
+
245
+
246
+ class TestAgentHomeEnvVar:
247
+ def test_env_var_produces_agents_subdir(self, monkeypatch):
248
+ """SKCAPSTONE_AGENT=opus → AGENT_HOME includes agents/opus."""
249
+ import importlib
250
+
251
+ monkeypatch.setenv("SKCAPSTONE_AGENT", "opus")
252
+ monkeypatch.setenv("SKCAPSTONE_ROOT", "/tmp/sk")
253
+
254
+ import skcapstone as pkg
255
+ importlib.reload(pkg)
256
+
257
+ assert "agents/opus" in pkg.AGENT_HOME or "agents\\opus" in pkg.AGENT_HOME
258
+
259
+ def test_no_env_var_uses_root_directly(self, monkeypatch):
260
+ """Without SKCAPSTONE_AGENT, AGENT_HOME == SKCAPSTONE_ROOT."""
261
+ import importlib
262
+
263
+ monkeypatch.delenv("SKCAPSTONE_AGENT", raising=False)
264
+ monkeypatch.setenv("SKCAPSTONE_ROOT", "/tmp/sk")
265
+
266
+ import skcapstone as pkg
267
+ importlib.reload(pkg)
268
+
269
+ assert pkg.AGENT_HOME == pkg.SHARED_ROOT