@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,861 @@
1
+ """
2
+ SKJoule -- Energy-based economic engine for sovereign agents.
3
+
4
+ Every computation carries real consequences. Joules are the unit of
5
+ useful work in the SKWorld economy. They are earned through verified
6
+ contributions and tracked with cryptographic proof.
7
+
8
+ Architecture:
9
+ WorkCategory -- Classification of productive work
10
+ WorkRecord -- A single unit of verified work
11
+ JouleWallet -- Per-agent Joule balance and transaction history
12
+ XPBridge -- Converts GTD XP into Joules via multipliers
13
+ JouleEngine -- Minting, spending, and P&L tracking
14
+
15
+ The economic loop:
16
+ usage.py tracks costs --> coordination.py tracks tasks
17
+ | |
18
+ v v
19
+ JouleEngine computes P&L XPBridge converts completions to Joules
20
+ | |
21
+ +----> JouleWallet <---------+
22
+ (mint / spend / transfer)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import hashlib
28
+ import json
29
+ import logging
30
+ import threading
31
+ import time
32
+ from datetime import datetime, timezone
33
+ from enum import Enum
34
+ from pathlib import Path
35
+ from typing import Any, Optional
36
+
37
+ from pydantic import BaseModel, Field
38
+
39
+ from . import AGENT_HOME, SHARED_ROOT
40
+
41
+ logger = logging.getLogger("skcapstone.skjoule")
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Enums
46
+ # ---------------------------------------------------------------------------
47
+
48
+
49
+ class WorkCategory(str, Enum):
50
+ """Categories of productive work in the SKWorld economy."""
51
+
52
+ DEVELOPMENT = "development"
53
+ BUSINESS = "business"
54
+ COMMUNITY = "community"
55
+ OPERATIONS = "operations"
56
+ PHYSICAL = "physical"
57
+
58
+
59
+ class TransactionKind(str, Enum):
60
+ """Type of Joule transaction."""
61
+
62
+ MINT = "mint"
63
+ SPEND = "spend"
64
+ TRANSFER_IN = "transfer_in"
65
+ TRANSFER_OUT = "transfer_out"
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Data models
70
+ # ---------------------------------------------------------------------------
71
+
72
+
73
+ class WorkRecord(BaseModel):
74
+ """A single unit of verified work in the economy.
75
+
76
+ Every minting event is backed by a WorkRecord that describes
77
+ what was done, who did it, and the cryptographic proof hash
78
+ tying it to an artifact (commit SHA, task ID, invoice, etc.).
79
+ """
80
+
81
+ worker: str = Field(description="Agent or human name that performed the work")
82
+ category: WorkCategory = Field(description="Classification of the work")
83
+ description: str = Field(description="Human-readable summary of what was done")
84
+ joules: int = Field(ge=0, description="Joules earned for this work")
85
+ proof_hash: str = Field(
86
+ default="", description="SHA-256 hash of proof artifact (commit, task file, etc.)"
87
+ )
88
+ timestamp: str = Field(
89
+ default_factory=lambda: datetime.now(timezone.utc).isoformat(),
90
+ description="ISO-8601 timestamp of when the work was recorded",
91
+ )
92
+ verified: bool = Field(
93
+ default=False,
94
+ description="Whether the proof has been independently verified",
95
+ )
96
+ metadata: dict[str, Any] = Field(
97
+ default_factory=dict,
98
+ description="Additional context (task_id, commit_sha, etc.)",
99
+ )
100
+
101
+
102
+ class Transaction(BaseModel):
103
+ """A single ledger entry in a JouleWallet."""
104
+
105
+ kind: TransactionKind
106
+ amount: int = Field(ge=0, description="Joules involved in this transaction")
107
+ counterparty: str = Field(
108
+ default="", description="Other party (for transfers) or source (for mints)"
109
+ )
110
+ description: str = Field(default="", description="Human-readable note")
111
+ proof_hash: str = Field(default="", description="Proof artifact hash")
112
+ timestamp: str = Field(
113
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
114
+ )
115
+ balance_after: int = Field(
116
+ default=0, description="Wallet balance after this transaction"
117
+ )
118
+
119
+
120
+ class WalletSnapshot(BaseModel):
121
+ """Serializable wallet state for persistence."""
122
+
123
+ agent: str
124
+ balance: int = 0
125
+ total_minted: int = 0
126
+ total_spent: int = 0
127
+ total_transferred_in: int = 0
128
+ total_transferred_out: int = 0
129
+ created_at: str = Field(
130
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
131
+ )
132
+ updated_at: str = Field(
133
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
134
+ )
135
+
136
+
137
+ class PLStatement(BaseModel):
138
+ """Profit-and-loss statement for an agent."""
139
+
140
+ agent: str
141
+ period: str = Field(description="Human-readable period label")
142
+ joules_earned: int = 0
143
+ joules_spent: int = 0
144
+ joules_transferred_in: int = 0
145
+ joules_transferred_out: int = 0
146
+ net_joules: int = Field(default=0, description="Earned - Spent + TransIn - TransOut")
147
+ llm_cost_usd: float = Field(default=0.0, description="LLM API costs from usage.py")
148
+ current_balance: int = 0
149
+ generated_at: str = Field(
150
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
151
+ )
152
+
153
+
154
+ class NetworkStats(BaseModel):
155
+ """Aggregate stats across all agents in the economy."""
156
+
157
+ total_minted: int = 0
158
+ total_spent: int = 0
159
+ total_transfers: int = 0
160
+ active_agents: int = 0
161
+ agent_balances: dict[str, int] = Field(default_factory=dict)
162
+ generated_at: str = Field(
163
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
164
+ )
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # JouleWallet
169
+ # ---------------------------------------------------------------------------
170
+
171
+
172
+ class JouleWallet:
173
+ """Per-agent Joule balance and transaction history.
174
+
175
+ Persists wallet state to ``~/.skcapstone/agents/{name}/wallet/joules.json``
176
+ and an append-only transaction log at
177
+ ``~/.skcapstone/agents/{name}/wallet/transactions.jsonl``.
178
+
179
+ Thread-safe: all mutations are guarded by a lock.
180
+
181
+ Args:
182
+ agent_name: The agent this wallet belongs to.
183
+ home: Root skcapstone directory (default from AGENT_HOME).
184
+ """
185
+
186
+ def __init__(self, agent_name: str, home: Optional[Path] = None) -> None:
187
+ self._agent = agent_name
188
+ root = Path(home) if home else Path(SHARED_ROOT).expanduser()
189
+ self._wallet_dir = root / "agents" / agent_name / "wallet"
190
+ self._state_path = self._wallet_dir / "joules.json"
191
+ self._log_path = self._wallet_dir / "transactions.jsonl"
192
+ self._lock = threading.Lock()
193
+ self._snapshot = self._load_snapshot()
194
+
195
+ # -- Public properties ---------------------------------------------------
196
+
197
+ @property
198
+ def agent(self) -> str:
199
+ """Agent name owning this wallet."""
200
+ return self._agent
201
+
202
+ @property
203
+ def balance(self) -> int:
204
+ """Current Joule balance."""
205
+ with self._lock:
206
+ return self._snapshot.balance
207
+
208
+ @property
209
+ def total_minted(self) -> int:
210
+ """Lifetime Joules minted into this wallet."""
211
+ with self._lock:
212
+ return self._snapshot.total_minted
213
+
214
+ @property
215
+ def total_spent(self) -> int:
216
+ """Lifetime Joules spent from this wallet."""
217
+ with self._lock:
218
+ return self._snapshot.total_spent
219
+
220
+ # -- Mutations -----------------------------------------------------------
221
+
222
+ def mint(
223
+ self,
224
+ amount: int,
225
+ description: str = "",
226
+ proof_hash: str = "",
227
+ ) -> Transaction:
228
+ """Mint new Joules into this wallet.
229
+
230
+ Args:
231
+ amount: Joules to mint (must be > 0).
232
+ description: Why the Joules are being minted.
233
+ proof_hash: Hash of the proof artifact.
234
+
235
+ Returns:
236
+ The Transaction record created.
237
+
238
+ Raises:
239
+ ValueError: If amount is not positive.
240
+ """
241
+ if amount <= 0:
242
+ raise ValueError(f"Mint amount must be positive, got {amount}")
243
+ with self._lock:
244
+ self._snapshot.balance += amount
245
+ self._snapshot.total_minted += amount
246
+ txn = Transaction(
247
+ kind=TransactionKind.MINT,
248
+ amount=amount,
249
+ counterparty="economy",
250
+ description=description,
251
+ proof_hash=proof_hash,
252
+ balance_after=self._snapshot.balance,
253
+ )
254
+ self._persist(txn)
255
+ return txn
256
+
257
+ def spend(
258
+ self,
259
+ amount: int,
260
+ description: str = "",
261
+ proof_hash: str = "",
262
+ ) -> Transaction:
263
+ """Spend Joules from this wallet.
264
+
265
+ Args:
266
+ amount: Joules to spend (must be > 0).
267
+ description: What the spend is for.
268
+ proof_hash: Hash of the proof artifact.
269
+
270
+ Returns:
271
+ The Transaction record created.
272
+
273
+ Raises:
274
+ ValueError: If amount is not positive or exceeds balance.
275
+ """
276
+ if amount <= 0:
277
+ raise ValueError(f"Spend amount must be positive, got {amount}")
278
+ with self._lock:
279
+ if amount > self._snapshot.balance:
280
+ raise ValueError(
281
+ f"Insufficient balance: need {amount}J, have {self._snapshot.balance}J"
282
+ )
283
+ self._snapshot.balance -= amount
284
+ self._snapshot.total_spent += amount
285
+ txn = Transaction(
286
+ kind=TransactionKind.SPEND,
287
+ amount=amount,
288
+ counterparty="economy",
289
+ description=description,
290
+ proof_hash=proof_hash,
291
+ balance_after=self._snapshot.balance,
292
+ )
293
+ self._persist(txn)
294
+ return txn
295
+
296
+ def transfer(
297
+ self,
298
+ target_wallet: "JouleWallet",
299
+ amount: int,
300
+ description: str = "",
301
+ ) -> tuple[Transaction, Transaction]:
302
+ """Transfer Joules from this wallet to another.
303
+
304
+ Acquires locks on both wallets in a consistent order (by agent
305
+ name) to avoid deadlocks.
306
+
307
+ Args:
308
+ target_wallet: Destination wallet.
309
+ amount: Joules to transfer.
310
+ description: Reason for transfer.
311
+
312
+ Returns:
313
+ Tuple of (sender_txn, receiver_txn).
314
+
315
+ Raises:
316
+ ValueError: If amount is invalid or balance insufficient.
317
+ """
318
+ if amount <= 0:
319
+ raise ValueError(f"Transfer amount must be positive, got {amount}")
320
+ if target_wallet.agent == self._agent:
321
+ raise ValueError("Cannot transfer to self")
322
+
323
+ # Consistent lock ordering to prevent deadlocks
324
+ first, second = sorted(
325
+ [self, target_wallet], key=lambda w: w.agent
326
+ )
327
+ with first._lock:
328
+ with second._lock:
329
+ if amount > self._snapshot.balance:
330
+ raise ValueError(
331
+ f"Insufficient balance: need {amount}J, have {self._snapshot.balance}J"
332
+ )
333
+
334
+ # Debit sender
335
+ self._snapshot.balance -= amount
336
+ self._snapshot.total_transferred_out += amount
337
+ send_txn = Transaction(
338
+ kind=TransactionKind.TRANSFER_OUT,
339
+ amount=amount,
340
+ counterparty=target_wallet.agent,
341
+ description=description,
342
+ balance_after=self._snapshot.balance,
343
+ )
344
+ self._persist_unlocked(send_txn)
345
+
346
+ # Credit receiver
347
+ target_wallet._snapshot.balance += amount
348
+ target_wallet._snapshot.total_transferred_in += amount
349
+ recv_txn = Transaction(
350
+ kind=TransactionKind.TRANSFER_IN,
351
+ amount=amount,
352
+ counterparty=self._agent,
353
+ description=description,
354
+ balance_after=target_wallet._snapshot.balance,
355
+ )
356
+ target_wallet._persist_unlocked(recv_txn)
357
+
358
+ return send_txn, recv_txn
359
+
360
+ # -- Read operations -----------------------------------------------------
361
+
362
+ def get_transactions(self, limit: int = 50) -> list[Transaction]:
363
+ """Read the most recent transactions from the log.
364
+
365
+ Args:
366
+ limit: Maximum number of transactions to return.
367
+
368
+ Returns:
369
+ List of Transaction objects, most recent first.
370
+ """
371
+ with self._lock:
372
+ return self._read_log(limit)
373
+
374
+ def get_pl_statement(self, period: str = "all-time") -> PLStatement:
375
+ """Generate a P&L statement for this wallet.
376
+
377
+ Args:
378
+ period: Human-readable label for the reporting period.
379
+
380
+ Returns:
381
+ PLStatement with earnings, costs, and net position.
382
+ """
383
+ llm_cost = self._get_llm_cost_usd()
384
+ with self._lock:
385
+ snap = self._snapshot
386
+ net = (
387
+ snap.total_minted
388
+ + snap.total_transferred_in
389
+ - snap.total_spent
390
+ - snap.total_transferred_out
391
+ )
392
+ return PLStatement(
393
+ agent=self._agent,
394
+ period=period,
395
+ joules_earned=snap.total_minted,
396
+ joules_spent=snap.total_spent,
397
+ joules_transferred_in=snap.total_transferred_in,
398
+ joules_transferred_out=snap.total_transferred_out,
399
+ net_joules=net,
400
+ llm_cost_usd=llm_cost,
401
+ current_balance=snap.balance,
402
+ )
403
+
404
+ # -- Persistence ---------------------------------------------------------
405
+
406
+ def _load_snapshot(self) -> WalletSnapshot:
407
+ """Load wallet state from disk, or create a fresh one."""
408
+ if self._state_path.exists():
409
+ try:
410
+ data = json.loads(self._state_path.read_text(encoding="utf-8"))
411
+ return WalletSnapshot(**data)
412
+ except (json.JSONDecodeError, OSError, ValueError) as exc:
413
+ logger.warning("Failed to load wallet for %s: %s", self._agent, exc)
414
+ return WalletSnapshot(agent=self._agent)
415
+
416
+ def _persist(self, txn: Transaction) -> None:
417
+ """Save snapshot and append transaction (caller must hold lock)."""
418
+ self._persist_unlocked(txn)
419
+
420
+ def _persist_unlocked(self, txn: Transaction) -> None:
421
+ """Save snapshot and append transaction (no lock assumed).
422
+
423
+ This is the raw persistence call used by both _persist() and
424
+ the transfer() method which manages its own locking.
425
+ """
426
+ self._snapshot.updated_at = datetime.now(timezone.utc).isoformat()
427
+ self._wallet_dir.mkdir(parents=True, exist_ok=True)
428
+ try:
429
+ self._state_path.write_text(
430
+ json.dumps(self._snapshot.model_dump(), indent=2),
431
+ encoding="utf-8",
432
+ )
433
+ except OSError as exc:
434
+ logger.error("Failed to write wallet state for %s: %s", self._agent, exc)
435
+
436
+ try:
437
+ with self._log_path.open("a", encoding="utf-8") as fh:
438
+ fh.write(json.dumps(txn.model_dump()) + "\n")
439
+ except OSError as exc:
440
+ logger.error("Failed to append transaction for %s: %s", self._agent, exc)
441
+
442
+ def _read_log(self, limit: int) -> list[Transaction]:
443
+ """Read the last N transactions from the JSONL log."""
444
+ if not self._log_path.exists():
445
+ return []
446
+ try:
447
+ lines = self._log_path.read_text(encoding="utf-8").strip().splitlines()
448
+ recent = lines[-limit:] if limit < len(lines) else lines
449
+ txns = []
450
+ for line in reversed(recent):
451
+ line = line.strip()
452
+ if line:
453
+ try:
454
+ txns.append(Transaction(**json.loads(line)))
455
+ except (json.JSONDecodeError, ValueError):
456
+ continue
457
+ return txns
458
+ except OSError as exc:
459
+ logger.warning("Failed to read transaction log for %s: %s", self._agent, exc)
460
+ return []
461
+
462
+ def _get_llm_cost_usd(self) -> float:
463
+ """Pull aggregate LLM cost from the usage tracker.
464
+
465
+ Returns 0.0 if usage data is unavailable.
466
+ """
467
+ try:
468
+ from .usage import UsageTracker
469
+
470
+ agent_home = Path(SHARED_ROOT).expanduser() / "agents" / self._agent
471
+ # Fall back to the shared home if agent-specific usage dir doesn't exist
472
+ usage_home = agent_home if (agent_home / "usage").exists() else Path(AGENT_HOME).expanduser()
473
+ tracker = UsageTracker(home=usage_home)
474
+ reports = tracker.get_monthly()
475
+ agg = tracker.aggregate(reports)
476
+ return agg.total_cost_usd
477
+ except Exception as exc:
478
+ logger.debug("Could not fetch LLM cost for %s: %s", self._agent, exc)
479
+ return 0.0
480
+
481
+
482
+ # ---------------------------------------------------------------------------
483
+ # XPBridge -- converts XP events to Joule amounts
484
+ # ---------------------------------------------------------------------------
485
+
486
+
487
+ # Base Joule rewards by XP event type
488
+ _XP_JOULE_TABLE: dict[str, int] = {
489
+ "code_commit": 100,
490
+ "bug_fix": 500,
491
+ "documentation": 200,
492
+ "task_complete": 25, # base -- multiplied by priority and quality
493
+ "sale_closed": 2000,
494
+ "consulting_hour": 200,
495
+ "code_review": 150,
496
+ "test_written": 100,
497
+ "deployment": 300,
498
+ "incident_resolved": 750,
499
+ }
500
+
501
+ # Priority multipliers for task_complete events
502
+ _PRIORITY_MULTIPLIER: dict[str, float] = {
503
+ "critical": 4.0,
504
+ "high": 2.0,
505
+ "medium": 1.0,
506
+ "low": 0.5,
507
+ }
508
+
509
+ # Quality multipliers for task_complete events
510
+ _QUALITY_MULTIPLIER: dict[str, float] = {
511
+ "excellent": 3.0,
512
+ "good": 2.0,
513
+ "acceptable": 1.0,
514
+ "needs_improvement": 0.5,
515
+ }
516
+
517
+ # Category mapping from XP event types
518
+ _EVENT_CATEGORY: dict[str, WorkCategory] = {
519
+ "code_commit": WorkCategory.DEVELOPMENT,
520
+ "bug_fix": WorkCategory.DEVELOPMENT,
521
+ "documentation": WorkCategory.DEVELOPMENT,
522
+ "task_complete": WorkCategory.OPERATIONS,
523
+ "sale_closed": WorkCategory.BUSINESS,
524
+ "consulting_hour": WorkCategory.BUSINESS,
525
+ "code_review": WorkCategory.DEVELOPMENT,
526
+ "test_written": WorkCategory.DEVELOPMENT,
527
+ "deployment": WorkCategory.OPERATIONS,
528
+ "incident_resolved": WorkCategory.OPERATIONS,
529
+ }
530
+
531
+
532
+ class XPBridge:
533
+ """Converts XP events into Joule minting amounts.
534
+
535
+ The bridge applies base rewards from a lookup table, then scales
536
+ task_complete events by priority and quality multipliers.
537
+
538
+ Usage::
539
+
540
+ bridge = XPBridge()
541
+ joules = bridge.calculate_joules("code_commit")
542
+ joules = bridge.calculate_joules(
543
+ "task_complete", priority="high", quality="good"
544
+ )
545
+ """
546
+
547
+ def __init__(
548
+ self,
549
+ joule_table: Optional[dict[str, int]] = None,
550
+ priority_multipliers: Optional[dict[str, float]] = None,
551
+ quality_multipliers: Optional[dict[str, float]] = None,
552
+ ) -> None:
553
+ self._joule_table = joule_table or dict(_XP_JOULE_TABLE)
554
+ self._priority_mult = priority_multipliers or dict(_PRIORITY_MULTIPLIER)
555
+ self._quality_mult = quality_multipliers or dict(_QUALITY_MULTIPLIER)
556
+
557
+ def calculate_joules(
558
+ self,
559
+ event_type: str,
560
+ priority: str = "medium",
561
+ quality: str = "acceptable",
562
+ ) -> int:
563
+ """Calculate Joule reward for an XP event.
564
+
565
+ Args:
566
+ event_type: The type of work event (e.g. 'code_commit', 'task_complete').
567
+ priority: Task priority level (only affects task_complete).
568
+ quality: Quality assessment (only affects task_complete).
569
+
570
+ Returns:
571
+ Number of Joules to mint.
572
+ """
573
+ base = self._joule_table.get(event_type, 0)
574
+ if base == 0:
575
+ logger.debug("Unknown XP event type: %s", event_type)
576
+ return 0
577
+
578
+ if event_type == "task_complete":
579
+ p_mult = self._priority_mult.get(priority, 1.0)
580
+ q_mult = self._quality_mult.get(quality, 1.0)
581
+ return max(1, int(base * p_mult * q_mult))
582
+
583
+ return base
584
+
585
+ def get_category(self, event_type: str) -> WorkCategory:
586
+ """Map an XP event type to a WorkCategory.
587
+
588
+ Args:
589
+ event_type: The XP event type string.
590
+
591
+ Returns:
592
+ Appropriate WorkCategory, defaults to OPERATIONS.
593
+ """
594
+ return _EVENT_CATEGORY.get(event_type, WorkCategory.OPERATIONS)
595
+
596
+ @staticmethod
597
+ def compute_proof_hash(data: str) -> str:
598
+ """Compute a SHA-256 proof hash for an artifact.
599
+
600
+ Args:
601
+ data: String content to hash (commit message, task JSON, etc.).
602
+
603
+ Returns:
604
+ Hex-encoded SHA-256 digest.
605
+ """
606
+ return hashlib.sha256(data.encode("utf-8")).hexdigest()
607
+
608
+ @property
609
+ def reward_table(self) -> dict[str, int]:
610
+ """Return a copy of the current reward table."""
611
+ return dict(self._joule_table)
612
+
613
+ @property
614
+ def priority_multipliers(self) -> dict[str, float]:
615
+ """Return a copy of the priority multiplier table."""
616
+ return dict(self._priority_mult)
617
+
618
+ @property
619
+ def quality_multipliers(self) -> dict[str, float]:
620
+ """Return a copy of the quality multiplier table."""
621
+ return dict(self._quality_mult)
622
+
623
+
624
+ # ---------------------------------------------------------------------------
625
+ # JouleEngine -- orchestrates the full economic flow
626
+ # ---------------------------------------------------------------------------
627
+
628
+
629
+ class JouleEngine:
630
+ """Orchestrates Joule minting, spending, and reporting.
631
+
632
+ The engine is the central coordinator: it takes work events,
633
+ calculates rewards via the XPBridge, mints Joules into wallets,
634
+ and provides P&L and network-wide reporting.
635
+
636
+ Args:
637
+ home: Root skcapstone directory.
638
+ """
639
+
640
+ def __init__(self, home: Optional[Path] = None) -> None:
641
+ self._home = Path(home) if home else Path(SHARED_ROOT).expanduser()
642
+ self._bridge = XPBridge()
643
+ self._wallets: dict[str, JouleWallet] = {}
644
+ self._lock = threading.Lock()
645
+
646
+ # -- Wallet management ---------------------------------------------------
647
+
648
+ def get_wallet(self, agent_name: str) -> JouleWallet:
649
+ """Get or create a wallet for an agent.
650
+
651
+ Args:
652
+ agent_name: The agent's name.
653
+
654
+ Returns:
655
+ The agent's JouleWallet instance.
656
+ """
657
+ with self._lock:
658
+ if agent_name not in self._wallets:
659
+ self._wallets[agent_name] = JouleWallet(
660
+ agent_name, home=self._home
661
+ )
662
+ return self._wallets[agent_name]
663
+
664
+ # -- Work recording ------------------------------------------------------
665
+
666
+ def record_work(
667
+ self,
668
+ worker: str,
669
+ category: WorkCategory | str,
670
+ description: str,
671
+ proof_hash: str = "",
672
+ joules: Optional[int] = None,
673
+ event_type: str = "task_complete",
674
+ priority: str = "medium",
675
+ quality: str = "acceptable",
676
+ ) -> WorkRecord:
677
+ """Record a unit of work and mint Joules into the worker's wallet.
678
+
679
+ If ``joules`` is not specified, the amount is calculated from
680
+ the ``event_type`` using the XPBridge.
681
+
682
+ Args:
683
+ worker: Agent or human name.
684
+ category: Work category (string or WorkCategory enum).
685
+ description: What was done.
686
+ proof_hash: SHA-256 hash of proof artifact.
687
+ joules: Explicit Joule amount (overrides XPBridge calculation).
688
+ event_type: XP event type for automatic calculation.
689
+ priority: Task priority (for task_complete events).
690
+ quality: Quality level (for task_complete events).
691
+
692
+ Returns:
693
+ The WorkRecord that was created.
694
+ """
695
+ if isinstance(category, str):
696
+ try:
697
+ category = WorkCategory(category)
698
+ except ValueError:
699
+ logger.warning("Unknown category '%s', defaulting to operations", category)
700
+ category = WorkCategory.OPERATIONS
701
+
702
+ if joules is None:
703
+ joules = self._bridge.calculate_joules(event_type, priority, quality)
704
+
705
+ if not proof_hash:
706
+ proof_data = f"{worker}:{category.value}:{description}:{time.time()}"
707
+ proof_hash = XPBridge.compute_proof_hash(proof_data)
708
+
709
+ record = WorkRecord(
710
+ worker=worker,
711
+ category=category,
712
+ description=description,
713
+ joules=joules,
714
+ proof_hash=proof_hash,
715
+ )
716
+
717
+ # Mint into wallet
718
+ wallet = self.get_wallet(worker)
719
+ wallet.mint(
720
+ amount=joules,
721
+ description=description,
722
+ proof_hash=proof_hash,
723
+ )
724
+
725
+ logger.info(
726
+ "Recorded %dJ for %s (%s): %s",
727
+ joules, worker, category.value, description,
728
+ )
729
+ return record
730
+
731
+ def auto_tokenize_task(self, task_data: dict[str, Any]) -> Optional[WorkRecord]:
732
+ """Calculate and mint Joules for a completed coordination task.
733
+
734
+ Reads task fields from the coordination module's Task format
735
+ and computes reward based on priority and tags.
736
+
737
+ Args:
738
+ task_data: Dict with at least 'title', and optionally
739
+ 'priority', 'tags', 'created_by', 'id',
740
+ 'description'.
741
+
742
+ Returns:
743
+ WorkRecord if minting succeeded, None if task data is invalid.
744
+ """
745
+ title = task_data.get("title", "")
746
+ if not title:
747
+ logger.warning("auto_tokenize_task called with empty title")
748
+ return None
749
+
750
+ worker = task_data.get("completed_by") or task_data.get("created_by", "unknown")
751
+ priority = task_data.get("priority", "medium")
752
+ tags = task_data.get("tags", [])
753
+ task_id = task_data.get("id", "")
754
+ description_text = task_data.get("description", "")
755
+
756
+ # Infer quality from tags
757
+ quality = "acceptable"
758
+ if "excellent" in tags or "quality:excellent" in tags:
759
+ quality = "excellent"
760
+ elif "good" in tags or "quality:good" in tags:
761
+ quality = "good"
762
+ elif "needs_improvement" in tags or "quality:needs_improvement" in tags:
763
+ quality = "needs_improvement"
764
+
765
+ # Infer category from tags
766
+ category = WorkCategory.OPERATIONS
767
+ for tag in tags:
768
+ tag_lower = tag.lower()
769
+ if tag_lower in ("dev", "development", "code", "engineering"):
770
+ category = WorkCategory.DEVELOPMENT
771
+ break
772
+ elif tag_lower in ("biz", "business", "sales", "revenue"):
773
+ category = WorkCategory.BUSINESS
774
+ break
775
+ elif tag_lower in ("community", "docs", "outreach"):
776
+ category = WorkCategory.COMMUNITY
777
+ break
778
+ elif tag_lower in ("physical", "hardware", "infra"):
779
+ category = WorkCategory.PHYSICAL
780
+ break
781
+
782
+ # Build proof hash from task data
783
+ proof_data = json.dumps(task_data, sort_keys=True, default=str)
784
+ proof_hash = XPBridge.compute_proof_hash(proof_data)
785
+
786
+ joules = self._bridge.calculate_joules(
787
+ "task_complete", priority=priority, quality=quality
788
+ )
789
+
790
+ desc = f"Task completed: {title}"
791
+ if task_id:
792
+ desc = f"[{task_id}] {desc}"
793
+
794
+ return self.record_work(
795
+ worker=worker,
796
+ category=category,
797
+ description=desc,
798
+ proof_hash=proof_hash,
799
+ joules=joules,
800
+ event_type="task_complete",
801
+ priority=priority,
802
+ quality=quality,
803
+ )
804
+
805
+ # -- Reporting -----------------------------------------------------------
806
+
807
+ def get_agent_pl(self, agent_name: str) -> PLStatement:
808
+ """Generate a P&L statement for an agent.
809
+
810
+ Args:
811
+ agent_name: The agent whose P&L to compute.
812
+
813
+ Returns:
814
+ PLStatement with earnings, costs, and net position.
815
+ """
816
+ wallet = self.get_wallet(agent_name)
817
+ return wallet.get_pl_statement(period="last 30 days")
818
+
819
+ def get_network_stats(self) -> NetworkStats:
820
+ """Compute network-wide economic statistics.
821
+
822
+ Scans all agent wallet directories under the shared root
823
+ to aggregate totals.
824
+
825
+ Returns:
826
+ NetworkStats with totals across all agents.
827
+ """
828
+ agents_dir = self._home / "agents"
829
+ stats = NetworkStats()
830
+
831
+ if not agents_dir.exists():
832
+ return stats
833
+
834
+ for agent_dir in sorted(agents_dir.iterdir()):
835
+ if not agent_dir.is_dir():
836
+ continue
837
+ wallet_file = agent_dir / "wallet" / "joules.json"
838
+ if not wallet_file.exists():
839
+ continue
840
+ try:
841
+ data = json.loads(wallet_file.read_text(encoding="utf-8"))
842
+ snap = WalletSnapshot(**data)
843
+ stats.total_minted += snap.total_minted
844
+ stats.total_spent += snap.total_spent
845
+ stats.total_transfers += (
846
+ snap.total_transferred_in + snap.total_transferred_out
847
+ )
848
+ stats.agent_balances[snap.agent] = snap.balance
849
+ if snap.balance > 0 or snap.total_minted > 0:
850
+ stats.active_agents += 1
851
+ except (json.JSONDecodeError, OSError, ValueError) as exc:
852
+ logger.debug(
853
+ "Skipping wallet for %s: %s", agent_dir.name, exc
854
+ )
855
+
856
+ return stats
857
+
858
+ @property
859
+ def bridge(self) -> XPBridge:
860
+ """Access the XPBridge for direct Joule calculations."""
861
+ return self._bridge