@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,519 @@
1
+ """Tests for the crush shim daemon.
2
+
3
+ Covers:
4
+ - CLI argument parsing
5
+ - Session and crush config loading
6
+ - System prompt construction
7
+ - Daemon loop (inbox polling, claude dispatch, state writing)
8
+ - Graceful shutdown via SIGTERM
9
+ - Health beacon / heartbeat writing
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import signal
17
+ from pathlib import Path
18
+ from typing import Any, Dict
19
+ from unittest.mock import MagicMock, patch
20
+
21
+ import pytest
22
+
23
+ from skcapstone.crush_shim import (
24
+ build_arg_parser,
25
+ build_system_prompt,
26
+ daemon_loop,
27
+ dispatch_to_claude,
28
+ load_crush_config,
29
+ load_session_config,
30
+ parse_args,
31
+ poll_inbox,
32
+ write_outbox,
33
+ write_state,
34
+ )
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Fixtures
39
+ # ---------------------------------------------------------------------------
40
+
41
+
42
+ @pytest.fixture()
43
+ def session_config(tmp_path: Path) -> Dict[str, Any]:
44
+ """Build and write a minimal session.json, return the parsed dict."""
45
+ config = {
46
+ "agent_name": "test-agent",
47
+ "team_name": "test-team",
48
+ "role": "worker",
49
+ "model": "fast",
50
+ "model_tier": "fast",
51
+ "soul_blueprint": None,
52
+ "skills": [],
53
+ "memory_dir": str(tmp_path / "memory"),
54
+ "scratch_dir": str(tmp_path / "scratch"),
55
+ "state_file": str(tmp_path / "session_state.json"),
56
+ "env": {},
57
+ }
58
+ (tmp_path / "session.json").write_text(json.dumps(config), encoding="utf-8")
59
+ return config
60
+
61
+
62
+ @pytest.fixture()
63
+ def crush_config(tmp_path: Path) -> Dict[str, Any]:
64
+ """Build and write a minimal crush.json, return the parsed dict."""
65
+ config = {
66
+ "$schema": "https://charm.land/crush.json",
67
+ "options": {
68
+ "context_paths": [],
69
+ "debug": False,
70
+ },
71
+ "permissions": {
72
+ "allowed_tools": ["view", "ls"],
73
+ },
74
+ "session": {
75
+ "agent_name": "test-agent",
76
+ "model": "fast",
77
+ "role": "worker",
78
+ "skills": [],
79
+ },
80
+ }
81
+ (tmp_path / "crush.json").write_text(json.dumps(config), encoding="utf-8")
82
+ return config
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Argument parsing
87
+ # ---------------------------------------------------------------------------
88
+
89
+
90
+ class TestArgParsing:
91
+ """Tests for crush CLI argument parsing."""
92
+
93
+ def test_parse_run_with_all_flags(self, tmp_path):
94
+ args = parse_args([
95
+ "run",
96
+ "--session", str(tmp_path / "session.json"),
97
+ "--config", str(tmp_path / "crush.json"),
98
+ "--headless",
99
+ "--state-file", str(tmp_path / "state.json"),
100
+ ])
101
+ assert args.command == "run"
102
+ assert args.session == str(tmp_path / "session.json")
103
+ assert args.config == str(tmp_path / "crush.json")
104
+ assert args.headless is True
105
+ assert args.state_file == str(tmp_path / "state.json")
106
+
107
+ def test_parse_run_without_headless(self, tmp_path):
108
+ args = parse_args([
109
+ "run",
110
+ "--session", str(tmp_path / "session.json"),
111
+ "--config", str(tmp_path / "crush.json"),
112
+ "--state-file", str(tmp_path / "state.json"),
113
+ ])
114
+ assert args.headless is False
115
+
116
+ def test_parse_run_requires_session(self, tmp_path):
117
+ with pytest.raises(SystemExit):
118
+ parse_args([
119
+ "run",
120
+ "--config", str(tmp_path / "crush.json"),
121
+ "--state-file", str(tmp_path / "state.json"),
122
+ ])
123
+
124
+ def test_parse_run_requires_config(self, tmp_path):
125
+ with pytest.raises(SystemExit):
126
+ parse_args([
127
+ "run",
128
+ "--session", str(tmp_path / "session.json"),
129
+ "--state-file", str(tmp_path / "state.json"),
130
+ ])
131
+
132
+ def test_parse_run_requires_state_file(self, tmp_path):
133
+ with pytest.raises(SystemExit):
134
+ parse_args([
135
+ "run",
136
+ "--session", str(tmp_path / "session.json"),
137
+ "--config", str(tmp_path / "crush.json"),
138
+ ])
139
+
140
+ def test_build_arg_parser_returns_parser(self):
141
+ parser = build_arg_parser()
142
+ assert parser is not None
143
+ assert parser.prog == "crush"
144
+
145
+
146
+ # ---------------------------------------------------------------------------
147
+ # Config loading
148
+ # ---------------------------------------------------------------------------
149
+
150
+
151
+ class TestSessionLoading:
152
+ """Tests for session.json and crush.json loading."""
153
+
154
+ def test_load_session_config(self, tmp_path, session_config):
155
+ loaded = load_session_config(str(tmp_path / "session.json"))
156
+ assert loaded["agent_name"] == "test-agent"
157
+ assert loaded["team_name"] == "test-team"
158
+ assert loaded["model"] == "fast"
159
+
160
+ def test_load_session_config_missing_file(self, tmp_path):
161
+ with pytest.raises(SystemExit):
162
+ load_session_config(str(tmp_path / "nonexistent.json"))
163
+
164
+ def test_load_session_config_invalid_json(self, tmp_path):
165
+ (tmp_path / "bad.json").write_text("not json!", encoding="utf-8")
166
+ with pytest.raises(SystemExit):
167
+ load_session_config(str(tmp_path / "bad.json"))
168
+
169
+ def test_load_crush_config(self, tmp_path, crush_config):
170
+ loaded = load_crush_config(str(tmp_path / "crush.json"))
171
+ assert "$schema" in loaded
172
+ assert loaded["session"]["agent_name"] == "test-agent"
173
+
174
+ def test_load_crush_config_missing_file(self, tmp_path):
175
+ with pytest.raises(SystemExit):
176
+ load_crush_config(str(tmp_path / "nonexistent.json"))
177
+
178
+ def test_load_crush_config_invalid_json(self, tmp_path):
179
+ (tmp_path / "bad.json").write_text("{{{", encoding="utf-8")
180
+ with pytest.raises(SystemExit):
181
+ load_crush_config(str(tmp_path / "bad.json"))
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # System prompt building
186
+ # ---------------------------------------------------------------------------
187
+
188
+
189
+ class TestBuildSystemPrompt:
190
+ """Tests for build_system_prompt()."""
191
+
192
+ def test_includes_agent_name(self):
193
+ prompt = build_system_prompt({"agent_name": "opus", "role": "coder"})
194
+ assert "opus" in prompt
195
+
196
+ def test_includes_role(self):
197
+ prompt = build_system_prompt({"agent_name": "a", "role": "researcher"})
198
+ assert "researcher" in prompt
199
+
200
+ def test_reads_soul_blueprint_file(self, tmp_path):
201
+ soul_file = tmp_path / "soul.md"
202
+ soul_file.write_text("You are a sovereign agent.")
203
+ config = {"agent_name": "a", "soul_blueprint": str(soul_file)}
204
+ prompt = build_system_prompt(config)
205
+ assert "sovereign agent" in prompt
206
+
207
+ def test_reads_soul_blueprint_directory(self, tmp_path):
208
+ soul_dir = tmp_path / "lumina"
209
+ soul_dir.mkdir()
210
+ (soul_dir / "identity.md").write_text("Identity: Lumina")
211
+ config = {"agent_name": "a", "soul_blueprint": str(soul_dir)}
212
+ prompt = build_system_prompt(config)
213
+ assert "Lumina" in prompt
214
+
215
+ def test_handles_missing_soul_blueprint(self):
216
+ prompt = build_system_prompt({"agent_name": "a", "soul_blueprint": "/nonexistent/path"})
217
+ assert "Agent: a" in prompt
218
+
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # Claude dispatch
222
+ # ---------------------------------------------------------------------------
223
+
224
+
225
+ class TestDispatchToClaude:
226
+ """Tests for dispatch_to_claude()."""
227
+
228
+ def test_calls_claude_binary(self):
229
+ with patch("subprocess.run") as mock_run:
230
+ mock_run.return_value = MagicMock(
231
+ returncode=0, stdout="Hello!", stderr=""
232
+ )
233
+ result = dispatch_to_claude(
234
+ "Hello", "fast", "system prompt", "/bin/claude"
235
+ )
236
+ assert result == "Hello!"
237
+ cmd = mock_run.call_args[0][0]
238
+ assert cmd[0] == "/bin/claude"
239
+ assert "-p" in cmd
240
+
241
+ def test_passes_model_flag(self):
242
+ with patch("subprocess.run") as mock_run:
243
+ mock_run.return_value = MagicMock(
244
+ returncode=0, stdout="ok", stderr=""
245
+ )
246
+ dispatch_to_claude("test", "claude-opus-4-6", "sp", "/bin/claude")
247
+ cmd = mock_run.call_args[0][0]
248
+ model_idx = cmd.index("--model")
249
+ assert cmd[model_idx + 1] == "claude-opus-4-6"
250
+
251
+ def test_returns_none_on_nonzero_exit(self):
252
+ with patch("subprocess.run") as mock_run:
253
+ mock_run.return_value = MagicMock(
254
+ returncode=1, stdout="", stderr="error"
255
+ )
256
+ result = dispatch_to_claude("test", "fast", "sp")
257
+ assert result is None
258
+
259
+ def test_returns_none_on_timeout(self):
260
+ import subprocess as sp
261
+
262
+ with patch("subprocess.run", side_effect=sp.TimeoutExpired("claude", 300)):
263
+ result = dispatch_to_claude("test", "fast", "sp")
264
+ assert result is None
265
+
266
+ def test_returns_none_on_oserror(self):
267
+ with patch("subprocess.run", side_effect=OSError("not found")):
268
+ result = dispatch_to_claude("test", "fast", "sp")
269
+ assert result is None
270
+
271
+
272
+ # ---------------------------------------------------------------------------
273
+ # Inbox / outbox
274
+ # ---------------------------------------------------------------------------
275
+
276
+
277
+ class TestInboxOutbox:
278
+ """Tests for poll_inbox and write_outbox."""
279
+
280
+ def test_poll_inbox_empty_when_no_dir(self, tmp_path):
281
+ with patch(
282
+ "skcapstone.crush_shim._comms_root", return_value=tmp_path / "comms"
283
+ ):
284
+ msgs = poll_inbox("team", "agent")
285
+ assert msgs == []
286
+
287
+ def test_poll_inbox_returns_files(self, tmp_path):
288
+ inbox = tmp_path / "comms" / "team" / "agent" / "inbox"
289
+ inbox.mkdir(parents=True)
290
+ (inbox / "msg1.json").write_text('{"task": "do stuff"}')
291
+ (inbox / "msg2.json").write_text('{"task": "do more"}')
292
+
293
+ with patch("skcapstone.crush_shim._comms_root", return_value=tmp_path / "comms"):
294
+ msgs = poll_inbox("team", "agent")
295
+
296
+ assert len(msgs) == 2
297
+
298
+ def test_write_outbox_creates_file(self, tmp_path):
299
+ with patch("skcapstone.crush_shim._comms_root", return_value=tmp_path / "comms"):
300
+ write_outbox("team", "agent", {"response": "done"})
301
+
302
+ outbox = tmp_path / "comms" / "team" / "agent" / "outbox"
303
+ assert outbox.is_dir()
304
+ files = list(outbox.iterdir())
305
+ assert len(files) == 1
306
+ data = json.loads(files[0].read_text())
307
+ assert data["response"] == "done"
308
+
309
+
310
+ # ---------------------------------------------------------------------------
311
+ # State file writing
312
+ # ---------------------------------------------------------------------------
313
+
314
+
315
+ class TestStateWriting:
316
+ """Tests for write_state()."""
317
+
318
+ def test_writes_state_file(self, tmp_path):
319
+ state_file = str(tmp_path / "state.json")
320
+ write_state(state_file, {"status": "running", "pid": 1234})
321
+ data = json.loads(Path(state_file).read_text())
322
+ assert data["status"] == "running"
323
+ assert data["pid"] == 1234
324
+
325
+ def test_overwrites_existing_state(self, tmp_path):
326
+ state_file = str(tmp_path / "state.json")
327
+ write_state(state_file, {"status": "running"})
328
+ write_state(state_file, {"status": "stopped"})
329
+ data = json.loads(Path(state_file).read_text())
330
+ assert data["status"] == "stopped"
331
+
332
+
333
+ # ---------------------------------------------------------------------------
334
+ # Daemon loop
335
+ # ---------------------------------------------------------------------------
336
+
337
+
338
+ class TestDaemonLoop:
339
+ """Tests for the daemon loop: polls inbox, calls claude, writes state."""
340
+
341
+ def test_loop_runs_and_writes_heartbeat(self, tmp_path, session_config, crush_config):
342
+ import skcapstone.crush_shim as shim
343
+
344
+ state_file = str(tmp_path / "daemon_state.json")
345
+
346
+ # Stop after one iteration
347
+ call_count = 0
348
+ original_running = True
349
+
350
+ def fake_sleep(duration):
351
+ nonlocal call_count
352
+ call_count += 1
353
+ if call_count >= 2:
354
+ shim._running = False
355
+
356
+ with patch("time.sleep", side_effect=fake_sleep):
357
+ with patch(
358
+ "skcapstone.crush_shim.poll_inbox", return_value=[]
359
+ ):
360
+ shim._running = True
361
+ daemon_loop(session_config, crush_config, state_file)
362
+
363
+ data = json.loads(Path(state_file).read_text())
364
+ assert data["status"] == "running"
365
+ assert data["agent_name"] == "test-agent"
366
+ assert "heartbeat" in data
367
+ assert "iteration" in data
368
+
369
+ def test_loop_processes_inbox_message(self, tmp_path, session_config, crush_config):
370
+ import skcapstone.crush_shim as shim
371
+
372
+ state_file = str(tmp_path / "daemon_state.json")
373
+
374
+ # Create a fake inbox message
375
+ msg_file = tmp_path / "msg.json"
376
+ msg_file.write_text(json.dumps({"prompt": "What is 2+2?"}))
377
+
378
+ call_count = 0
379
+
380
+ def fake_sleep(duration):
381
+ nonlocal call_count
382
+ call_count += 1
383
+ if call_count >= 2:
384
+ shim._running = False
385
+
386
+ # First call returns msg, second returns empty
387
+ inbox_calls = iter([[msg_file], []])
388
+
389
+ with patch("time.sleep", side_effect=fake_sleep):
390
+ with patch(
391
+ "skcapstone.crush_shim.poll_inbox",
392
+ side_effect=lambda *a: next(inbox_calls, []),
393
+ ):
394
+ with patch(
395
+ "skcapstone.crush_shim.dispatch_to_claude",
396
+ return_value="4",
397
+ ) as mock_dispatch:
398
+ with patch("skcapstone.crush_shim.write_outbox") as mock_outbox:
399
+ shim._running = True
400
+ daemon_loop(session_config, crush_config, state_file)
401
+
402
+ mock_dispatch.assert_called_once()
403
+ mock_outbox.assert_called_once()
404
+ outbox_msg = mock_outbox.call_args[0][2]
405
+ assert outbox_msg["response"] == "4"
406
+
407
+
408
+ # ---------------------------------------------------------------------------
409
+ # Graceful shutdown
410
+ # ---------------------------------------------------------------------------
411
+
412
+
413
+ class TestGracefulShutdown:
414
+ """Tests for SIGTERM → stopped state."""
415
+
416
+ def test_sigterm_sets_running_false(self):
417
+ import skcapstone.crush_shim as shim
418
+
419
+ shim._running = True
420
+ shim._handle_signal(signal.SIGTERM, None)
421
+ assert shim._running is False
422
+
423
+ def test_sigint_sets_running_false(self):
424
+ import skcapstone.crush_shim as shim
425
+
426
+ shim._running = True
427
+ shim._handle_signal(signal.SIGINT, None)
428
+ assert shim._running is False
429
+
430
+ def test_daemon_writes_stopped_on_exit(self, tmp_path, session_config, crush_config):
431
+ """Verify the main() flow writes stopped state after loop exits."""
432
+ import skcapstone.crush_shim as shim
433
+
434
+ state_file = str(tmp_path / "exit_state.json")
435
+
436
+ # Immediately stop
437
+ shim._running = False
438
+
439
+ with patch("skcapstone.crush_shim.poll_inbox", return_value=[]):
440
+ daemon_loop(session_config, crush_config, state_file)
441
+
442
+ # The daemon_loop itself writes running state each iteration,
443
+ # but since _running is False at entry, it exits without writing.
444
+ # The caller (main) writes stopped state. Let's verify write_state works.
445
+ write_state(state_file, {
446
+ "status": "stopped",
447
+ "agent_name": "test-agent",
448
+ })
449
+ data = json.loads(Path(state_file).read_text())
450
+ assert data["status"] == "stopped"
451
+
452
+
453
+ # ---------------------------------------------------------------------------
454
+ # Health beacon
455
+ # ---------------------------------------------------------------------------
456
+
457
+
458
+ class TestHealthBeacon:
459
+ """Tests for heartbeat / state file updates each loop iteration."""
460
+
461
+ def test_state_file_updated_each_iteration(self, tmp_path, session_config, crush_config):
462
+ import skcapstone.crush_shim as shim
463
+
464
+ state_file = str(tmp_path / "beacon_state.json")
465
+
466
+ iterations_seen = []
467
+
468
+ original_write = write_state
469
+
470
+ def tracking_write(sf, state):
471
+ original_write(sf, state)
472
+ if "iteration" in state:
473
+ iterations_seen.append(state["iteration"])
474
+
475
+ call_count = 0
476
+
477
+ def fake_sleep(duration):
478
+ nonlocal call_count
479
+ call_count += 1
480
+ if call_count >= 6: # 3 iterations * 2 sleeps per iteration (approx)
481
+ shim._running = False
482
+
483
+ with patch("time.sleep", side_effect=fake_sleep):
484
+ with patch("skcapstone.crush_shim.poll_inbox", return_value=[]):
485
+ with patch(
486
+ "skcapstone.crush_shim.write_state",
487
+ side_effect=tracking_write,
488
+ ):
489
+ shim._running = True
490
+ daemon_loop(session_config, crush_config, state_file)
491
+
492
+ # Should have seen multiple iterations
493
+ assert len(iterations_seen) >= 1
494
+ # Iterations should be sequential
495
+ for i, val in enumerate(iterations_seen):
496
+ assert val == i + 1
497
+
498
+ def test_heartbeat_has_timestamp(self, tmp_path, session_config, crush_config):
499
+ import skcapstone.crush_shim as shim
500
+
501
+ state_file = str(tmp_path / "hb_state.json")
502
+
503
+ call_count = 0
504
+
505
+ def fake_sleep(duration):
506
+ nonlocal call_count
507
+ call_count += 1
508
+ if call_count >= 2:
509
+ shim._running = False
510
+
511
+ with patch("time.sleep", side_effect=fake_sleep):
512
+ with patch("skcapstone.crush_shim.poll_inbox", return_value=[]):
513
+ shim._running = True
514
+ daemon_loop(session_config, crush_config, state_file)
515
+
516
+ data = json.loads(Path(state_file).read_text())
517
+ assert "heartbeat" in data
518
+ # ISO timestamp format check
519
+ assert "T" in data["heartbeat"]