@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,832 @@
1
+ """Sovereign metrics collector -- unified stats across all packages.
2
+
3
+ Aggregates runtime statistics from every component of the sovereign
4
+ stack into a single JSON-serializable report. Designed for dashboard
5
+ consumption, health monitoring, and debugging.
6
+
7
+ No external dependencies. Gracefully handles missing packages.
8
+
9
+ Usage:
10
+ collector = MetricsCollector(home=Path("~/.skcapstone"))
11
+ report = collector.collect()
12
+ print(report.model_dump_json(indent=2))
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import logging
19
+ import os
20
+ import threading
21
+ import time
22
+ from datetime import datetime, timezone
23
+ from pathlib import Path
24
+ from typing import Any, Optional
25
+
26
+ from pydantic import BaseModel, Field
27
+
28
+ from . import AGENT_HOME, __version__
29
+
30
+ logger = logging.getLogger("skcapstone.metrics")
31
+
32
+
33
+ class IdentityMetrics(BaseModel):
34
+ """CapAuth identity stats."""
35
+
36
+ available: bool = False
37
+ fingerprint: str = ""
38
+ entity_type: str = ""
39
+ name: str = ""
40
+
41
+
42
+ class MemoryMetrics(BaseModel):
43
+ """SKMemory stats."""
44
+
45
+ available: bool = False
46
+ total_memories: int = 0
47
+ short_term: int = 0
48
+ mid_term: int = 0
49
+ long_term: int = 0
50
+ store_size_bytes: int = 0
51
+
52
+
53
+ class ChatMetrics(BaseModel):
54
+ """SKChat stats."""
55
+
56
+ available: bool = False
57
+ total_messages: int = 0
58
+ total_threads: int = 0
59
+
60
+
61
+ class TransportMetrics(BaseModel):
62
+ """SKComm transport stats."""
63
+
64
+ available: bool = False
65
+ transport_count: int = 0
66
+ outbox_pending: int = 0
67
+ outbox_dead: int = 0
68
+
69
+
70
+ class CoordinationMetrics(BaseModel):
71
+ """Coordination board stats."""
72
+
73
+ total_tasks: int = 0
74
+ done: int = 0
75
+ open: int = 0
76
+ in_progress: int = 0
77
+ claimed: int = 0
78
+
79
+
80
+ class TrustMetrics(BaseModel):
81
+ """Cloud 9 trust stats."""
82
+
83
+ available: bool = False
84
+ depth: float = 0.0
85
+ trust_level: float = 0.0
86
+ love_intensity: float = 0.0
87
+ entangled: bool = False
88
+ feb_count: int = 0
89
+ last_rehydration: str = ""
90
+
91
+
92
+ class SecurityMetrics(BaseModel):
93
+ """Security audit stats."""
94
+
95
+ available: bool = False
96
+ audit_entries: int = 0
97
+ tamper_alerts: int = 0
98
+ event_types: dict[str, int] = Field(default_factory=dict)
99
+
100
+
101
+ class SyncMetrics(BaseModel):
102
+ """Sync layer stats."""
103
+
104
+ available: bool = False
105
+ seeds_outbox: int = 0
106
+ seeds_inbox: int = 0
107
+ peers_known: int = 0
108
+ last_push: str = ""
109
+ last_pull: str = ""
110
+
111
+
112
+ class PubSubMetrics(BaseModel):
113
+ """Pub/sub messaging stats."""
114
+
115
+ available: bool = False
116
+ topics: int = 0
117
+ messages: int = 0
118
+ subscriptions: int = 0
119
+
120
+
121
+ class KmsMetrics(BaseModel):
122
+ """KMS key management stats."""
123
+
124
+ available: bool = False
125
+ total_keys: int = 0
126
+ active_keys: int = 0
127
+ by_type: dict[str, int] = Field(default_factory=dict)
128
+ rotations: int = 0
129
+
130
+
131
+ class FortressMetrics(BaseModel):
132
+ """Memory fortress stats."""
133
+
134
+ enabled: bool = False
135
+ encryption_enabled: bool = False
136
+ seal_algorithm: str = ""
137
+
138
+
139
+ class BackupMetrics(BaseModel):
140
+ """Backup stats."""
141
+
142
+ backup_count: int = 0
143
+ latest_backup: str = ""
144
+ latest_size_bytes: int = 0
145
+
146
+
147
+ class MetricsReport(BaseModel):
148
+ """Complete sovereign agent metrics report.
149
+
150
+ JSON-serializable snapshot of the entire stack's state.
151
+ """
152
+
153
+ collected_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
154
+ agent_name: str = ""
155
+ version: str = __version__
156
+ home: str = ""
157
+ uptime_seconds: float = 0.0
158
+
159
+ identity: IdentityMetrics = Field(default_factory=IdentityMetrics)
160
+ memory: MemoryMetrics = Field(default_factory=MemoryMetrics)
161
+ trust: TrustMetrics = Field(default_factory=TrustMetrics)
162
+ security: SecurityMetrics = Field(default_factory=SecurityMetrics)
163
+ chat: ChatMetrics = Field(default_factory=ChatMetrics)
164
+ transport: TransportMetrics = Field(default_factory=TransportMetrics)
165
+ sync: SyncMetrics = Field(default_factory=SyncMetrics)
166
+ coordination: CoordinationMetrics = Field(default_factory=CoordinationMetrics)
167
+ pubsub: PubSubMetrics = Field(default_factory=PubSubMetrics)
168
+ kms: KmsMetrics = Field(default_factory=KmsMetrics)
169
+ fortress: FortressMetrics = Field(default_factory=FortressMetrics)
170
+ backup: BackupMetrics = Field(default_factory=BackupMetrics)
171
+
172
+ collection_time_ms: float = 0.0
173
+ errors: list[str] = Field(default_factory=list)
174
+
175
+ def summary(self) -> str:
176
+ """One-line summary for logging.
177
+
178
+ Returns:
179
+ str: Compact status line.
180
+ """
181
+ parts = [
182
+ f"id={'yes' if self.identity.available else 'no'}",
183
+ f"mem={self.memory.total_memories}",
184
+ f"trust={self.trust.depth:.0f}",
185
+ f"keys={self.kms.active_keys}",
186
+ f"chat={self.chat.total_messages}",
187
+ f"board={self.coordination.done}/{self.coordination.total_tasks}",
188
+ f"topics={self.pubsub.topics}",
189
+ f"fortress={'on' if self.fortress.enabled else 'off'}",
190
+ ]
191
+ return f"[{self.agent_name}] " + " | ".join(parts)
192
+
193
+
194
+ class MetricsCollector:
195
+ """Collects metrics from all sovereign stack components.
196
+
197
+ Each subsystem is queried independently. Missing packages
198
+ are skipped gracefully -- no ImportErrors propagate.
199
+
200
+ Args:
201
+ home: Agent home directory.
202
+ """
203
+
204
+ def __init__(self, home: Optional[Path] = None) -> None:
205
+ self._home = (home or Path(AGENT_HOME)).expanduser()
206
+ self._start_time = time.monotonic()
207
+
208
+ def collect(self) -> MetricsReport:
209
+ """Collect a full metrics snapshot.
210
+
211
+ Returns:
212
+ MetricsReport: Complete report from all subsystems.
213
+ """
214
+ start = time.monotonic()
215
+ report = MetricsReport(
216
+ home=str(self._home),
217
+ uptime_seconds=time.monotonic() - self._start_time,
218
+ )
219
+
220
+ report.agent_name = self._read_agent_name()
221
+ self._collect_identity(report)
222
+ self._collect_memory(report)
223
+ self._collect_trust(report)
224
+ self._collect_security(report)
225
+ self._collect_chat(report)
226
+ self._collect_transport(report)
227
+ self._collect_sync(report)
228
+ self._collect_coordination(report)
229
+ self._collect_pubsub(report)
230
+ self._collect_kms(report)
231
+ self._collect_fortress(report)
232
+ self._collect_backup(report)
233
+
234
+ report.collection_time_ms = (time.monotonic() - start) * 1000
235
+ return report
236
+
237
+ def _read_agent_name(self) -> str:
238
+ """Read agent name from manifest or config."""
239
+ for filename in ("manifest.json", "config/config.yaml"):
240
+ fp = self._home / filename
241
+ if fp.exists():
242
+ try:
243
+ data = json.loads(fp.read_text(encoding="utf-8"))
244
+ return data.get("name") or data.get("agent_name") or ""
245
+ except Exception:
246
+ continue
247
+ return "unknown"
248
+
249
+ def _collect_identity(self, report: MetricsReport) -> None:
250
+ """Collect CapAuth identity metrics."""
251
+ try:
252
+ capauth_dir = self._home.parent / ".capauth"
253
+ if not capauth_dir.exists():
254
+ capauth_dir = Path.home() / ".capauth"
255
+
256
+ profile_path = capauth_dir / "identity" / "profile.json"
257
+ if profile_path.exists():
258
+ data = json.loads(profile_path.read_text(encoding="utf-8"))
259
+ entity = data.get("entity", {})
260
+ key_info = data.get("key_info", {})
261
+ report.identity = IdentityMetrics(
262
+ available=True,
263
+ fingerprint=key_info.get("fingerprint", "")[:16],
264
+ entity_type=entity.get("entity_type", ""),
265
+ name=entity.get("name", ""),
266
+ )
267
+ except Exception as exc:
268
+ report.errors.append(f"identity: {exc}")
269
+
270
+ def _collect_memory(self, report: MetricsReport) -> None:
271
+ """Collect SKMemory metrics."""
272
+ try:
273
+ from skmemory import MemoryStore, SQLiteBackend
274
+ from skmemory.models import MemoryLayer
275
+
276
+ mem_path = self._home / "memory"
277
+ if not mem_path.exists():
278
+ mem_path = Path.home() / ".skcapstone"
279
+
280
+ if not mem_path.exists():
281
+ return
282
+
283
+ backend = SQLiteBackend(base_path=str(mem_path))
284
+ store = MemoryStore(primary=backend)
285
+
286
+ all_mems = store.list_memories(limit=10000)
287
+ short = sum(1 for m in all_mems if m.layer == MemoryLayer.SHORT)
288
+ mid = sum(1 for m in all_mems if m.layer == MemoryLayer.MID)
289
+ long_ = sum(1 for m in all_mems if m.layer == MemoryLayer.LONG)
290
+
291
+ store_size = sum(
292
+ f.stat().st_size
293
+ for f in mem_path.rglob("*") if f.is_file()
294
+ )
295
+
296
+ report.memory = MemoryMetrics(
297
+ available=True,
298
+ total_memories=len(all_mems),
299
+ short_term=short,
300
+ mid_term=mid,
301
+ long_term=long_,
302
+ store_size_bytes=store_size,
303
+ )
304
+ except ImportError:
305
+ pass
306
+ except Exception as exc:
307
+ report.errors.append(f"memory: {exc}")
308
+
309
+ def _collect_chat(self, report: MetricsReport) -> None:
310
+ """Collect SKChat metrics."""
311
+ try:
312
+ from skmemory import MemoryStore, SQLiteBackend
313
+ from skchat.history import ChatHistory
314
+
315
+ mem_path = self._home / "memory"
316
+ if not mem_path.exists():
317
+ mem_path = Path.home() / ".skchat" / "memory"
318
+
319
+ if not mem_path.exists():
320
+ return
321
+
322
+ backend = SQLiteBackend(base_path=str(mem_path))
323
+ store = MemoryStore(primary=backend)
324
+ history = ChatHistory(store=store)
325
+
326
+ report.chat = ChatMetrics(
327
+ available=True,
328
+ total_messages=history.message_count(),
329
+ total_threads=len(history.list_threads()),
330
+ )
331
+ except ImportError:
332
+ pass
333
+ except Exception as exc:
334
+ report.errors.append(f"chat: {exc}")
335
+
336
+ def _collect_transport(self, report: MetricsReport) -> None:
337
+ """Collect SKComm transport metrics."""
338
+ try:
339
+ skcomm_dir = Path.home() / ".skcomm"
340
+ outbox_dir = skcomm_dir / "outbox"
341
+
342
+ pending = 0
343
+ dead = 0
344
+ if (outbox_dir / "pending").exists():
345
+ pending = len(list((outbox_dir / "pending").glob("*.json")))
346
+ if (outbox_dir / "dead").exists():
347
+ dead = len(list((outbox_dir / "dead").glob("*.json")))
348
+
349
+ config_path = skcomm_dir / "config.yml"
350
+ transport_count = 0
351
+ if config_path.exists():
352
+ try:
353
+ import yaml
354
+
355
+ cfg = yaml.safe_load(config_path.read_text(encoding="utf-8"))
356
+ transports = cfg.get("skcomm", {}).get("transports", {})
357
+ transport_count = sum(
358
+ 1 for t in transports.values()
359
+ if isinstance(t, dict) and t.get("enabled", True)
360
+ )
361
+ except Exception:
362
+ pass
363
+
364
+ report.transport = TransportMetrics(
365
+ available=True,
366
+ transport_count=transport_count,
367
+ outbox_pending=pending,
368
+ outbox_dead=dead,
369
+ )
370
+ except Exception as exc:
371
+ report.errors.append(f"transport: {exc}")
372
+
373
+ def _collect_coordination(self, report: MetricsReport) -> None:
374
+ """Collect coordination board metrics."""
375
+ try:
376
+ tasks_dir = self._home / "coordination" / "tasks"
377
+ if not tasks_dir.exists():
378
+ tasks_dir = Path.home() / ".skcapstone" / "coordination" / "tasks"
379
+
380
+ if not tasks_dir.exists():
381
+ return
382
+
383
+ counts: dict[str, int] = {"open": 0, "claimed": 0, "in_progress": 0, "done": 0}
384
+ total = 0
385
+
386
+ for f in tasks_dir.glob("*.json"):
387
+ try:
388
+ data = json.loads(f.read_text(encoding="utf-8"))
389
+ status = data.get("status", "open").lower()
390
+ if status in counts:
391
+ counts[status] += 1
392
+ total += 1
393
+ except Exception:
394
+ total += 1
395
+
396
+ report.coordination = CoordinationMetrics(
397
+ total_tasks=total,
398
+ done=counts["done"],
399
+ open=counts["open"],
400
+ in_progress=counts["in_progress"],
401
+ claimed=counts["claimed"],
402
+ )
403
+ except Exception as exc:
404
+ report.errors.append(f"coordination: {exc}")
405
+
406
+ def _collect_trust(self, report: MetricsReport) -> None:
407
+ """Collect Cloud 9 trust metrics."""
408
+ try:
409
+ trust_path = self._home / "trust" / "trust.json"
410
+ if not trust_path.exists():
411
+ return
412
+
413
+ data = json.loads(trust_path.read_text(encoding="utf-8"))
414
+ febs_dir = self._home / "trust" / "febs"
415
+ feb_count = sum(1 for _ in febs_dir.glob("*.feb")) if febs_dir.is_dir() else 0
416
+
417
+ report.trust = TrustMetrics(
418
+ available=True,
419
+ depth=data.get("depth", 0),
420
+ trust_level=data.get("trust_level", 0),
421
+ love_intensity=data.get("love_intensity", 0),
422
+ entangled=data.get("entangled", False),
423
+ feb_count=feb_count,
424
+ last_rehydration=data.get("last_rehydration", ""),
425
+ )
426
+ except Exception as exc:
427
+ report.errors.append(f"trust: {exc}")
428
+
429
+ def _collect_security(self, report: MetricsReport) -> None:
430
+ """Collect security audit metrics."""
431
+ try:
432
+ audit_log = self._home / "security" / "audit.log"
433
+ if not audit_log.exists():
434
+ return
435
+
436
+ lines = audit_log.read_text(encoding="utf-8").splitlines()
437
+ total = len([line for line in lines if line.strip()])
438
+
439
+ type_counts: dict[str, int] = {}
440
+ for line in lines:
441
+ line = line.strip()
442
+ if not line:
443
+ continue
444
+ try:
445
+ entry = json.loads(line)
446
+ et = entry.get("event_type", "UNKNOWN")
447
+ type_counts[et] = type_counts.get(et, 0) + 1
448
+ except json.JSONDecodeError:
449
+ type_counts["LEGACY"] = type_counts.get("LEGACY", 0) + 1
450
+
451
+ report.security = SecurityMetrics(
452
+ available=True,
453
+ audit_entries=total,
454
+ tamper_alerts=type_counts.get("MEMORY_TAMPER_ALERT", 0),
455
+ event_types=type_counts,
456
+ )
457
+ except Exception as exc:
458
+ report.errors.append(f"security: {exc}")
459
+
460
+ def _collect_sync(self, report: MetricsReport) -> None:
461
+ """Collect sync layer metrics."""
462
+ try:
463
+ sync_dir = self._home / "sync"
464
+ if not sync_dir.is_dir():
465
+ return
466
+
467
+ outbox = sync_dir / "outbox"
468
+ inbox = sync_dir / "inbox"
469
+ seeds_out = sum(1 for _ in outbox.glob("*")) if outbox.is_dir() else 0
470
+ seeds_in = sum(1 for _ in inbox.glob("*")) if inbox.is_dir() else 0
471
+
472
+ state_path = sync_dir / "sync_state.json"
473
+ state: dict[str, Any] = {}
474
+ if state_path.exists():
475
+ try:
476
+ state = json.loads(state_path.read_text(encoding="utf-8"))
477
+ except Exception:
478
+ pass
479
+
480
+ report.sync = SyncMetrics(
481
+ available=True,
482
+ seeds_outbox=seeds_out,
483
+ seeds_inbox=seeds_in,
484
+ peers_known=state.get("peers_known", 0),
485
+ last_push=state.get("last_push", ""),
486
+ last_pull=state.get("last_pull", ""),
487
+ )
488
+ except Exception as exc:
489
+ report.errors.append(f"sync: {exc}")
490
+
491
+ def _collect_pubsub(self, report: MetricsReport) -> None:
492
+ """Collect pub/sub messaging metrics."""
493
+ try:
494
+ pubsub_dir = self._home / "pubsub"
495
+ if not pubsub_dir.is_dir():
496
+ return
497
+
498
+ topics_dir = pubsub_dir / "topics"
499
+ topic_count = 0
500
+ message_count = 0
501
+ if topics_dir.is_dir():
502
+ for td in topics_dir.iterdir():
503
+ if td.is_dir():
504
+ topic_count += 1
505
+ message_count += sum(1 for _ in td.glob("msg-*.json"))
506
+
507
+ subs_file = pubsub_dir / "subscriptions.json"
508
+ sub_count = 0
509
+ if subs_file.exists():
510
+ try:
511
+ subs = json.loads(subs_file.read_text(encoding="utf-8"))
512
+ sub_count = len(subs)
513
+ except Exception:
514
+ pass
515
+
516
+ report.pubsub = PubSubMetrics(
517
+ available=True,
518
+ topics=topic_count,
519
+ messages=message_count,
520
+ subscriptions=sub_count,
521
+ )
522
+ except Exception as exc:
523
+ report.errors.append(f"pubsub: {exc}")
524
+
525
+ def _collect_kms(self, report: MetricsReport) -> None:
526
+ """Collect KMS key management metrics."""
527
+ try:
528
+ keystore_path = self._home / "security" / "kms" / "keystore.json"
529
+ if not keystore_path.exists():
530
+ return
531
+
532
+ data = json.loads(keystore_path.read_text(encoding="utf-8"))
533
+ keys = data.get("keys", {})
534
+ by_type: dict[str, int] = {}
535
+ active = 0
536
+
537
+ for key_data in keys.values():
538
+ kt = key_data.get("key_type", "unknown")
539
+ by_type[kt] = by_type.get(kt, 0) + 1
540
+ if key_data.get("status") == "active":
541
+ active += 1
542
+
543
+ rot_log = self._home / "security" / "kms" / "rotation-log.json"
544
+ rotations = 0
545
+ if rot_log.exists():
546
+ try:
547
+ rot_data = json.loads(rot_log.read_text(encoding="utf-8"))
548
+ rotations = len(rot_data)
549
+ except Exception:
550
+ pass
551
+
552
+ report.kms = KmsMetrics(
553
+ available=True,
554
+ total_keys=len(keys),
555
+ active_keys=active,
556
+ by_type=by_type,
557
+ rotations=rotations,
558
+ )
559
+ except Exception as exc:
560
+ report.errors.append(f"kms: {exc}")
561
+
562
+ def _collect_fortress(self, report: MetricsReport) -> None:
563
+ """Collect memory fortress metrics."""
564
+ try:
565
+ config_path = self._home / "memory" / "fortress.json"
566
+ if not config_path.exists():
567
+ return
568
+
569
+ data = json.loads(config_path.read_text(encoding="utf-8"))
570
+ report.fortress = FortressMetrics(
571
+ enabled=data.get("enabled", False),
572
+ encryption_enabled=data.get("encryption_enabled", False),
573
+ seal_algorithm=data.get("seal_algorithm", ""),
574
+ )
575
+ except Exception as exc:
576
+ report.errors.append(f"fortress: {exc}")
577
+
578
+ def _collect_backup(self, report: MetricsReport) -> None:
579
+ """Collect backup metrics."""
580
+ try:
581
+ backup_dir = self._home / "backups"
582
+ if not backup_dir.exists():
583
+ return
584
+
585
+ backups = sorted(backup_dir.glob("backup-*.tar.gz"), reverse=True)
586
+ if not backups:
587
+ return
588
+
589
+ latest = backups[0]
590
+ report.backup = BackupMetrics(
591
+ backup_count=len(backups),
592
+ latest_backup=latest.name,
593
+ latest_size_bytes=latest.stat().st_size,
594
+ )
595
+ except Exception as exc:
596
+ report.errors.append(f"backup: {exc}")
597
+
598
+
599
+ # ---------------------------------------------------------------------------
600
+ # Consciousness loop runtime metrics
601
+ # ---------------------------------------------------------------------------
602
+
603
+
604
+ class ConsciousnessMetrics:
605
+ """Thread-safe runtime metrics for the consciousness loop.
606
+
607
+ Tracks per-message counters, a response-time histogram (min/max/avg/p99),
608
+ per-backend and per-tier usage counters, and per-peer message counts.
609
+ Persists daily snapshots to ``{home}/metrics/daily/{date}.json`` every
610
+ *persist_interval* seconds and loads the current-day snapshot on startup.
611
+
612
+ Args:
613
+ home: Agent home directory (default: ``~/.skcapstone``).
614
+ persist_interval: Seconds between auto-saves. Pass ``0`` to disable
615
+ the background thread (useful in tests).
616
+ """
617
+
618
+ def __init__(
619
+ self, home: Optional[Path] = None, persist_interval: int = 300
620
+ ) -> None:
621
+ self._lock = threading.Lock()
622
+ self._home = (home or Path("~/.skcapstone")).expanduser()
623
+ self._persist_interval = persist_interval
624
+
625
+ # Counters
626
+ self._messages_processed: int = 0
627
+ self._responses_sent: int = 0
628
+ self._errors: int = 0
629
+
630
+ # Response-time histogram samples (ms) — capped at 1 000 entries
631
+ self._response_times: list[float] = []
632
+
633
+ # Per-backend, per-tier, per-peer counters
634
+ self._backend_usage: dict[str, int] = {}
635
+ self._tier_usage: dict[str, int] = {}
636
+ self._messages_per_peer: dict[str, int] = {}
637
+
638
+ # Quality score accumulators (running sums + count for avg)
639
+ self._quality_sum_length: float = 0.0
640
+ self._quality_sum_coherence: float = 0.0
641
+ self._quality_sum_latency: float = 0.0
642
+ self._quality_sum_overall: float = 0.0
643
+ self._quality_count: int = 0
644
+
645
+ # Session start
646
+ self._session_start = datetime.now(timezone.utc)
647
+
648
+ # Load today's persisted data before starting background thread
649
+ self._load_today()
650
+
651
+ # Background persist thread (daemon so it never blocks process exit)
652
+ self._stop_event = threading.Event()
653
+ if persist_interval > 0:
654
+ t = threading.Thread(
655
+ target=self._persist_loop,
656
+ name="metrics-persist",
657
+ daemon=True,
658
+ )
659
+ t.start()
660
+
661
+ # ------------------------------------------------------------------
662
+ # Public record methods
663
+ # ------------------------------------------------------------------
664
+
665
+ def record_message(self, peer: str) -> None:
666
+ """Record an incoming message from *peer*."""
667
+ safe_peer = peer[:64]
668
+ with self._lock:
669
+ self._messages_processed += 1
670
+ self._messages_per_peer[safe_peer] = (
671
+ self._messages_per_peer.get(safe_peer, 0) + 1
672
+ )
673
+
674
+ def record_response(
675
+ self, response_time_ms: float, backend: str, tier: str
676
+ ) -> None:
677
+ """Record a successful response: timing, backend, and tier."""
678
+ with self._lock:
679
+ self._responses_sent += 1
680
+ self._response_times.append(response_time_ms)
681
+ # Bound memory — keep the most recent 1 000 samples
682
+ if len(self._response_times) > 1000:
683
+ self._response_times = self._response_times[-1000:]
684
+ self._backend_usage[backend] = self._backend_usage.get(backend, 0) + 1
685
+ self._tier_usage[tier] = self._tier_usage.get(tier, 0) + 1
686
+
687
+ def record_error(self) -> None:
688
+ """Record a processing error."""
689
+ with self._lock:
690
+ self._errors += 1
691
+
692
+ def record_quality(self, score: Any) -> None:
693
+ """Record a response quality score from :class:`ResponseScore`.
694
+
695
+ Args:
696
+ score: A ``ResponseScore`` instance (or any object with
697
+ ``length_score``, ``coherence_score``, ``latency_score``,
698
+ and ``overall`` float attributes).
699
+ """
700
+ with self._lock:
701
+ self._quality_sum_length += score.length_score
702
+ self._quality_sum_coherence += score.coherence_score
703
+ self._quality_sum_latency += score.latency_score
704
+ self._quality_sum_overall += score.overall
705
+ self._quality_count += 1
706
+
707
+ def quality_avg(self) -> dict:
708
+ """Return average quality scores across all recorded responses.
709
+
710
+ Returns:
711
+ Dict with keys ``length``, ``coherence``, ``latency``,
712
+ ``overall``, and ``count``. All averages are 0.0 when no
713
+ responses have been scored yet.
714
+ """
715
+ with self._lock:
716
+ n = self._quality_count
717
+ if n == 0:
718
+ return {
719
+ "length": 0.0,
720
+ "coherence": 0.0,
721
+ "latency": 0.0,
722
+ "overall": 0.0,
723
+ "count": 0,
724
+ }
725
+ return {
726
+ "length": round(self._quality_sum_length / n, 4),
727
+ "coherence": round(self._quality_sum_coherence / n, 4),
728
+ "latency": round(self._quality_sum_latency / n, 4),
729
+ "overall": round(self._quality_sum_overall / n, 4),
730
+ "count": n,
731
+ }
732
+
733
+ # ------------------------------------------------------------------
734
+ # Snapshot / persistence
735
+ # ------------------------------------------------------------------
736
+
737
+ def to_dict(self) -> dict:
738
+ """Return a JSON-serializable snapshot of today's metrics."""
739
+ with self._lock:
740
+ n = self._quality_count
741
+ quality_avg = {
742
+ "length": round(self._quality_sum_length / n, 4) if n else 0.0,
743
+ "coherence": round(self._quality_sum_coherence / n, 4) if n else 0.0,
744
+ "latency": round(self._quality_sum_latency / n, 4) if n else 0.0,
745
+ "overall": round(self._quality_sum_overall / n, 4) if n else 0.0,
746
+ "count": n,
747
+ }
748
+ return {
749
+ "date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
750
+ "session_start": self._session_start.isoformat(),
751
+ "messages_processed": self._messages_processed,
752
+ "responses_sent": self._responses_sent,
753
+ "errors": self._errors,
754
+ "response_time_ms": self._histogram_stats(),
755
+ "backend_usage": dict(self._backend_usage),
756
+ "tier_usage": dict(self._tier_usage),
757
+ "messages_per_peer": dict(self._messages_per_peer),
758
+ "quality_avg": quality_avg,
759
+ "quality_sums": {
760
+ "length": self._quality_sum_length,
761
+ "coherence": self._quality_sum_coherence,
762
+ "latency": self._quality_sum_latency,
763
+ "overall": self._quality_sum_overall,
764
+ "count": n,
765
+ },
766
+ }
767
+
768
+ def _histogram_stats(self) -> dict:
769
+ """Compute histogram stats (caller must hold self._lock)."""
770
+ if not self._response_times:
771
+ return {"min": 0.0, "max": 0.0, "avg": 0.0, "p99": 0.0, "count": 0}
772
+ times = sorted(self._response_times)
773
+ count = len(times)
774
+ # ceil(count * 0.99) - 1 gives the 0-based index for the p99 value:
775
+ # e.g. 100 samples → index 98 → the 99th-fastest value.
776
+ p99_idx = max(0, min(count - 1, int(count * 0.99) - 1))
777
+ return {
778
+ "min": round(times[0], 1),
779
+ "max": round(times[-1], 1),
780
+ "avg": round(sum(times) / count, 1),
781
+ "p99": round(times[p99_idx], 1),
782
+ "count": count,
783
+ }
784
+
785
+ def _daily_path(self) -> Path:
786
+ date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
787
+ return self._home / "metrics" / "daily" / f"{date_str}.json"
788
+
789
+ def save(self) -> None:
790
+ """Persist today's metrics to disk."""
791
+ try:
792
+ path = self._daily_path()
793
+ path.parent.mkdir(parents=True, exist_ok=True)
794
+ data = self.to_dict()
795
+ path.write_text(json.dumps(data, indent=2), encoding="utf-8")
796
+ except Exception as exc:
797
+ logger.warning("Failed to save consciousness metrics: %s", exc)
798
+
799
+ def _load_today(self) -> None:
800
+ """Load today's persisted counters on startup (skips histogram)."""
801
+ try:
802
+ path = self._daily_path()
803
+ if not path.exists():
804
+ return
805
+ data = json.loads(path.read_text(encoding="utf-8"))
806
+ with self._lock:
807
+ self._messages_processed = int(data.get("messages_processed", 0))
808
+ self._responses_sent = int(data.get("responses_sent", 0))
809
+ self._errors = int(data.get("errors", 0))
810
+ self._backend_usage = dict(data.get("backend_usage", {}))
811
+ self._tier_usage = dict(data.get("tier_usage", {}))
812
+ self._messages_per_peer = dict(data.get("messages_per_peer", {}))
813
+ # response_times are session-only; do not restore
814
+ # Restore quality score accumulators
815
+ sums = data.get("quality_sums", {})
816
+ self._quality_sum_length = float(sums.get("length", 0.0))
817
+ self._quality_sum_coherence = float(sums.get("coherence", 0.0))
818
+ self._quality_sum_latency = float(sums.get("latency", 0.0))
819
+ self._quality_sum_overall = float(sums.get("overall", 0.0))
820
+ self._quality_count = int(sums.get("count", 0))
821
+ except Exception as exc:
822
+ logger.warning("Failed to load historical metrics: %s", exc)
823
+
824
+ def _persist_loop(self) -> None:
825
+ """Background thread: save every persist_interval seconds."""
826
+ while not self._stop_event.wait(self._persist_interval):
827
+ self.save()
828
+
829
+ def stop(self) -> None:
830
+ """Stop background persistence and flush a final save."""
831
+ self._stop_event.set()
832
+ self.save()