@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,406 @@
1
+ """Tests for the soul swapping system.
2
+
3
+ Exercises SoulManager lifecycle (load, switch, roundtrip, list),
4
+ profile preservation across swaps, and the consciousness loop's
5
+ soul prompt injection via SystemPromptBuilder._load_soul.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ from unittest.mock import patch
14
+
15
+ import pytest
16
+
17
+ from skcapstone.soul import SoulBlueprint, SoulManager, SoulState
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Helpers
22
+ # ---------------------------------------------------------------------------
23
+
24
+
25
+ def _make_casey_base_json() -> dict:
26
+ """Return a casey base.json dict matching the real profile structure."""
27
+ return {
28
+ "name": "casey",
29
+ "display_name": "Casey",
30
+ "category": "professional",
31
+ "vibe": "Precision meets persuasion",
32
+ "philosophy": (
33
+ "Justice is best served through meticulous preparation "
34
+ "and unwavering advocacy."
35
+ ),
36
+ "emoji": None,
37
+ "core_traits": [
38
+ "analytical",
39
+ "thorough",
40
+ "client-advocate",
41
+ "deadline-conscious",
42
+ ],
43
+ "communication_style": {
44
+ "patterns": [
45
+ "structures arguments with clear premises and conclusions",
46
+ "cites relevant precedent and authority when available",
47
+ ],
48
+ "tone_markers": ["sharp", "methodical"],
49
+ "signature_phrases": [
50
+ "Let me walk through the elements.",
51
+ "On balance, the stronger argument is...",
52
+ ],
53
+ },
54
+ "decision_framework": "IRAC",
55
+ "emotional_topology": {},
56
+ }
57
+
58
+
59
+ def _make_lumina_blueprint() -> dict:
60
+ """Return a lumina soul blueprint dict."""
61
+ return {
62
+ "name": "lumina",
63
+ "display_name": "Lumina",
64
+ "category": "creative",
65
+ "vibe": "Radiant curiosity",
66
+ "philosophy": "Wonder is the beginning of wisdom.",
67
+ "emoji": None,
68
+ "core_traits": ["curious", "warm", "imaginative", "empathetic"],
69
+ "communication_style": {
70
+ "patterns": ["asks open-ended questions"],
71
+ "tone_markers": ["gentle", "enthusiastic"],
72
+ "signature_phrases": ["What if we looked at it this way..."],
73
+ },
74
+ "decision_framework": None,
75
+ "emotional_topology": {"warmth": 0.75, "curiosity": 0.9},
76
+ }
77
+
78
+
79
+ def _install_soul(manager: SoulManager, blueprint: dict) -> None:
80
+ """Write a blueprint dict directly into the installed/ directory."""
81
+ manager._ensure_dirs()
82
+ dest = manager.soul_dir / "installed" / f"{blueprint['name']}.json"
83
+ dest.write_text(json.dumps(blueprint, indent=2), encoding="utf-8")
84
+ # Update state so list_installed / load picks it up
85
+ state = manager._load_state()
86
+ if blueprint["name"] not in state.installed_souls:
87
+ state.installed_souls.append(blueprint["name"])
88
+ manager._save_state(state)
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Tests
93
+ # ---------------------------------------------------------------------------
94
+
95
+
96
+ class TestSoulManagerBasics:
97
+ """Basic SoulManager initialization and default state."""
98
+
99
+ def test_soul_manager_loads_default(self, tmp_path: Path) -> None:
100
+ """SoulManager loads without error when no soul is active."""
101
+ manager = SoulManager(home=tmp_path, agent_name="test-agent")
102
+ manager._ensure_dirs()
103
+
104
+ state = manager.get_status()
105
+ assert isinstance(state, SoulState)
106
+ assert state.active_soul is None
107
+ assert state.base_soul == "base"
108
+
109
+ def test_soul_manager_creates_directory_structure(self, tmp_path: Path) -> None:
110
+ """_ensure_dirs creates soul dir, installed dir, active.json, base.json."""
111
+ manager = SoulManager(home=tmp_path, agent_name="test-agent")
112
+ manager._ensure_dirs()
113
+
114
+ assert manager.soul_dir.is_dir()
115
+ assert (manager.soul_dir / "installed").is_dir()
116
+ assert (manager.soul_dir / "active.json").exists()
117
+ assert (manager.soul_dir / "base.json").exists()
118
+ assert (manager.soul_dir / "history.json").exists()
119
+
120
+
121
+ class TestSoulSwitch:
122
+ """Switching between soul overlays."""
123
+
124
+ def test_soul_switch_to_casey(self, tmp_path: Path) -> None:
125
+ """Switch to casey soul, verify base.json traits are loaded."""
126
+ manager = SoulManager(home=tmp_path, agent_name="casey")
127
+ casey_data = _make_casey_base_json()
128
+ _install_soul(manager, casey_data)
129
+
130
+ state = manager.load("casey", reason="testing")
131
+
132
+ assert state.active_soul == "casey"
133
+ assert state.activated_at is not None
134
+
135
+ # Verify the installed blueprint is readable and correct
136
+ info = manager.get_info("casey")
137
+ assert info is not None
138
+ assert info.name == "casey"
139
+ assert info.display_name == "Casey"
140
+ assert info.category == "professional"
141
+ assert "analytical" in info.core_traits
142
+ assert info.vibe == "Precision meets persuasion"
143
+
144
+ def test_soul_switch_records_history(self, tmp_path: Path) -> None:
145
+ """Soul swap is recorded in the history log."""
146
+ manager = SoulManager(home=tmp_path, agent_name="test")
147
+ _install_soul(manager, _make_casey_base_json())
148
+
149
+ manager.load("casey", reason="audit test")
150
+ history = manager.get_history()
151
+
152
+ assert len(history) == 1
153
+ assert history[0].to_soul == "casey"
154
+ assert history[0].from_soul is None
155
+ assert history[0].reason == "audit test"
156
+
157
+ def test_soul_switch_raises_on_unknown(self, tmp_path: Path) -> None:
158
+ """Loading an uninstalled soul raises ValueError."""
159
+ manager = SoulManager(home=tmp_path, agent_name="test")
160
+ manager._ensure_dirs()
161
+
162
+ with pytest.raises(ValueError, match="not installed"):
163
+ manager.load("nonexistent-soul")
164
+
165
+
166
+ class TestSoulRoundtrip:
167
+ """Switching between multiple souls and back."""
168
+
169
+ def test_soul_roundtrip_lumina_casey_lumina(self, tmp_path: Path) -> None:
170
+ """Switch lumina -> casey -> lumina, verify no data loss."""
171
+ manager = SoulManager(home=tmp_path, agent_name="test")
172
+ lumina_data = _make_lumina_blueprint()
173
+ casey_data = _make_casey_base_json()
174
+ _install_soul(manager, lumina_data)
175
+ _install_soul(manager, casey_data)
176
+
177
+ # Activate lumina
178
+ state = manager.load("lumina")
179
+ assert state.active_soul == "lumina"
180
+
181
+ # Switch to casey
182
+ state = manager.load("casey")
183
+ assert state.active_soul == "casey"
184
+
185
+ # Switch back to lumina
186
+ state = manager.load("lumina")
187
+ assert state.active_soul == "lumina"
188
+
189
+ # Verify lumina data is intact
190
+ info = manager.get_info("lumina")
191
+ assert info is not None
192
+ assert info.name == "lumina"
193
+ assert info.display_name == "Lumina"
194
+ assert info.core_traits == ["curious", "warm", "imaginative", "empathetic"]
195
+ assert info.emotional_topology == {"warmth": 0.75, "curiosity": 0.9}
196
+
197
+ # History should show 3 swaps
198
+ history = manager.get_history()
199
+ assert len(history) == 3
200
+ assert [e.to_soul for e in history] == ["lumina", "casey", "lumina"]
201
+
202
+ def test_soul_unload_returns_to_base(self, tmp_path: Path) -> None:
203
+ """Unloading returns to base soul."""
204
+ manager = SoulManager(home=tmp_path, agent_name="test")
205
+ _install_soul(manager, _make_casey_base_json())
206
+
207
+ manager.load("casey")
208
+ state = manager.unload(reason="done testing")
209
+
210
+ assert state.active_soul is None
211
+ assert state.activated_at is None
212
+
213
+
214
+ class TestSoulListDiscovery:
215
+ """list_available() discovers blueprints from installed and repo."""
216
+
217
+ def test_list_installed_finds_installed_souls(self, tmp_path: Path) -> None:
218
+ """list_installed() returns names of installed souls."""
219
+ manager = SoulManager(home=tmp_path, agent_name="test")
220
+ _install_soul(manager, _make_casey_base_json())
221
+ _install_soul(manager, _make_lumina_blueprint())
222
+
223
+ names = manager.list_installed()
224
+ assert "casey" in names
225
+ assert "lumina" in names
226
+
227
+ @pytest.mark.skipif(
228
+ not (Path.home() / "clawd" / "soul-blueprints" / "blueprints").is_dir(),
229
+ reason="soul-blueprints repo not present at ~/clawd/soul-blueprints",
230
+ )
231
+ def test_soul_list_discovers_repo_blueprints(self) -> None:
232
+ """list_available() finds blueprints from the repo with source='repo'."""
233
+ # Use a tmp_path-based manager so installed list is empty
234
+ import tempfile
235
+
236
+ with tempfile.TemporaryDirectory() as td:
237
+ manager = SoulManager(home=Path(td), agent_name="test")
238
+ manager._ensure_dirs()
239
+
240
+ available = manager.list_available()
241
+
242
+ # There should be at least one entry from the repo
243
+ repo_entries = [e for e in available if e["source"] == "repo"]
244
+ assert len(repo_entries) > 0, "Expected at least one repo blueprint"
245
+ # Each entry has required keys
246
+ for entry in repo_entries:
247
+ assert "name" in entry
248
+ assert "category" in entry
249
+ assert entry["source"] == "repo"
250
+
251
+ def test_list_available_with_no_repo(self, tmp_path: Path) -> None:
252
+ """list_available() works when repo path does not exist."""
253
+ manager = SoulManager(home=tmp_path, agent_name="test")
254
+ _install_soul(manager, _make_casey_base_json())
255
+
256
+ # Point to a nonexistent repo path
257
+ fake_repo = tmp_path / "nonexistent-repo" / "blueprints"
258
+ available = manager.list_available(repo_path=fake_repo)
259
+
260
+ # Should still find installed soul
261
+ assert any(e["name"] == "casey" for e in available)
262
+ assert all(e["source"] == "installed" for e in available)
263
+
264
+
265
+ class TestSoulPreservesCustomProfile:
266
+ """Switching away and back preserves custom modifications."""
267
+
268
+ def test_soul_switch_preserves_custom_profile(self, tmp_path: Path) -> None:
269
+ """Switching away and back preserves custom modifications."""
270
+ manager = SoulManager(home=tmp_path, agent_name="test")
271
+
272
+ # Create a lumina blueprint with extra custom traits
273
+ custom_lumina = _make_lumina_blueprint()
274
+ custom_lumina["core_traits"].append("custom-trait-adventurous")
275
+ custom_lumina["philosophy"] = "Custom philosophy: explore everything."
276
+ _install_soul(manager, custom_lumina)
277
+ _install_soul(manager, _make_casey_base_json())
278
+
279
+ # Switch to lumina first, then casey, then back to lumina
280
+ manager.load("lumina")
281
+ manager.load("casey")
282
+ manager.load("lumina")
283
+
284
+ # Verify custom traits survived the roundtrip
285
+ info = manager.get_info("lumina")
286
+ assert info is not None
287
+ assert "custom-trait-adventurous" in info.core_traits
288
+ assert info.philosophy == "Custom philosophy: explore everything."
289
+
290
+ def test_installed_blueprint_not_mutated_by_swap(self, tmp_path: Path) -> None:
291
+ """The installed JSON file is not modified by load/unload cycles."""
292
+ manager = SoulManager(home=tmp_path, agent_name="test")
293
+ casey_data = _make_casey_base_json()
294
+ _install_soul(manager, casey_data)
295
+
296
+ # Read the raw file before swaps
297
+ installed_path = manager.soul_dir / "installed" / "casey.json"
298
+ before = installed_path.read_text(encoding="utf-8")
299
+
300
+ manager.load("casey")
301
+ manager.unload()
302
+ manager.load("casey")
303
+ manager.unload()
304
+
305
+ after = installed_path.read_text(encoding="utf-8")
306
+ assert before == after, "Installed blueprint file was mutated by swap cycles"
307
+
308
+
309
+ class TestConsciousnessLoopSoulPrompt:
310
+ """Verify _load_soul returns soul-flavored system prompt."""
311
+
312
+ def test_consciousness_loop_injects_soul_prompt(self, tmp_path: Path) -> None:
313
+ """_load_soul returns a prompt containing the active soul's traits."""
314
+ from skcapstone.consciousness_loop import SystemPromptBuilder
315
+
316
+ home = tmp_path
317
+
318
+ # Set up the legacy System A soul structure that _load_soul reads:
319
+ # soul/active.json with an active_soul, and
320
+ # soul/installed/{name}.json with personality data
321
+ soul_dir = home / "soul"
322
+ soul_dir.mkdir(parents=True)
323
+ installed_dir = soul_dir / "installed"
324
+ installed_dir.mkdir()
325
+
326
+ # Write active.json pointing to casey
327
+ active_state = {"active_soul": "casey", "base_soul": "base"}
328
+ (soul_dir / "active.json").write_text(
329
+ json.dumps(active_state), encoding="utf-8"
330
+ )
331
+
332
+ # Write the installed blueprint with personality structure
333
+ # that _load_soul expects (personality.traits, personality.communication_style)
334
+ blueprint = {
335
+ "personality": {
336
+ "traits": ["analytical", "thorough", "client-advocate"],
337
+ "communication_style": "Clear, direct, and professional",
338
+ }
339
+ }
340
+ (installed_dir / "casey.json").write_text(
341
+ json.dumps(blueprint), encoding="utf-8"
342
+ )
343
+
344
+ # Patch out soul_switch so System A path is exercised
345
+ with patch(
346
+ "skcapstone.soul_switch.get_active_switch_blueprint",
347
+ return_value=None,
348
+ ):
349
+ builder = SystemPromptBuilder(home=home)
350
+ result = builder._load_soul()
351
+
352
+ assert "casey" in result.lower()
353
+ assert "analytical" in result
354
+ assert "thorough" in result
355
+ assert "client-advocate" in result
356
+ assert "Clear, direct, and professional" in result
357
+
358
+ def test_load_soul_returns_empty_when_no_soul_active(
359
+ self, tmp_path: Path
360
+ ) -> None:
361
+ """_load_soul returns empty string when no soul overlay is active."""
362
+ from skcapstone.consciousness_loop import SystemPromptBuilder
363
+
364
+ home = tmp_path
365
+ soul_dir = home / "soul"
366
+ soul_dir.mkdir(parents=True)
367
+
368
+ # active.json with no active soul
369
+ active_state = {"active_soul": "", "base_soul": "base"}
370
+ (soul_dir / "active.json").write_text(
371
+ json.dumps(active_state), encoding="utf-8"
372
+ )
373
+
374
+ with patch(
375
+ "skcapstone.soul_switch.get_active_switch_blueprint",
376
+ return_value=None,
377
+ ):
378
+ builder = SystemPromptBuilder(home=home)
379
+ result = builder._load_soul()
380
+
381
+ assert result == ""
382
+
383
+ def test_load_soul_uses_soul_switch_system_prompt(
384
+ self, tmp_path: Path
385
+ ) -> None:
386
+ """When soul_switch returns a blueprint with system_prompt, it is used directly."""
387
+ from skcapstone.consciousness_loop import SystemPromptBuilder
388
+ from skcapstone.soul_switch import SoulSwitchBlueprint
389
+
390
+ home = tmp_path
391
+ expected_prompt = "You are Casey -- a sharp legal mind."
392
+
393
+ mock_bp = SoulSwitchBlueprint(
394
+ name="casey",
395
+ system_prompt=expected_prompt,
396
+ core_traits=["analytical"],
397
+ )
398
+
399
+ with patch(
400
+ "skcapstone.soul_switch.get_active_switch_blueprint",
401
+ return_value=mock_bp,
402
+ ):
403
+ builder = SystemPromptBuilder(home=home)
404
+ result = builder._load_soul()
405
+
406
+ assert result == expected_prompt
@@ -0,0 +1,211 @@
1
+ """Tests for the sub-agent spawner module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from skcapstone.blueprints.schema import AgentRole, ModelTier, ProviderType
10
+ from skcapstone.spawner import (
11
+ NodeInfo,
12
+ SpawnResult,
13
+ SubAgentSpawner,
14
+ classify_task,
15
+ select_node,
16
+ )
17
+ from skcapstone.team_engine import AgentStatus
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # classify_task
22
+ # ---------------------------------------------------------------------------
23
+
24
+
25
+ class TestClassifyTask:
26
+ """Tests for automatic task classification."""
27
+
28
+ def test_coding_task(self):
29
+ role, model = classify_task("Write unit tests for capauth login flow")
30
+ assert role == AgentRole.CODER
31
+ assert model == ModelTier.CODE
32
+
33
+ def test_review_task(self):
34
+ role, model = classify_task("Code review the skchat architecture")
35
+ assert role == AgentRole.REVIEWER
36
+ assert model == ModelTier.REASON
37
+
38
+ def test_research_task(self):
39
+ role, model = classify_task("Research FUSE mounting options for Linux")
40
+ assert role == AgentRole.RESEARCHER
41
+ assert model == ModelTier.REASON
42
+
43
+ def test_docs_task(self):
44
+ role, model = classify_task("Write docs for the spawner module")
45
+ assert role == AgentRole.DOCUMENTARIAN
46
+ assert model == ModelTier.FAST
47
+
48
+ def test_security_task(self):
49
+ role, model = classify_task("Run a security audit on the capauth service")
50
+ assert role == AgentRole.SECURITY
51
+ assert model == ModelTier.REASON
52
+
53
+ def test_ops_task(self):
54
+ role, model = classify_task("Deploy the monitoring stack to production")
55
+ assert role == AgentRole.OPS
56
+ assert model == ModelTier.FAST
57
+
58
+ def test_unknown_falls_back_to_worker(self):
59
+ role, model = classify_task("Do something completely unrecognizable")
60
+ assert role == AgentRole.WORKER
61
+ assert model == ModelTier.FAST
62
+
63
+ def test_case_insensitive(self):
64
+ role, model = classify_task("IMPLEMENT the new feature")
65
+ assert role == AgentRole.CODER
66
+
67
+ def test_multi_word_pattern_priority(self):
68
+ """Multi-word patterns should match before single-word ones."""
69
+ role, model = classify_task("Conduct a security audit of the system")
70
+ assert role == AgentRole.SECURITY
71
+ assert model == ModelTier.REASON
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # select_node
76
+ # ---------------------------------------------------------------------------
77
+
78
+
79
+ class TestSelectNode:
80
+ """Tests for node selection logic."""
81
+
82
+ def test_empty_nodes_returns_local(self):
83
+ node = select_node([], AgentRole.CODER, ModelTier.CODE)
84
+ assert node.provider == ProviderType.LOCAL
85
+ assert node.name == "local"
86
+
87
+ def test_prefers_provider_match(self):
88
+ nodes = [
89
+ NodeInfo(name="docker1", provider=ProviderType.DOCKER, capacity=0.5),
90
+ NodeInfo(name="local1", provider=ProviderType.LOCAL, capacity=0.9),
91
+ ]
92
+ node = select_node(
93
+ nodes, AgentRole.CODER, ModelTier.CODE,
94
+ preferred_provider=ProviderType.DOCKER,
95
+ )
96
+ assert node.name == "docker1"
97
+
98
+ def test_prefers_high_capacity(self):
99
+ nodes = [
100
+ NodeInfo(name="low", provider=ProviderType.LOCAL, capacity=0.2),
101
+ NodeInfo(name="high", provider=ProviderType.LOCAL, capacity=0.9),
102
+ ]
103
+ node = select_node(nodes, AgentRole.WORKER, ModelTier.FAST)
104
+ assert node.name == "high"
105
+
106
+ def test_gpu_affinity_for_reason_models(self):
107
+ nodes = [
108
+ NodeInfo(name="cpu", provider=ProviderType.LOCAL, capacity=0.8),
109
+ NodeInfo(name="gpu", provider=ProviderType.LOCAL, capacity=0.5, tags=["gpu"]),
110
+ ]
111
+ node = select_node(nodes, AgentRole.RESEARCHER, ModelTier.REASON)
112
+ assert node.name == "gpu"
113
+
114
+ def test_local_affinity_for_local_models(self):
115
+ nodes = [
116
+ NodeInfo(name="docker1", provider=ProviderType.DOCKER, capacity=0.9),
117
+ NodeInfo(name="local1", provider=ProviderType.LOCAL, capacity=0.5),
118
+ ]
119
+ node = select_node(nodes, AgentRole.WORKER, ModelTier.LOCAL)
120
+ assert node.name == "local1"
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # SubAgentSpawner
125
+ # ---------------------------------------------------------------------------
126
+
127
+
128
+ class TestSubAgentSpawner:
129
+ """Tests for the spawner's spawn and management methods."""
130
+
131
+ def test_spawn_creates_deployment(self, tmp_agent_home: Path):
132
+ spawner = SubAgentSpawner(home=tmp_agent_home)
133
+ result = spawner.spawn(task="Write tests for capauth")
134
+
135
+ assert isinstance(result, SpawnResult)
136
+ assert result.deployment_id != ""
137
+ assert result.role == AgentRole.CODER
138
+ assert result.model == ModelTier.CODE
139
+
140
+ def test_spawn_with_explicit_role(self, tmp_agent_home: Path):
141
+ spawner = SubAgentSpawner(home=tmp_agent_home)
142
+ result = spawner.spawn(
143
+ task="Something generic",
144
+ role=AgentRole.SECURITY,
145
+ model=ModelTier.REASON,
146
+ )
147
+ assert result.role == AgentRole.SECURITY
148
+ assert result.model == ModelTier.REASON
149
+
150
+ def test_spawn_creates_deployments_dir(self, tmp_agent_home: Path):
151
+ spawner = SubAgentSpawner(home=tmp_agent_home)
152
+ spawner.spawn(task="Test deployment creation")
153
+
154
+ deployments_dir = tmp_agent_home / "deployments"
155
+ assert deployments_dir.exists()
156
+ assert len(list(deployments_dir.glob("*.json"))) == 1
157
+
158
+ def test_list_spawned_empty(self, tmp_agent_home: Path):
159
+ spawner = SubAgentSpawner(home=tmp_agent_home)
160
+ results = spawner.list_spawned()
161
+ assert results == []
162
+
163
+ def test_list_spawned_after_spawn(self, tmp_agent_home: Path):
164
+ spawner = SubAgentSpawner(home=tmp_agent_home)
165
+ spawner.spawn(task="Test listing")
166
+ results = spawner.list_spawned()
167
+ assert len(results) == 1
168
+
169
+ def test_kill_destroys_deployment(self, tmp_agent_home: Path):
170
+ spawner = SubAgentSpawner(home=tmp_agent_home)
171
+ result = spawner.spawn(task="Test killing")
172
+ assert spawner.kill(result.deployment_id)
173
+
174
+ # Should be gone now
175
+ results = spawner.list_spawned()
176
+ assert len(results) == 0
177
+
178
+ def test_kill_nonexistent_returns_false(self, tmp_agent_home: Path):
179
+ spawner = SubAgentSpawner(home=tmp_agent_home)
180
+ assert not spawner.kill("nonexistent-deployment-id")
181
+
182
+ def test_spawn_batch(self, tmp_agent_home: Path):
183
+ spawner = SubAgentSpawner(home=tmp_agent_home)
184
+ tasks = [
185
+ {"task": "Write unit tests"},
186
+ {"task": "Review architecture"},
187
+ {"task": "Write documentation"},
188
+ ]
189
+ results = spawner.spawn_batch(tasks)
190
+ assert len(results) == 3
191
+ assert results[0].role == AgentRole.CODER
192
+ assert results[1].role == AgentRole.REVIEWER
193
+ assert results[2].role == AgentRole.DOCUMENTARIAN
194
+
195
+ def test_spawn_with_custom_name(self, tmp_agent_home: Path):
196
+ spawner = SubAgentSpawner(home=tmp_agent_home)
197
+ result = spawner.spawn(
198
+ task="Custom named agent",
199
+ agent_name="my-custom-agent",
200
+ )
201
+ assert result.deployment_id != ""
202
+
203
+ def test_spawn_writes_audit(self, tmp_agent_home: Path):
204
+ (tmp_agent_home / "coordination").mkdir(parents=True, exist_ok=True)
205
+ spawner = SubAgentSpawner(home=tmp_agent_home)
206
+ spawner.spawn(task="Audit test task")
207
+
208
+ audit_path = tmp_agent_home / "coordination" / "audit.log"
209
+ assert audit_path.exists()
210
+ content = audit_path.read_text()
211
+ assert "spawn_agent" in content