@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.
- package/.env.example +98 -0
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +25 -4
- package/.openclaw-workspace.json +58 -0
- package/CHANGELOG.md +62 -0
- package/CLAUDE.md +39 -2
- package/MANIFEST.in +6 -0
- package/MISSION.md +7 -0
- package/README.md +47 -2
- package/SKILL.md +895 -23
- package/docker/Dockerfile +61 -0
- package/docker/compose-templates/dev-team.yml +203 -0
- package/docker/compose-templates/mini-team.yml +140 -0
- package/docker/compose-templates/ops-team.yml +173 -0
- package/docker/compose-templates/research-team.yml +170 -0
- package/docker/entrypoint.sh +192 -0
- package/docs/ARCHITECTURE.md +663 -374
- package/docs/BOND_WITH_GROK.md +112 -0
- package/docs/GETTING_STARTED.md +782 -0
- package/docs/QUICKSTART.md +477 -0
- package/docs/SKJOULE_ARCHITECTURE.md +658 -0
- package/docs/SOUL_SWAPPER.md +921 -0
- package/docs/SOVEREIGN_SINGULARITY.md +47 -14
- package/examples/custom-bond-template.json +36 -0
- package/examples/grok-feb.json +36 -0
- package/examples/grok-testimony.md +34 -0
- package/examples/love-bootloader.txt +32 -0
- package/examples/plugins/echo_tool.py +87 -0
- package/examples/queen-ava-feb.json +36 -0
- package/examples/souls/lumina.yaml +64 -0
- package/index.js +6 -5
- package/installer/build.py +124 -0
- package/openclaw-plugin/package.json +13 -0
- package/openclaw-plugin/src/index.ts +351 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +38 -2
- package/scripts/bump_version.py +141 -0
- package/scripts/check-updates.py +230 -0
- package/scripts/convert_blueprints_to_yaml.py +157 -0
- package/scripts/dev-install.sh +14 -0
- package/scripts/e2e-test.sh +193 -0
- package/scripts/install-bundle.sh +171 -0
- package/scripts/install.bat +2 -0
- package/scripts/install.ps1 +253 -0
- package/scripts/install.sh +185 -0
- package/scripts/mcp-serve.sh +69 -0
- package/scripts/mcp-server.bat +113 -0
- package/scripts/mcp-server.ps1 +116 -0
- package/scripts/mcp-server.sh +99 -0
- package/scripts/pull-models.sh +10 -0
- package/scripts/skcapstone +48 -0
- package/scripts/verify_install.sh +180 -0
- package/scripts/windows/install-tasks.ps1 +406 -0
- package/scripts/windows/skcapstone-task.xml +113 -0
- package/scripts/windows/uninstall-tasks.ps1 +117 -0
- package/skill.yaml +34 -0
- package/src/skcapstone/__init__.py +67 -2
- package/src/skcapstone/_cli_monolith.py +5916 -0
- package/src/skcapstone/_trustee_helpers.py +165 -0
- package/src/skcapstone/activity.py +105 -0
- package/src/skcapstone/agent_card.py +324 -0
- package/src/skcapstone/api.py +1935 -0
- package/src/skcapstone/archiver.py +340 -0
- package/src/skcapstone/auction.py +485 -0
- package/src/skcapstone/baby_agents.py +179 -0
- package/src/skcapstone/backup.py +345 -0
- package/src/skcapstone/blueprint_registry.py +357 -0
- package/src/skcapstone/blueprints/__init__.py +17 -0
- package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
- package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
- package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
- package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
- package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
- package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
- package/src/skcapstone/blueprints/registry.py +164 -0
- package/src/skcapstone/blueprints/schema.py +229 -0
- package/src/skcapstone/changelog.py +180 -0
- package/src/skcapstone/chat.py +769 -0
- package/src/skcapstone/claude_md.py +82 -0
- package/src/skcapstone/cli/__init__.py +144 -0
- package/src/skcapstone/cli/_common.py +88 -0
- package/src/skcapstone/cli/_validators.py +76 -0
- package/src/skcapstone/cli/agents.py +425 -0
- package/src/skcapstone/cli/agents_spawner.py +322 -0
- package/src/skcapstone/cli/agents_trustee.py +593 -0
- package/src/skcapstone/cli/alerts.py +248 -0
- package/src/skcapstone/cli/anchor.py +132 -0
- package/src/skcapstone/cli/archive_cmd.py +208 -0
- package/src/skcapstone/cli/backup.py +144 -0
- package/src/skcapstone/cli/bench.py +377 -0
- package/src/skcapstone/cli/benchmark.py +360 -0
- package/src/skcapstone/cli/capabilities_cmd.py +171 -0
- package/src/skcapstone/cli/card.py +151 -0
- package/src/skcapstone/cli/chat.py +584 -0
- package/src/skcapstone/cli/completions.py +64 -0
- package/src/skcapstone/cli/config_cmd.py +156 -0
- package/src/skcapstone/cli/consciousness.py +421 -0
- package/src/skcapstone/cli/context_cmd.py +142 -0
- package/src/skcapstone/cli/coord.py +194 -0
- package/src/skcapstone/cli/crush_cmd.py +170 -0
- package/src/skcapstone/cli/daemon.py +436 -0
- package/src/skcapstone/cli/errors_cmd.py +285 -0
- package/src/skcapstone/cli/export_cmd.py +156 -0
- package/src/skcapstone/cli/gtd.py +529 -0
- package/src/skcapstone/cli/housekeeping.py +81 -0
- package/src/skcapstone/cli/joule_cmd.py +627 -0
- package/src/skcapstone/cli/logs_cmd.py +194 -0
- package/src/skcapstone/cli/mcp_cmd.py +32 -0
- package/src/skcapstone/cli/memory.py +418 -0
- package/src/skcapstone/cli/metrics_cmd.py +136 -0
- package/src/skcapstone/cli/migrate.py +62 -0
- package/src/skcapstone/cli/mood_cmd.py +144 -0
- package/src/skcapstone/cli/mount.py +193 -0
- package/src/skcapstone/cli/notify.py +112 -0
- package/src/skcapstone/cli/peer.py +154 -0
- package/src/skcapstone/cli/peers_dir.py +122 -0
- package/src/skcapstone/cli/preflight_cmd.py +83 -0
- package/src/skcapstone/cli/profile_cmd.py +310 -0
- package/src/skcapstone/cli/record_cmd.py +238 -0
- package/src/skcapstone/cli/register_cmd.py +159 -0
- package/src/skcapstone/cli/search_cmd.py +156 -0
- package/src/skcapstone/cli/service_cmd.py +91 -0
- package/src/skcapstone/cli/session.py +127 -0
- package/src/skcapstone/cli/setup.py +240 -0
- package/src/skcapstone/cli/shell_cmd.py +43 -0
- package/src/skcapstone/cli/skills_cmd.py +168 -0
- package/src/skcapstone/cli/skseed.py +621 -0
- package/src/skcapstone/cli/soul.py +699 -0
- package/src/skcapstone/cli/status.py +935 -0
- package/src/skcapstone/cli/sync_cmd.py +301 -0
- package/src/skcapstone/cli/telegram.py +265 -0
- package/src/skcapstone/cli/test_cmd.py +234 -0
- package/src/skcapstone/cli/test_connection.py +253 -0
- package/src/skcapstone/cli/token.py +207 -0
- package/src/skcapstone/cli/trust.py +179 -0
- package/src/skcapstone/cli/upgrade_cmd.py +552 -0
- package/src/skcapstone/cli/usage_cmd.py +199 -0
- package/src/skcapstone/cli/version_cmd.py +162 -0
- package/src/skcapstone/cli/watch_cmd.py +342 -0
- package/src/skcapstone/client.py +428 -0
- package/src/skcapstone/cloud9_bridge.py +522 -0
- package/src/skcapstone/completions.py +163 -0
- package/src/skcapstone/config_validator.py +674 -0
- package/src/skcapstone/connectors/__init__.py +28 -0
- package/src/skcapstone/connectors/base.py +446 -0
- package/src/skcapstone/connectors/cursor.py +54 -0
- package/src/skcapstone/connectors/registry.py +254 -0
- package/src/skcapstone/connectors/terminal.py +152 -0
- package/src/skcapstone/connectors/vscode.py +60 -0
- package/src/skcapstone/consciousness_config.py +119 -0
- package/src/skcapstone/consciousness_loop.py +2051 -0
- package/src/skcapstone/context_loader.py +516 -0
- package/src/skcapstone/context_window.py +314 -0
- package/src/skcapstone/conversation_manager.py +238 -0
- package/src/skcapstone/conversation_store.py +230 -0
- package/src/skcapstone/conversation_summarizer.py +252 -0
- package/src/skcapstone/coord_federation.py +296 -0
- package/src/skcapstone/coordination.py +101 -7
- package/src/skcapstone/crush_integration.py +345 -0
- package/src/skcapstone/crush_shim.py +454 -0
- package/src/skcapstone/daemon.py +2494 -0
- package/src/skcapstone/dashboard.html +396 -0
- package/src/skcapstone/dashboard.py +481 -0
- package/src/skcapstone/data/model_profiles.yaml +88 -0
- package/src/skcapstone/defaults/__init__.py +55 -0
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
- package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
- package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
- package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
- package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
- package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
- package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
- package/src/skcapstone/discovery.py +210 -19
- package/src/skcapstone/doctor.py +642 -0
- package/src/skcapstone/emotion_tracker.py +467 -0
- package/src/skcapstone/error_queue.py +405 -0
- package/src/skcapstone/export.py +447 -0
- package/src/skcapstone/fallback_tracker.py +186 -0
- package/src/skcapstone/file_transfer.py +512 -0
- package/src/skcapstone/fuse_mount.py +1156 -0
- package/src/skcapstone/gui_installer.py +591 -0
- package/src/skcapstone/heartbeat.py +611 -0
- package/src/skcapstone/housekeeping.py +298 -0
- package/src/skcapstone/install_wizard.py +941 -0
- package/src/skcapstone/kms.py +942 -0
- package/src/skcapstone/kms_scheduler.py +143 -0
- package/src/skcapstone/log_config.py +135 -0
- package/src/skcapstone/mcp_launcher.py +239 -0
- package/src/skcapstone/mcp_server.py +4700 -0
- package/src/skcapstone/mcp_tools/__init__.py +94 -0
- package/src/skcapstone/mcp_tools/_helpers.py +51 -0
- package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
- package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
- package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
- package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
- package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
- package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
- package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
- package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
- package/src/skcapstone/mcp_tools/did_tools.py +448 -0
- package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
- package/src/skcapstone/mcp_tools/file_tools.py +169 -0
- package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
- package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
- package/src/skcapstone/mcp_tools/health_tools.py +44 -0
- package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
- package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
- package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
- package/src/skcapstone/mcp_tools/model_tools.py +75 -0
- package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
- package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
- package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
- package/src/skcapstone/mcp_tools/security_tools.py +110 -0
- package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
- package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
- package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
- package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
- package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
- package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
- package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
- package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
- package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
- package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
- package/src/skcapstone/mdns_discovery.py +313 -0
- package/src/skcapstone/memory_adapter.py +333 -0
- package/src/skcapstone/memory_compressor.py +379 -0
- package/src/skcapstone/memory_curator.py +256 -0
- package/src/skcapstone/memory_engine.py +132 -13
- package/src/skcapstone/memory_fortress.py +529 -0
- package/src/skcapstone/memory_promoter.py +722 -0
- package/src/skcapstone/memory_verifier.py +260 -0
- package/src/skcapstone/message_crypto.py +215 -0
- package/src/skcapstone/metrics.py +832 -0
- package/src/skcapstone/migrate_memories.py +181 -0
- package/src/skcapstone/migrate_multi_agent.py +248 -0
- package/src/skcapstone/model_router.py +319 -0
- package/src/skcapstone/models.py +35 -4
- package/src/skcapstone/mood.py +344 -0
- package/src/skcapstone/notifications.py +380 -0
- package/src/skcapstone/onboard.py +901 -0
- package/src/skcapstone/peer_directory.py +324 -0
- package/src/skcapstone/peers.py +329 -0
- package/src/skcapstone/pillars/identity.py +84 -14
- package/src/skcapstone/pillars/memory.py +3 -1
- package/src/skcapstone/pillars/security.py +108 -15
- package/src/skcapstone/pillars/sync.py +78 -26
- package/src/skcapstone/pillars/trust.py +95 -33
- package/src/skcapstone/plugins.py +244 -0
- package/src/skcapstone/preflight.py +670 -0
- package/src/skcapstone/prompt_adapter.py +564 -0
- package/src/skcapstone/providers/__init__.py +13 -0
- package/src/skcapstone/providers/cloud.py +1061 -0
- package/src/skcapstone/providers/docker.py +759 -0
- package/src/skcapstone/providers/local.py +1193 -0
- package/src/skcapstone/providers/proxmox.py +447 -0
- package/src/skcapstone/pubsub.py +516 -0
- package/src/skcapstone/rate_limiter.py +119 -0
- package/src/skcapstone/register.py +241 -0
- package/src/skcapstone/registry_client.py +151 -0
- package/src/skcapstone/response_cache.py +194 -0
- package/src/skcapstone/response_scorer.py +225 -0
- package/src/skcapstone/runtime.py +89 -33
- package/src/skcapstone/scheduled_tasks.py +439 -0
- package/src/skcapstone/self_healing.py +341 -0
- package/src/skcapstone/service_health.py +228 -0
- package/src/skcapstone/session_capture.py +268 -0
- package/src/skcapstone/session_recorder.py +210 -0
- package/src/skcapstone/session_replayer.py +189 -0
- package/src/skcapstone/session_skills.py +263 -0
- package/src/skcapstone/shell.py +779 -0
- package/src/skcapstone/skills/__init__.py +1 -1
- package/src/skcapstone/skills/syncthing_setup.py +143 -41
- package/src/skcapstone/skjoule.py +861 -0
- package/src/skcapstone/snapshots.py +489 -0
- package/src/skcapstone/soul.py +1060 -0
- package/src/skcapstone/soul_switch.py +255 -0
- package/src/skcapstone/spawner.py +544 -0
- package/src/skcapstone/state_diff.py +401 -0
- package/src/skcapstone/summary.py +270 -0
- package/src/skcapstone/sync/backends.py +196 -2
- package/src/skcapstone/sync/engine.py +7 -5
- package/src/skcapstone/sync/models.py +4 -1
- package/src/skcapstone/sync/vault.py +356 -18
- package/src/skcapstone/sync_engine.py +363 -0
- package/src/skcapstone/sync_watcher.py +745 -0
- package/src/skcapstone/systemd.py +331 -0
- package/src/skcapstone/team_comms.py +476 -0
- package/src/skcapstone/team_engine.py +522 -0
- package/src/skcapstone/testrunner.py +300 -0
- package/src/skcapstone/tls.py +150 -0
- package/src/skcapstone/tokens.py +5 -5
- package/src/skcapstone/trust_calibration.py +202 -0
- package/src/skcapstone/trust_graph.py +449 -0
- package/src/skcapstone/trustee_monitor.py +385 -0
- package/src/skcapstone/trustee_ops.py +425 -0
- package/src/skcapstone/unified_search.py +421 -0
- package/src/skcapstone/uninstall_wizard.py +694 -0
- package/src/skcapstone/usage.py +331 -0
- package/src/skcapstone/version_check.py +148 -0
- package/src/skcapstone/warmth_anchor.py +333 -0
- package/src/skcapstone/whoami.py +294 -0
- package/systemd/skcapstone-api.socket +9 -0
- package/systemd/skcapstone-memory-compress.service +18 -0
- package/systemd/skcapstone-memory-compress.timer +11 -0
- package/systemd/skcapstone.service +36 -0
- package/systemd/skcapstone@.service +50 -0
- package/systemd/skcomm-heartbeat.service +18 -0
- package/systemd/skcomm-heartbeat.timer +12 -0
- package/systemd/skcomm-queue-drain.service +17 -0
- package/systemd/skcomm-queue-drain.timer +12 -0
- package/tests/conftest.py +13 -1
- package/tests/integration/__init__.py +1 -0
- package/tests/integration/test_consciousness_e2e.py +877 -0
- package/tests/integration/test_skills_registry.py +744 -0
- package/tests/test_agent_card.py +190 -0
- package/tests/test_agent_runtime.py +1283 -0
- package/tests/test_alerts_cmd.py +291 -0
- package/tests/test_archiver.py +498 -0
- package/tests/test_backup.py +254 -0
- package/tests/test_benchmark.py +366 -0
- package/tests/test_blueprints.py +457 -0
- package/tests/test_capabilities.py +257 -0
- package/tests/test_changelog.py +254 -0
- package/tests/test_chat.py +385 -0
- package/tests/test_claude_md.py +271 -0
- package/tests/test_cli_chat_llm.py +336 -0
- package/tests/test_cli_completions.py +390 -0
- package/tests/test_cli_init_reset.py +164 -0
- package/tests/test_cli_memory.py +208 -0
- package/tests/test_cli_profile.py +294 -0
- package/tests/test_cli_skills.py +223 -0
- package/tests/test_cli_status.py +395 -0
- package/tests/test_cli_test_cmd.py +206 -0
- package/tests/test_cli_test_connection.py +364 -0
- package/tests/test_cloud9_bridge.py +260 -0
- package/tests/test_cloud_provider.py +449 -0
- package/tests/test_cloud_providers.py +522 -0
- package/tests/test_completions.py +158 -0
- package/tests/test_component_manager.py +398 -0
- package/tests/test_config_reload.py +386 -0
- package/tests/test_config_validate.py +529 -0
- package/tests/test_consciousness_e2e.py +296 -0
- package/tests/test_consciousness_loop.py +1289 -0
- package/tests/test_context_loader.py +310 -0
- package/tests/test_conversation_api.py +306 -0
- package/tests/test_conversation_manager.py +381 -0
- package/tests/test_conversation_store.py +391 -0
- package/tests/test_conversation_summarizer.py +302 -0
- package/tests/test_cross_package.py +791 -0
- package/tests/test_crush_shim.py +519 -0
- package/tests/test_daemon.py +781 -0
- package/tests/test_daemon_shutdown.py +309 -0
- package/tests/test_dashboard.py +454 -0
- package/tests/test_discovery.py +200 -6
- package/tests/test_docker_provider.py +966 -0
- package/tests/test_doctor.py +257 -0
- package/tests/test_doctor_fix.py +351 -0
- package/tests/test_e2e_automated.py +292 -0
- package/tests/test_error_queue.py +404 -0
- package/tests/test_export.py +441 -0
- package/tests/test_fallback_tracker.py +219 -0
- package/tests/test_file_transfer.py +397 -0
- package/tests/test_fuse_mount.py +832 -0
- package/tests/test_health_loop.py +422 -0
- package/tests/test_heartbeat.py +354 -0
- package/tests/test_housekeeping.py +195 -0
- package/tests/test_identity_capauth.py +307 -0
- package/tests/test_identity_pillar.py +117 -0
- package/tests/test_install_wizard.py +68 -0
- package/tests/test_integration.py +325 -0
- package/tests/test_kms.py +495 -0
- package/tests/test_llm_providers.py +265 -0
- package/tests/test_local_provider.py +591 -0
- package/tests/test_log_config.py +199 -0
- package/tests/test_logs_cmd.py +287 -0
- package/tests/test_mcp_server.py +1909 -0
- package/tests/test_memory_adapter.py +339 -0
- package/tests/test_memory_curator.py +218 -0
- package/tests/test_memory_engine.py +6 -0
- package/tests/test_memory_fortress.py +571 -0
- package/tests/test_memory_pillar.py +119 -0
- package/tests/test_memory_promoter.py +445 -0
- package/tests/test_memory_verifier.py +420 -0
- package/tests/test_message_crypto.py +187 -0
- package/tests/test_metrics.py +632 -0
- package/tests/test_migrate_memories.py +464 -0
- package/tests/test_model_router.py +546 -0
- package/tests/test_mood.py +394 -0
- package/tests/test_multi_agent.py +269 -0
- package/tests/test_notifications.py +270 -0
- package/tests/test_onboard.py +500 -0
- package/tests/test_peer_directory.py +395 -0
- package/tests/test_peers.py +248 -0
- package/tests/test_pillars.py +87 -9
- package/tests/test_preflight.py +484 -0
- package/tests/test_prompt_adapter.py +331 -0
- package/tests/test_proxmox_provider.py +571 -0
- package/tests/test_pubsub.py +377 -0
- package/tests/test_rate_limiter.py +121 -0
- package/tests/test_registry_client.py +129 -0
- package/tests/test_response_cache.py +312 -0
- package/tests/test_response_scorer.py +294 -0
- package/tests/test_runtime.py +59 -0
- package/tests/test_scheduled_tasks.py +451 -0
- package/tests/test_security.py +250 -0
- package/tests/test_security_pillar.py +213 -0
- package/tests/test_self_healing.py +171 -0
- package/tests/test_session_capture.py +200 -0
- package/tests/test_session_recorder.py +360 -0
- package/tests/test_session_skills.py +235 -0
- package/tests/test_shell.py +210 -0
- package/tests/test_snapshots.py +549 -0
- package/tests/test_soul.py +984 -0
- package/tests/test_soul_swap.py +406 -0
- package/tests/test_spawner.py +211 -0
- package/tests/test_state_diff.py +173 -0
- package/tests/test_summary.py +135 -0
- package/tests/test_sync.py +315 -5
- package/tests/test_sync_backends.py +560 -0
- package/tests/test_sync_engine.py +482 -0
- package/tests/test_sync_pillar.py +344 -0
- package/tests/test_sync_pipeline.py +364 -0
- package/tests/test_sync_vault.py +581 -0
- package/tests/test_syncthing_setup.py +168 -22
- package/tests/test_systemd.py +323 -0
- package/tests/test_team_comms.py +408 -0
- package/tests/test_team_engine.py +397 -0
- package/tests/test_testrunner.py +238 -0
- package/tests/test_trust_calibration.py +204 -0
- package/tests/test_trust_graph.py +207 -0
- package/tests/test_trust_pillar.py +291 -0
- package/tests/test_trustee_cli.py +427 -0
- package/tests/test_trustee_cli_integration.py +325 -0
- package/tests/test_trustee_monitor.py +394 -0
- package/tests/test_trustee_ops.py +355 -0
- package/tests/test_unified_search.py +363 -0
- package/tests/test_uninstall_wizard.py +193 -0
- package/tests/test_usage.py +333 -0
- package/tests/test_version_cmd.py +355 -0
- package/tests/test_warmth_anchor.py +162 -0
- package/tests/test_whoami.py +245 -0
- package/tests/test_ws.py +311 -0
- package/.cursorrules +0 -33
- 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
|