@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,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SKCapstone Coordination Federation — Syncthing-based multi-instance task sync.
|
|
3
|
+
|
|
4
|
+
Watches ~/.skcapstone/coordination/ for incoming task and agent files from
|
|
5
|
+
peer instances. Handles last-writer-wins conflict resolution by mtime and
|
|
6
|
+
announces changes via the coord.sync pubsub topic.
|
|
7
|
+
|
|
8
|
+
Design:
|
|
9
|
+
- Uses watchdog (inotify on Linux) to detect file-system events
|
|
10
|
+
- Debounces events (Syncthing writes in stages)
|
|
11
|
+
- Resolves Syncthing conflict files by mtime: newer wins
|
|
12
|
+
- Publishes coord.sync messages so peers can react immediately
|
|
13
|
+
|
|
14
|
+
Syncthing conflict filename format:
|
|
15
|
+
original-name.sync-conflict-YYYYMMDD-HHMMSS-DEVICEID.json
|
|
16
|
+
→ canonical name: original-name.json (conflict suffix stripped)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import re
|
|
25
|
+
import time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Callable, Optional
|
|
28
|
+
|
|
29
|
+
from .pubsub import PubSub
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("skcapstone.coord_federation")
|
|
32
|
+
|
|
33
|
+
# Syncthing appends this pattern to the stem before the extension on conflict.
|
|
34
|
+
# e.g. "abc1-my-task.sync-conflict-20260302-120000-ABCDEF7.json"
|
|
35
|
+
_CONFLICT_RE = re.compile(r"\.sync-conflict-\d{8}-\d{6}-[A-Z0-9]+$", re.IGNORECASE)
|
|
36
|
+
|
|
37
|
+
# Pub/sub topic name for coordination sync events
|
|
38
|
+
COORD_SYNC_TOPIC = "coord.sync"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Watchdog event handler
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _CoordEventHandler:
|
|
47
|
+
"""Watchdog FileSystemEventHandler for coordination directory.
|
|
48
|
+
|
|
49
|
+
Filters to .json files only, debounces Syncthing multi-stage writes,
|
|
50
|
+
and dispatches to a callback on create or modify.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
callback: Called with the Path of each relevant changed file.
|
|
54
|
+
debounce_ms: Minimum ms between events for the same file path.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, callback: Callable[[Path], None], debounce_ms: int = 500) -> None:
|
|
58
|
+
self._callback = callback
|
|
59
|
+
self._debounce_ms = debounce_ms
|
|
60
|
+
self._last_event: dict[str, float] = {}
|
|
61
|
+
|
|
62
|
+
def _accept(self, path_str: str) -> bool:
|
|
63
|
+
"""Return True if this event should be processed (not debounced)."""
|
|
64
|
+
if not path_str.endswith(".json"):
|
|
65
|
+
return False
|
|
66
|
+
now = time.monotonic()
|
|
67
|
+
last = self._last_event.get(path_str, 0.0)
|
|
68
|
+
if (now - last) * 1000 < self._debounce_ms:
|
|
69
|
+
return False
|
|
70
|
+
self._last_event[path_str] = now
|
|
71
|
+
# Prune stale entries every ~60 s
|
|
72
|
+
cutoff = now - 60.0
|
|
73
|
+
self._last_event = {k: v for k, v in self._last_event.items() if v > cutoff}
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
def _dispatch(self, event) -> None:
|
|
77
|
+
if getattr(event, "is_directory", False):
|
|
78
|
+
return
|
|
79
|
+
src = getattr(event, "src_path", str(event))
|
|
80
|
+
if self._accept(src):
|
|
81
|
+
self._callback(Path(src))
|
|
82
|
+
|
|
83
|
+
def on_created(self, event) -> None: # noqa: D401
|
|
84
|
+
self._dispatch(event)
|
|
85
|
+
|
|
86
|
+
def on_modified(self, event) -> None: # noqa: D401
|
|
87
|
+
self._dispatch(event)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Federation watcher
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class CoordFederationWatcher:
|
|
96
|
+
"""Watches the coordination directory for incoming peer files.
|
|
97
|
+
|
|
98
|
+
Starts an inotify (watchdog) observer on:
|
|
99
|
+
<shared_root>/coordination/tasks/
|
|
100
|
+
<shared_root>/coordination/agents/
|
|
101
|
+
|
|
102
|
+
On every new or modified .json file:
|
|
103
|
+
1. If it is a Syncthing conflict file, resolve by mtime (newer wins).
|
|
104
|
+
2. Publish a coord.sync pubsub message announcing the change.
|
|
105
|
+
|
|
106
|
+
Thread safety: the watchdog observer runs in its own thread. File
|
|
107
|
+
callbacks are dispatched back to the asyncio event loop via
|
|
108
|
+
``asyncio.run_coroutine_threadsafe``.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
shared_root: Path to the shared ~/.skcapstone (or equivalent).
|
|
112
|
+
agent_name: Name used as the pubsub sender.
|
|
113
|
+
debounce_ms: Debounce window in milliseconds (default 500).
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
shared_root: Path,
|
|
119
|
+
agent_name: str = "anonymous",
|
|
120
|
+
debounce_ms: int = 500,
|
|
121
|
+
) -> None:
|
|
122
|
+
self._root = Path(shared_root).expanduser()
|
|
123
|
+
self._coord_dir = self._root / "coordination"
|
|
124
|
+
self._agent_name = agent_name
|
|
125
|
+
self._debounce_ms = debounce_ms
|
|
126
|
+
|
|
127
|
+
self._pubsub: Optional[PubSub] = None
|
|
128
|
+
self._observer = None # watchdog.Observer
|
|
129
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
# Lifecycle
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def start(self, loop: asyncio.AbstractEventLoop) -> None:
|
|
136
|
+
"""Start the inotify observer (call from the main asyncio thread).
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
loop: The running asyncio event loop (used for thread-safe dispatch).
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
from watchdog.observers import Observer
|
|
143
|
+
except ImportError:
|
|
144
|
+
logger.warning(
|
|
145
|
+
"watchdog not installed — coord federation disabled. "
|
|
146
|
+
"Install with: pip install watchdog"
|
|
147
|
+
)
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
self._loop = loop
|
|
151
|
+
self._pubsub = PubSub(self._root, agent_name=self._agent_name)
|
|
152
|
+
self._pubsub.initialize()
|
|
153
|
+
|
|
154
|
+
# Ensure the directories exist before watching
|
|
155
|
+
(self._coord_dir / "tasks").mkdir(parents=True, exist_ok=True)
|
|
156
|
+
(self._coord_dir / "agents").mkdir(parents=True, exist_ok=True)
|
|
157
|
+
|
|
158
|
+
handler = _CoordEventHandler(self._on_fs_event, debounce_ms=self._debounce_ms)
|
|
159
|
+
self._observer = Observer()
|
|
160
|
+
self._observer.schedule(handler, str(self._coord_dir), recursive=True)
|
|
161
|
+
self._observer.start()
|
|
162
|
+
logger.info("CoordFederationWatcher started on %s", self._coord_dir)
|
|
163
|
+
|
|
164
|
+
def stop(self) -> None:
|
|
165
|
+
"""Stop the inotify observer."""
|
|
166
|
+
if self._observer is not None:
|
|
167
|
+
self._observer.stop()
|
|
168
|
+
self._observer.join()
|
|
169
|
+
self._observer = None
|
|
170
|
+
logger.info("CoordFederationWatcher stopped")
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------------
|
|
173
|
+
# Event dispatch (watchdog thread → asyncio)
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
def _on_fs_event(self, path: Path) -> None:
|
|
177
|
+
"""Called from the watchdog thread. Forwards to the asyncio loop."""
|
|
178
|
+
if self._loop is not None and not self._loop.is_closed():
|
|
179
|
+
asyncio.run_coroutine_threadsafe(self._handle_change(path), self._loop)
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------------
|
|
182
|
+
# Core logic (runs in asyncio)
|
|
183
|
+
# ------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
async def _handle_change(self, path: Path) -> None:
|
|
186
|
+
"""Process one incoming file change from a peer."""
|
|
187
|
+
if not path.exists():
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# Check if this is a Syncthing conflict file
|
|
191
|
+
stem = path.stem # e.g. "abc1-task.sync-conflict-20260302-120000-ABC"
|
|
192
|
+
if _CONFLICT_RE.search(stem):
|
|
193
|
+
await self._resolve_conflict(path)
|
|
194
|
+
else:
|
|
195
|
+
await self._announce(path, event="synced")
|
|
196
|
+
|
|
197
|
+
async def _resolve_conflict(self, conflict_path: Path) -> None:
|
|
198
|
+
"""Resolve a Syncthing conflict file using last-writer-wins (mtime).
|
|
199
|
+
|
|
200
|
+
The conflict file's stem contains the `.sync-conflict-DATE-TIME-ID`
|
|
201
|
+
suffix. Strip it to find the canonical filename, then keep whichever
|
|
202
|
+
version has the newer mtime and delete the loser.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
conflict_path: Path to the `.sync-conflict-*.json` file.
|
|
206
|
+
"""
|
|
207
|
+
stem = conflict_path.stem
|
|
208
|
+
canonical_stem = _CONFLICT_RE.sub("", stem)
|
|
209
|
+
canonical_path = conflict_path.parent / f"{canonical_stem}.json"
|
|
210
|
+
|
|
211
|
+
if not canonical_path.exists():
|
|
212
|
+
# No canonical version — promote conflict to canonical
|
|
213
|
+
try:
|
|
214
|
+
conflict_path.rename(canonical_path)
|
|
215
|
+
logger.info("Promoted conflict to canonical: %s", canonical_path.name)
|
|
216
|
+
await self._announce(canonical_path, event="conflict_resolved")
|
|
217
|
+
except OSError as exc:
|
|
218
|
+
logger.warning("Could not promote conflict file: %s", exc)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
# Compare modification times
|
|
222
|
+
try:
|
|
223
|
+
conflict_mtime = conflict_path.stat().st_mtime
|
|
224
|
+
canonical_mtime = canonical_path.stat().st_mtime
|
|
225
|
+
except OSError:
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
if conflict_mtime > canonical_mtime:
|
|
229
|
+
# Conflict is newer — replace canonical
|
|
230
|
+
try:
|
|
231
|
+
conflict_path.replace(canonical_path)
|
|
232
|
+
logger.info(
|
|
233
|
+
"Conflict newer — replaced canonical: %s", canonical_path.name
|
|
234
|
+
)
|
|
235
|
+
await self._announce(canonical_path, event="conflict_resolved")
|
|
236
|
+
except OSError as exc:
|
|
237
|
+
logger.warning("Could not replace canonical with conflict: %s", exc)
|
|
238
|
+
else:
|
|
239
|
+
# Canonical is newer (or same age) — drop conflict
|
|
240
|
+
try:
|
|
241
|
+
conflict_path.unlink(missing_ok=True)
|
|
242
|
+
logger.debug("Dropped older conflict: %s", conflict_path.name)
|
|
243
|
+
except OSError as exc:
|
|
244
|
+
logger.warning("Could not remove conflict file: %s", exc)
|
|
245
|
+
|
|
246
|
+
async def _announce(self, path: Path, event: str = "synced") -> None:
|
|
247
|
+
"""Publish a coord.sync message for a changed coordination file.
|
|
248
|
+
|
|
249
|
+
Payload schema:
|
|
250
|
+
event — "synced" | "conflict_resolved"
|
|
251
|
+
kind — "tasks" | "agents" | "other"
|
|
252
|
+
file — filename (not full path)
|
|
253
|
+
task_id — (tasks only) task id from JSON
|
|
254
|
+
title — (tasks only) task title
|
|
255
|
+
agent — (agents only) agent name from JSON
|
|
256
|
+
source — announcing agent name
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
path: The canonical file that changed.
|
|
260
|
+
event: Human-readable event type.
|
|
261
|
+
"""
|
|
262
|
+
if self._pubsub is None:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
# Determine sub-directory kind
|
|
266
|
+
try:
|
|
267
|
+
rel = path.relative_to(self._coord_dir)
|
|
268
|
+
except ValueError:
|
|
269
|
+
return
|
|
270
|
+
kind = rel.parts[0] if rel.parts else "other" # "tasks" or "agents"
|
|
271
|
+
|
|
272
|
+
# Read file content for metadata
|
|
273
|
+
try:
|
|
274
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
275
|
+
except Exception:
|
|
276
|
+
data = {}
|
|
277
|
+
|
|
278
|
+
payload: dict = {
|
|
279
|
+
"event": event,
|
|
280
|
+
"kind": kind,
|
|
281
|
+
"file": path.name,
|
|
282
|
+
"source": self._agent_name,
|
|
283
|
+
}
|
|
284
|
+
if kind == "tasks":
|
|
285
|
+
payload["task_id"] = data.get("id")
|
|
286
|
+
payload["title"] = data.get("title")
|
|
287
|
+
payload["priority"] = data.get("priority")
|
|
288
|
+
elif kind == "agents":
|
|
289
|
+
payload["agent"] = data.get("agent")
|
|
290
|
+
payload["state"] = data.get("state")
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
self._pubsub.publish(COORD_SYNC_TOPIC, payload, ttl_seconds=3600)
|
|
294
|
+
logger.info("coord.sync: %s %s/%s", event, kind, path.name)
|
|
295
|
+
except Exception as exc:
|
|
296
|
+
logger.warning("Failed to publish coord.sync: %s", exc)
|
|
@@ -14,6 +14,7 @@ Directory layout:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
|
+
import re
|
|
17
18
|
import socket
|
|
18
19
|
import uuid
|
|
19
20
|
from datetime import datetime, timezone
|
|
@@ -24,6 +25,32 @@ from typing import Optional
|
|
|
24
25
|
from pydantic import BaseModel, Field
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
def _slugify_filename(text: str) -> str:
|
|
29
|
+
"""Convert text to a filesystem-safe slug.
|
|
30
|
+
|
|
31
|
+
Removes or replaces characters that are illegal in filenames:
|
|
32
|
+
- Forward slash (/) → dash (-)
|
|
33
|
+
- Backslash (\\) → dash (-)
|
|
34
|
+
- Colon (:) → dash (-)
|
|
35
|
+
- Other special chars → removed
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
text: Input string (e.g., task title)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Safe filename slug
|
|
42
|
+
"""
|
|
43
|
+
slug = text.lower().strip()
|
|
44
|
+
# Replace path separators and other illegal chars with dash
|
|
45
|
+
slug = re.sub(r'[/\\:*?"<>|]', '-', slug)
|
|
46
|
+
# Remove remaining non-word chars (except dash and space)
|
|
47
|
+
slug = re.sub(r'[^\w\s-]', '', slug)
|
|
48
|
+
# Convert spaces and underscores to single dash
|
|
49
|
+
slug = re.sub(r'[\s_]+', '-', slug)
|
|
50
|
+
# Remove leading/trailing dashes
|
|
51
|
+
return slug.strip('-')
|
|
52
|
+
|
|
53
|
+
|
|
27
54
|
class TaskPriority(str, Enum):
|
|
28
55
|
"""Task urgency levels."""
|
|
29
56
|
|
|
@@ -113,6 +140,8 @@ class Board:
|
|
|
113
140
|
|
|
114
141
|
Args:
|
|
115
142
|
home: Path to ~/.skcapstone (or test equivalent).
|
|
143
|
+
In multi-agent mode, pass the shared root (not the
|
|
144
|
+
per-agent home) so all agents see the same board.
|
|
116
145
|
"""
|
|
117
146
|
|
|
118
147
|
def __init__(self, home: Path) -> None:
|
|
@@ -137,7 +166,7 @@ class Board:
|
|
|
137
166
|
return tasks
|
|
138
167
|
for f in sorted(self.tasks_dir.glob("*.json")):
|
|
139
168
|
try:
|
|
140
|
-
data = json.loads(f.read_text())
|
|
169
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
141
170
|
tasks.append(Task.model_validate(data))
|
|
142
171
|
except (json.JSONDecodeError, Exception):
|
|
143
172
|
continue
|
|
@@ -154,7 +183,7 @@ class Board:
|
|
|
154
183
|
return agents
|
|
155
184
|
for f in sorted(self.agents_dir.glob("*.json")):
|
|
156
185
|
try:
|
|
157
|
-
data = json.loads(f.read_text())
|
|
186
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
158
187
|
agents.append(AgentFile.model_validate(data))
|
|
159
188
|
except (json.JSONDecodeError, Exception):
|
|
160
189
|
continue
|
|
@@ -172,7 +201,7 @@ class Board:
|
|
|
172
201
|
path = self.agents_dir / f"{name}.json"
|
|
173
202
|
if not path.exists():
|
|
174
203
|
return None
|
|
175
|
-
data = json.loads(path.read_text())
|
|
204
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
176
205
|
return AgentFile.model_validate(data)
|
|
177
206
|
|
|
178
207
|
def save_agent(self, agent: AgentFile) -> Path:
|
|
@@ -189,7 +218,7 @@ class Board:
|
|
|
189
218
|
path = self.agents_dir / f"{agent.agent}.json"
|
|
190
219
|
path.write_text(
|
|
191
220
|
json.dumps(agent.model_dump(), indent=2) + "\n"
|
|
192
|
-
)
|
|
221
|
+
, encoding="utf-8")
|
|
193
222
|
return path
|
|
194
223
|
|
|
195
224
|
def create_task(self, task: Task) -> Path:
|
|
@@ -202,13 +231,13 @@ class Board:
|
|
|
202
231
|
Path to the written file.
|
|
203
232
|
"""
|
|
204
233
|
self.ensure_dirs()
|
|
205
|
-
slug = task.title
|
|
234
|
+
slug = _slugify_filename(task.title)[:40]
|
|
206
235
|
# Reason: filename includes id + slug for human readability
|
|
207
236
|
filename = f"{task.id}-{slug}.json"
|
|
208
237
|
path = self.tasks_dir / filename
|
|
209
238
|
path.write_text(
|
|
210
239
|
json.dumps(task.model_dump(), indent=2) + "\n"
|
|
211
|
-
)
|
|
240
|
+
, encoding="utf-8")
|
|
212
241
|
return path
|
|
213
242
|
|
|
214
243
|
def get_task_views(self) -> list[TaskView]:
|
|
@@ -309,6 +338,10 @@ class Board:
|
|
|
309
338
|
if agent.current_task == task_id:
|
|
310
339
|
agent.current_task = agent.claimed_tasks[0] if agent.claimed_tasks else None
|
|
311
340
|
self.save_agent(agent)
|
|
341
|
+
|
|
342
|
+
# Mint Joules for completed task
|
|
343
|
+
_mint_joules_for_task(self, task_id, agent_name)
|
|
344
|
+
|
|
312
345
|
return agent
|
|
313
346
|
|
|
314
347
|
def generate_board_md(self) -> str:
|
|
@@ -380,10 +413,71 @@ class Board:
|
|
|
380
413
|
self.ensure_dirs()
|
|
381
414
|
content = self.generate_board_md()
|
|
382
415
|
path = self.coord_dir / "BOARD.md"
|
|
383
|
-
path.write_text(content)
|
|
416
|
+
path.write_text(content, encoding="utf-8")
|
|
384
417
|
return path
|
|
385
418
|
|
|
386
419
|
|
|
420
|
+
# Priority → (work_category, event_type, joules) mapping for Joule minting
|
|
421
|
+
_PRIORITY_JOULE_MAP: dict[str, tuple[str, str, int]] = {
|
|
422
|
+
"critical": ("development", "bug_fix", 500),
|
|
423
|
+
"high": ("development", "code_commit", 100),
|
|
424
|
+
"medium": ("community", "support_ticket", 50),
|
|
425
|
+
"low": ("development", "documentation", 25),
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _mint_joules_for_task(board: Board, task_id: str, agent_name: str) -> None:
|
|
430
|
+
"""Mint Joules via SKJoule when a task is completed.
|
|
431
|
+
|
|
432
|
+
Looks up the task to get priority/title, maps priority to a work
|
|
433
|
+
category and Joule amount, then calls JouleEngine.auto_tokenize_task().
|
|
434
|
+
Failures are silently caught so tokenization never blocks task completion.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
board: The Board instance (used to look up task data).
|
|
438
|
+
task_id: ID of the completed task.
|
|
439
|
+
agent_name: Agent who completed the task.
|
|
440
|
+
"""
|
|
441
|
+
try:
|
|
442
|
+
from .skjoule import JouleEngine
|
|
443
|
+
|
|
444
|
+
# Find the task to get its metadata
|
|
445
|
+
task_data: dict[str, object] = {"id": task_id, "completed_by": agent_name}
|
|
446
|
+
for t in board.load_tasks():
|
|
447
|
+
if t.id == task_id:
|
|
448
|
+
task_data.update(t.model_dump())
|
|
449
|
+
break
|
|
450
|
+
|
|
451
|
+
priority = str(task_data.get("priority", "medium"))
|
|
452
|
+
category, _event_type, joules = _PRIORITY_JOULE_MAP.get(
|
|
453
|
+
priority, ("community", "support_ticket", 50)
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Inject tags so auto_tokenize_task picks up the right category
|
|
457
|
+
tags = list(task_data.get("tags", [])) # type: ignore[arg-type]
|
|
458
|
+
if category == "development":
|
|
459
|
+
if "dev" not in tags and "development" not in tags:
|
|
460
|
+
tags.append("dev")
|
|
461
|
+
elif category == "community":
|
|
462
|
+
if "community" not in tags:
|
|
463
|
+
tags.append("community")
|
|
464
|
+
task_data["tags"] = tags
|
|
465
|
+
|
|
466
|
+
# Use assignee if available, else default to "lumina"
|
|
467
|
+
worker = task_data.get("completed_by") or task_data.get("created_by") or "lumina"
|
|
468
|
+
task_data["completed_by"] = worker
|
|
469
|
+
|
|
470
|
+
engine = JouleEngine()
|
|
471
|
+
record = engine.auto_tokenize_task(task_data)
|
|
472
|
+
|
|
473
|
+
if record:
|
|
474
|
+
title = task_data.get("title", task_id)
|
|
475
|
+
print(f"[SKJoule] Minted {record.joules} Joules for task: {title}")
|
|
476
|
+
except Exception:
|
|
477
|
+
# Never let tokenization failure block task completion
|
|
478
|
+
pass
|
|
479
|
+
|
|
480
|
+
|
|
387
481
|
_BRIEFING_PROTOCOL = """\
|
|
388
482
|
# SKCapstone Agent Coordination Protocol
|
|
389
483
|
|