@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,354 @@
1
+ """Tests for Sovereign Heartbeat v2."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timedelta, timezone
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from skcapstone.heartbeat import (
12
+ AgentCapability,
13
+ Heartbeat,
14
+ HeartbeatBeacon,
15
+ MeshHealth,
16
+ NodeCapacity,
17
+ PeerInfo,
18
+ )
19
+
20
+
21
+ @pytest.fixture
22
+ def home(tmp_path: Path) -> Path:
23
+ """Create a minimal agent home."""
24
+ (tmp_path / "identity").mkdir()
25
+ (tmp_path / "identity" / "identity.json").write_text(json.dumps({
26
+ "name": "opus",
27
+ "fingerprint": "ABCD1234567890AB",
28
+ }), encoding="utf-8")
29
+ return tmp_path
30
+
31
+
32
+ @pytest.fixture
33
+ def beacon(home: Path) -> HeartbeatBeacon:
34
+ """Create an initialized HeartbeatBeacon."""
35
+ b = HeartbeatBeacon(home, agent_name="opus")
36
+ b.initialize()
37
+ return b
38
+
39
+
40
+ def _write_peer_heartbeat(
41
+ home: Path,
42
+ agent_name: str,
43
+ status: str = "alive",
44
+ ttl_seconds: int = 300,
45
+ age_seconds: float = 0,
46
+ capabilities: list[dict] | None = None,
47
+ ) -> None:
48
+ """Helper to create a peer heartbeat file."""
49
+ ts = datetime.now(timezone.utc) - timedelta(seconds=age_seconds)
50
+ hb = {
51
+ "agent_name": agent_name,
52
+ "status": status,
53
+ "hostname": f"{agent_name}-host",
54
+ "platform": "Linux x86_64",
55
+ "timestamp": ts.isoformat(),
56
+ "ttl_seconds": ttl_seconds,
57
+ "uptime_hours": 1.0,
58
+ "capabilities": capabilities or [],
59
+ "claimed_tasks": [],
60
+ "capacity": {},
61
+ }
62
+ hb_dir = home / "heartbeats"
63
+ hb_dir.mkdir(parents=True, exist_ok=True)
64
+ (hb_dir / f"{agent_name}.json").write_text(
65
+ json.dumps(hb, indent=2), encoding="utf-8",
66
+ )
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Initialization
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ class TestInitialization:
75
+ """Tests for heartbeat setup."""
76
+
77
+ def test_initialize_creates_dir(self, home: Path) -> None:
78
+ """Initialize creates heartbeats directory."""
79
+ b = HeartbeatBeacon(home)
80
+ b.initialize()
81
+ assert (home / "heartbeats").is_dir()
82
+
83
+ def test_initialize_idempotent(self, beacon: HeartbeatBeacon, home: Path) -> None:
84
+ """Multiple initializations don't break anything."""
85
+ beacon.initialize()
86
+ beacon.initialize()
87
+ assert (home / "heartbeats").is_dir()
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Pulse
92
+ # ---------------------------------------------------------------------------
93
+
94
+
95
+ class TestPulse:
96
+ """Tests for heartbeat publishing."""
97
+
98
+ def test_pulse_creates_file(self, beacon: HeartbeatBeacon, home: Path) -> None:
99
+ """Pulse creates the agent's heartbeat file."""
100
+ beacon.pulse()
101
+ assert (home / "heartbeats" / "opus.json").exists()
102
+
103
+ def test_pulse_returns_heartbeat(self, beacon: HeartbeatBeacon) -> None:
104
+ """Pulse returns a Heartbeat object."""
105
+ hb = beacon.pulse()
106
+ assert hb.agent_name == "opus"
107
+ assert hb.status == "alive"
108
+ assert hb.is_alive is True
109
+
110
+ def test_pulse_with_status(self, beacon: HeartbeatBeacon) -> None:
111
+ """Pulse accepts custom status."""
112
+ hb = beacon.pulse(status="busy")
113
+ assert hb.status == "busy"
114
+
115
+ def test_pulse_with_tasks(self, beacon: HeartbeatBeacon) -> None:
116
+ """Pulse tracks claimed tasks."""
117
+ hb = beacon.pulse(claimed_tasks=["task1", "task2"])
118
+ assert hb.claimed_tasks == ["task1", "task2"]
119
+
120
+ def test_pulse_with_model(self, beacon: HeartbeatBeacon) -> None:
121
+ """Pulse tracks loaded model."""
122
+ hb = beacon.pulse(loaded_model="claude-opus-4-6")
123
+ assert hb.loaded_model == "claude-opus-4-6"
124
+
125
+ def test_pulse_detects_capacity(self, beacon: HeartbeatBeacon) -> None:
126
+ """Pulse detects node capacity."""
127
+ hb = beacon.pulse()
128
+ assert hb.capacity.cpu_count > 0
129
+
130
+ def test_pulse_detects_fingerprint(self, beacon: HeartbeatBeacon) -> None:
131
+ """Pulse reads identity fingerprint."""
132
+ hb = beacon.pulse()
133
+ assert hb.fingerprint == "ABCD1234567890AB"
134
+
135
+ def test_pulse_atomic_write(self, beacon: HeartbeatBeacon, home: Path) -> None:
136
+ """Pulse uses atomic write (no .tmp left)."""
137
+ beacon.pulse()
138
+ tmp = home / "heartbeats" / "opus.json.tmp"
139
+ assert not tmp.exists()
140
+
141
+ def test_pulse_with_capabilities(self, beacon: HeartbeatBeacon) -> None:
142
+ """Pulse accepts custom capabilities."""
143
+ caps = [AgentCapability(name="code-review", version="2.0")]
144
+ hb = beacon.pulse(capabilities=caps)
145
+ assert len(hb.capabilities) == 1
146
+ assert hb.capabilities[0].name == "code-review"
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # Read heartbeat
151
+ # ---------------------------------------------------------------------------
152
+
153
+
154
+ class TestReadHeartbeat:
155
+ """Tests for reading heartbeats."""
156
+
157
+ def test_read_own_heartbeat(self, beacon: HeartbeatBeacon) -> None:
158
+ """Read own heartbeat after pulse."""
159
+ beacon.pulse()
160
+ hb = beacon.read_heartbeat("opus")
161
+ assert hb is not None
162
+ assert hb.agent_name == "opus"
163
+
164
+ def test_read_peer_heartbeat(self, beacon: HeartbeatBeacon, home: Path) -> None:
165
+ """Read a peer's heartbeat."""
166
+ _write_peer_heartbeat(home, "lumina")
167
+ hb = beacon.read_heartbeat("lumina")
168
+ assert hb is not None
169
+ assert hb.agent_name == "lumina"
170
+
171
+ def test_read_nonexistent(self, beacon: HeartbeatBeacon) -> None:
172
+ """Reading nonexistent heartbeat returns None."""
173
+ assert beacon.read_heartbeat("ghost") is None
174
+
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Discover peers
178
+ # ---------------------------------------------------------------------------
179
+
180
+
181
+ class TestDiscoverPeers:
182
+ """Tests for peer discovery."""
183
+
184
+ def test_discover_empty(self, beacon: HeartbeatBeacon) -> None:
185
+ """Empty mesh returns no peers."""
186
+ peers = beacon.discover_peers()
187
+ assert peers == []
188
+
189
+ def test_discover_excludes_self(self, beacon: HeartbeatBeacon, home: Path) -> None:
190
+ """Discovery excludes own heartbeat by default."""
191
+ beacon.pulse()
192
+ _write_peer_heartbeat(home, "lumina")
193
+ peers = beacon.discover_peers()
194
+ names = [p.agent_name for p in peers]
195
+ assert "lumina" in names
196
+ assert "opus" not in names
197
+
198
+ def test_discover_includes_self(self, beacon: HeartbeatBeacon, home: Path) -> None:
199
+ """Discovery can include own heartbeat."""
200
+ beacon.pulse()
201
+ peers = beacon.discover_peers(include_self=True)
202
+ names = [p.agent_name for p in peers]
203
+ assert "opus" in names
204
+
205
+ def test_discover_marks_stale_offline(self, beacon: HeartbeatBeacon, home: Path) -> None:
206
+ """Stale heartbeats are marked as offline."""
207
+ _write_peer_heartbeat(home, "stale-agent", ttl_seconds=60, age_seconds=120)
208
+ peers = beacon.discover_peers()
209
+ stale = next(p for p in peers if p.agent_name == "stale-agent")
210
+ assert stale.alive is False
211
+ assert stale.status == "offline"
212
+
213
+ def test_discover_live_peers(self, beacon: HeartbeatBeacon, home: Path) -> None:
214
+ """Live heartbeats are correctly identified."""
215
+ _write_peer_heartbeat(home, "live-agent", ttl_seconds=300, age_seconds=10)
216
+ peers = beacon.discover_peers()
217
+ live = next(p for p in peers if p.agent_name == "live-agent")
218
+ assert live.alive is True
219
+ assert live.status == "alive"
220
+
221
+ def test_discover_with_capabilities(self, beacon: HeartbeatBeacon, home: Path) -> None:
222
+ """Peer capabilities are included in discovery."""
223
+ _write_peer_heartbeat(
224
+ home, "capable-agent",
225
+ capabilities=[{"name": "code-review", "enabled": True}],
226
+ )
227
+ peers = beacon.discover_peers()
228
+ cap = next(p for p in peers if p.agent_name == "capable-agent")
229
+ assert "code-review" in cap.capabilities
230
+
231
+
232
+ # ---------------------------------------------------------------------------
233
+ # Mesh health
234
+ # ---------------------------------------------------------------------------
235
+
236
+
237
+ class TestMeshHealth:
238
+ """Tests for mesh health reporting."""
239
+
240
+ def test_mesh_health_empty(self, beacon: HeartbeatBeacon) -> None:
241
+ """Empty mesh health."""
242
+ health = beacon.mesh_health()
243
+ assert health.total_peers == 0
244
+ assert health.alive_peers == 0
245
+
246
+ def test_mesh_health_mixed(self, beacon: HeartbeatBeacon, home: Path) -> None:
247
+ """Mixed mesh with alive and stale peers."""
248
+ beacon.pulse()
249
+ _write_peer_heartbeat(home, "lumina", age_seconds=10)
250
+ _write_peer_heartbeat(home, "stale", ttl_seconds=60, age_seconds=120)
251
+
252
+ health = beacon.mesh_health()
253
+ assert health.total_peers == 3 # opus + lumina + stale
254
+ assert health.alive_peers == 2
255
+ assert health.offline_peers == 1
256
+
257
+ def test_mesh_health_capabilities(self, beacon: HeartbeatBeacon, home: Path) -> None:
258
+ """Mesh health aggregates capabilities."""
259
+ _write_peer_heartbeat(
260
+ home, "agent-a",
261
+ capabilities=[{"name": "code-review", "enabled": True}],
262
+ )
263
+ _write_peer_heartbeat(
264
+ home, "agent-b",
265
+ capabilities=[{"name": "deployment", "enabled": True}],
266
+ )
267
+
268
+ health = beacon.mesh_health()
269
+ assert "code-review" in health.total_capabilities
270
+ assert "deployment" in health.total_capabilities
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # Find capable
275
+ # ---------------------------------------------------------------------------
276
+
277
+
278
+ class TestFindCapable:
279
+ """Tests for capability-based peer search."""
280
+
281
+ def test_find_capable_peers(self, beacon: HeartbeatBeacon, home: Path) -> None:
282
+ """Find peers with a specific capability."""
283
+ _write_peer_heartbeat(
284
+ home, "reviewer",
285
+ capabilities=[{"name": "code-review", "enabled": True}],
286
+ )
287
+ _write_peer_heartbeat(
288
+ home, "deployer",
289
+ capabilities=[{"name": "deployment", "enabled": True}],
290
+ )
291
+
292
+ reviewers = beacon.find_capable("code-review")
293
+ assert len(reviewers) == 1
294
+ assert reviewers[0].agent_name == "reviewer"
295
+
296
+ def test_find_capable_none(self, beacon: HeartbeatBeacon) -> None:
297
+ """No peers with capability returns empty."""
298
+ assert beacon.find_capable("nonexistent") == []
299
+
300
+
301
+ # ---------------------------------------------------------------------------
302
+ # Mark offline
303
+ # ---------------------------------------------------------------------------
304
+
305
+
306
+ class TestMarkOffline:
307
+ """Tests for offline marking."""
308
+
309
+ def test_mark_offline(self, beacon: HeartbeatBeacon) -> None:
310
+ """Mark offline publishes offline status."""
311
+ beacon.mark_offline()
312
+ hb = beacon.read_heartbeat("opus")
313
+ assert hb is not None
314
+ assert hb.status == "offline"
315
+
316
+
317
+ # ---------------------------------------------------------------------------
318
+ # Model tests
319
+ # ---------------------------------------------------------------------------
320
+
321
+
322
+ class TestModels:
323
+ """Tests for heartbeat models."""
324
+
325
+ def test_heartbeat_defaults(self) -> None:
326
+ """Heartbeat has sensible defaults."""
327
+ hb = Heartbeat(agent_name="test")
328
+ assert hb.status == "alive"
329
+ assert hb.is_alive is True
330
+ assert hb.ttl_seconds == 300
331
+
332
+ def test_heartbeat_expired(self) -> None:
333
+ """Expired heartbeat detected."""
334
+ hb = Heartbeat(
335
+ agent_name="old",
336
+ timestamp=datetime.now(timezone.utc) - timedelta(hours=1),
337
+ ttl_seconds=60,
338
+ )
339
+ assert hb.is_alive is False
340
+
341
+ def test_node_capacity_defaults(self) -> None:
342
+ """NodeCapacity has sensible defaults."""
343
+ cap = NodeCapacity()
344
+ assert cap.cpu_count == 0
345
+ assert cap.gpu_available is False
346
+
347
+ def test_peer_info_defaults(self) -> None:
348
+ """PeerInfo has sensible defaults."""
349
+ p = PeerInfo(
350
+ agent_name="test", status="alive",
351
+ alive=True, age_seconds=10,
352
+ )
353
+ assert p.capabilities == []
354
+ assert p.claimed_tasks == 0
@@ -0,0 +1,195 @@
1
+ """Tests for skcapstone.housekeeping — storage pruning."""
2
+
3
+ import time
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from skcapstone.housekeeping import (
9
+ prune_acks,
10
+ prune_comms_outbox,
11
+ prune_seeds,
12
+ run_housekeeping,
13
+ )
14
+
15
+
16
+ @pytest.fixture
17
+ def skcomm_home(tmp_path):
18
+ """Create a mock ~/.skcomm directory with test ACK files."""
19
+ acks_dir = tmp_path / "acks"
20
+ acks_dir.mkdir()
21
+ return tmp_path
22
+
23
+
24
+ @pytest.fixture
25
+ def skcapstone_home(tmp_path):
26
+ """Create a mock ~/.skcapstone directory with sync structure."""
27
+ sync_dir = tmp_path / "sync"
28
+ comms_out = sync_dir / "comms" / "outbox"
29
+ seed_out = sync_dir / "sync" / "outbox"
30
+ comms_out.mkdir(parents=True)
31
+ seed_out.mkdir(parents=True)
32
+ return tmp_path
33
+
34
+
35
+ class TestPruneAcks:
36
+ """Tests for prune_acks."""
37
+
38
+ def test_no_acks_dir(self, tmp_path):
39
+ """Returns 0 when acks directory doesn't exist."""
40
+ assert prune_acks(tmp_path) == 0
41
+
42
+ def test_empty_acks_dir(self, skcomm_home):
43
+ """Returns 0 when acks directory is empty."""
44
+ assert prune_acks(skcomm_home) == 0
45
+
46
+ def test_deletes_old_acks(self, skcomm_home):
47
+ """Deletes ACK files older than max_age_hours."""
48
+ acks_dir = skcomm_home / "acks"
49
+ # Create 5 old files (mtime set to 48h ago)
50
+ old_time = time.time() - (48 * 3600)
51
+ for i in range(5):
52
+ f = acks_dir / f"ack-{i}.json"
53
+ f.write_text("{}")
54
+ import os
55
+ os.utime(f, (old_time, old_time))
56
+
57
+ # Create 3 fresh files
58
+ for i in range(3):
59
+ f = acks_dir / f"fresh-{i}.json"
60
+ f.write_text("{}")
61
+
62
+ deleted = prune_acks(skcomm_home, max_age_hours=24)
63
+ assert deleted == 5
64
+ remaining = list(acks_dir.iterdir())
65
+ assert len(remaining) == 3
66
+
67
+ def test_respects_max_age(self, skcomm_home):
68
+ """Only deletes files older than the specified max_age."""
69
+ acks_dir = skcomm_home / "acks"
70
+ # File 1h old
71
+ f = acks_dir / "recent.json"
72
+ f.write_text("{}")
73
+ import os
74
+ os.utime(f, (time.time() - 3600, time.time() - 3600))
75
+
76
+ assert prune_acks(skcomm_home, max_age_hours=2) == 0
77
+ assert prune_acks(skcomm_home, max_age_hours=0) == 1
78
+
79
+
80
+ class TestPruneCommsOutbox:
81
+ """Tests for prune_comms_outbox."""
82
+
83
+ def test_no_outbox_dir(self, tmp_path):
84
+ """Returns 0 when outbox directory doesn't exist."""
85
+ assert prune_comms_outbox(tmp_path) == 0
86
+
87
+ def test_empty_outbox(self, skcapstone_home):
88
+ """Returns 0 when outbox is empty."""
89
+ assert prune_comms_outbox(skcapstone_home / "sync") == 0
90
+
91
+ def test_deletes_old_envelopes(self, skcapstone_home):
92
+ """Deletes envelope files older than max_age_hours."""
93
+ agent_dir = skcapstone_home / "sync" / "comms" / "outbox" / "lumina"
94
+ agent_dir.mkdir(parents=True)
95
+
96
+ old_time = time.time() - (72 * 3600)
97
+ for i in range(4):
98
+ f = agent_dir / f"env-{i}.json"
99
+ f.write_text("{}")
100
+ import os
101
+ os.utime(f, (old_time, old_time))
102
+
103
+ f = agent_dir / "fresh.json"
104
+ f.write_text("{}")
105
+
106
+ deleted = prune_comms_outbox(skcapstone_home / "sync", max_age_hours=48)
107
+ assert deleted == 4
108
+ assert (agent_dir / "fresh.json").exists()
109
+
110
+
111
+ class TestPruneSeeds:
112
+ """Tests for prune_seeds."""
113
+
114
+ def test_no_outbox_dir(self, tmp_path):
115
+ """Returns 0 when seed outbox doesn't exist."""
116
+ assert prune_seeds(tmp_path / "nonexistent") == 0
117
+
118
+ def test_keeps_recent_seeds(self, skcapstone_home):
119
+ """Keeps only keep_per_agent most recent seeds."""
120
+ seed_dir = skcapstone_home / "sync" / "sync" / "outbox"
121
+
122
+ # Create 15 seeds for agent "opus"
123
+ for i in range(15):
124
+ f = seed_dir / f"opus-170900000{i:01d}.json.gpg"
125
+ f.write_text("{}")
126
+ import os
127
+ os.utime(f, (time.time() - (15 - i) * 300, time.time() - (15 - i) * 300))
128
+
129
+ deleted = prune_seeds(seed_dir, keep_per_agent=10)
130
+ assert deleted == 5
131
+ remaining = list(seed_dir.iterdir())
132
+ assert len(remaining) == 10
133
+
134
+ def test_handles_multiple_agents(self, skcapstone_home):
135
+ """Keeps seeds per-agent, not globally."""
136
+ seed_dir = skcapstone_home / "sync" / "sync" / "outbox"
137
+
138
+ for agent in ("opus", "lumina"):
139
+ for i in range(5):
140
+ f = seed_dir / f"{agent}-170900000{i}.json"
141
+ f.write_text("{}")
142
+
143
+ deleted = prune_seeds(seed_dir, keep_per_agent=3)
144
+ assert deleted == 4 # 2 excess per agent
145
+
146
+ def test_empty_outbox(self, skcapstone_home):
147
+ """Returns 0 when seed outbox is empty."""
148
+ seed_dir = skcapstone_home / "sync" / "sync" / "outbox"
149
+ assert prune_seeds(seed_dir) == 0
150
+
151
+
152
+ class TestRunHousekeeping:
153
+ """Tests for run_housekeeping."""
154
+
155
+ def test_dry_run(self, tmp_path):
156
+ """Dry run reports counts without deleting."""
157
+ # Set up dirs
158
+ acks_dir = tmp_path / "skcomm" / "acks"
159
+ acks_dir.mkdir(parents=True)
160
+ for i in range(3):
161
+ f = acks_dir / f"old-{i}.json"
162
+ f.write_text("{}")
163
+ import os
164
+ os.utime(f, (time.time() - 48 * 3600, time.time() - 48 * 3600))
165
+
166
+ results = run_housekeeping(
167
+ skcapstone_home=tmp_path / "skcapstone",
168
+ skcomm_home=tmp_path / "skcomm",
169
+ dry_run=True,
170
+ )
171
+
172
+ assert results.get("dry_run") is True
173
+ assert results["acks"]["would_delete"] == 3
174
+ # Files should still exist
175
+ assert len(list(acks_dir.iterdir())) == 3
176
+
177
+ def test_full_run(self, tmp_path):
178
+ """Full run deletes files and reports summary."""
179
+ acks_dir = tmp_path / "skcomm" / "acks"
180
+ acks_dir.mkdir(parents=True)
181
+ for i in range(2):
182
+ f = acks_dir / f"old-{i}.json"
183
+ f.write_text("{}")
184
+ import os
185
+ os.utime(f, (time.time() - 48 * 3600, time.time() - 48 * 3600))
186
+
187
+ results = run_housekeeping(
188
+ skcapstone_home=tmp_path / "skcapstone",
189
+ skcomm_home=tmp_path / "skcomm",
190
+ dry_run=False,
191
+ )
192
+
193
+ assert "summary" in results
194
+ assert results["acks"]["deleted"] == 2
195
+ assert len(list(acks_dir.iterdir())) == 0