@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,319 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model Router — automatic model selection based on task requirements.
|
|
3
|
+
|
|
4
|
+
Reads a TaskSignal (description, tags, privacy flags, token estimate) and
|
|
5
|
+
returns a RouteDecision that identifies the optimal model tier and a concrete
|
|
6
|
+
model name to use for the task.
|
|
7
|
+
|
|
8
|
+
Decision precedence:
|
|
9
|
+
1. privacy_sensitive=True → LOCAL tier (never leaves the node)
|
|
10
|
+
2. requires_localhost=True → LOCAL tier on the originating node
|
|
11
|
+
3. Tag-rule matching → highest-priority matching TagRule wins
|
|
12
|
+
4. Token-based fallback → estimated_tokens > 16 000 → REASON, else FAST
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
import yaml
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
|
+
|
|
23
|
+
from skcapstone.blueprints.schema import ModelTier
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Supporting models
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TagRule(BaseModel):
|
|
32
|
+
"""Maps a set of keywords to a model tier with a priority weight.
|
|
33
|
+
|
|
34
|
+
When any keyword in *keywords* matches a tag in the incoming
|
|
35
|
+
:class:`TaskSignal`, this rule is considered a candidate. Among all
|
|
36
|
+
candidates, the one with the highest *priority* wins.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
keywords: List[str] = Field(description="Keywords that trigger this rule")
|
|
40
|
+
tier: ModelTier = Field(description="Target tier when the rule fires")
|
|
41
|
+
priority: int = Field(default=0, description="Higher value wins on conflict")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TaskSignal(BaseModel):
|
|
45
|
+
"""Describes the nature of a task so the router can pick the right model.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
description: Human-readable summary of what the task involves.
|
|
49
|
+
tags: Free-form labels (e.g. ["code", "refactor"]).
|
|
50
|
+
requires_localhost: If True, the task must run on the originating node.
|
|
51
|
+
privacy_sensitive: If True, forces LOCAL tier regardless of other signals.
|
|
52
|
+
estimated_tokens: Rough token budget hint (context + expected output).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
description: str = Field(description="What the task is about")
|
|
56
|
+
tags: List[str] = Field(default_factory=list, description="Classification tags")
|
|
57
|
+
requires_localhost: bool = Field(
|
|
58
|
+
default=False, description="Must run on the originating node"
|
|
59
|
+
)
|
|
60
|
+
privacy_sensitive: bool = Field(
|
|
61
|
+
default=False, description="Forces LOCAL tier"
|
|
62
|
+
)
|
|
63
|
+
estimated_tokens: int = Field(
|
|
64
|
+
default=0, description="Estimated token usage hint"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RouteDecision(BaseModel):
|
|
69
|
+
"""The output of the router describing which model/tier to use.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
tier: Selected model tier.
|
|
73
|
+
model_name: Specific model identifier within that tier.
|
|
74
|
+
reasoning: Human-readable explanation of why this decision was made.
|
|
75
|
+
preferred_node: Optional node hostname if a specific node is required.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
tier: ModelTier
|
|
79
|
+
model_name: str
|
|
80
|
+
reasoning: str
|
|
81
|
+
preferred_node: Optional[str] = Field(
|
|
82
|
+
default=None, description="Specific node if required"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ModelRouterConfig(BaseModel):
|
|
87
|
+
"""Configuration for the :class:`ModelRouter`.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
tier_models: Maps tier name (e.g. ``"code"``) to an ordered list of
|
|
91
|
+
model names. The first entry is the preferred model for that tier.
|
|
92
|
+
tag_rules: Ordered list of keyword-to-tier mappings.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
tier_models: Dict[str, List[str]] = Field(
|
|
96
|
+
default_factory=dict,
|
|
97
|
+
description="Maps tier name to list of model names (first = preferred)",
|
|
98
|
+
)
|
|
99
|
+
tag_rules: List[TagRule] = Field(
|
|
100
|
+
default_factory=list,
|
|
101
|
+
description="Keyword→tier rules evaluated against task tags",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def default(cls) -> "ModelRouterConfig":
|
|
106
|
+
"""Return the default configuration with NVIDIA-aligned model assignments.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
ModelRouterConfig: Sensible defaults covering all five tiers and
|
|
110
|
+
the four primary tag-rule groups.
|
|
111
|
+
"""
|
|
112
|
+
return cls(
|
|
113
|
+
tier_models={
|
|
114
|
+
ModelTier.FAST.value: ["llama3.2", "qwen3-coder", "grok-3-mini"],
|
|
115
|
+
ModelTier.CODE.value: ["devstral", "qwen3-coder", "grok-3"],
|
|
116
|
+
ModelTier.REASON.value: ["deepseek-r1:8b", "llama3.1", "grok-3"],
|
|
117
|
+
ModelTier.NUANCE.value: ["moonshot-v1-128k", "claude-sonnet-4-5", "kimi-k2.5"],
|
|
118
|
+
ModelTier.LOCAL.value: ["llama3.2", "devstral"],
|
|
119
|
+
},
|
|
120
|
+
tag_rules=[
|
|
121
|
+
TagRule(
|
|
122
|
+
keywords=["code", "refactor", "debug", "test", "implement"],
|
|
123
|
+
tier=ModelTier.CODE,
|
|
124
|
+
priority=10,
|
|
125
|
+
),
|
|
126
|
+
TagRule(
|
|
127
|
+
keywords=[
|
|
128
|
+
"architecture",
|
|
129
|
+
"design",
|
|
130
|
+
"analyze",
|
|
131
|
+
"research",
|
|
132
|
+
"plan",
|
|
133
|
+
],
|
|
134
|
+
tier=ModelTier.REASON,
|
|
135
|
+
priority=10,
|
|
136
|
+
),
|
|
137
|
+
TagRule(
|
|
138
|
+
keywords=[
|
|
139
|
+
"marketing",
|
|
140
|
+
"creative",
|
|
141
|
+
"email",
|
|
142
|
+
"copy",
|
|
143
|
+
"comms",
|
|
144
|
+
"writing",
|
|
145
|
+
],
|
|
146
|
+
tier=ModelTier.NUANCE,
|
|
147
|
+
priority=10,
|
|
148
|
+
),
|
|
149
|
+
TagRule(
|
|
150
|
+
keywords=["format", "rename", "lint", "simple", "trivial"],
|
|
151
|
+
tier=ModelTier.FAST,
|
|
152
|
+
priority=10,
|
|
153
|
+
),
|
|
154
|
+
],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# Router
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
_LARGE_TOKEN_THRESHOLD = 16_000
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ModelRouter:
|
|
166
|
+
"""Routes a :class:`TaskSignal` to the most appropriate model tier and name.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
config: Router configuration containing tier-to-model mappings and
|
|
170
|
+
tag rules. Defaults to :meth:`ModelRouterConfig.default`.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def __init__(self, config: Optional[ModelRouterConfig] = None) -> None:
|
|
174
|
+
self.config: ModelRouterConfig = config or ModelRouterConfig.default()
|
|
175
|
+
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
# Public API
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def route(self, signal: TaskSignal) -> RouteDecision:
|
|
181
|
+
"""Select the optimal model tier and name for *signal*.
|
|
182
|
+
|
|
183
|
+
Decision precedence (first match wins):
|
|
184
|
+
1. ``privacy_sensitive=True`` → LOCAL
|
|
185
|
+
2. ``requires_localhost=True`` → LOCAL (pinned to originating node)
|
|
186
|
+
3. Tag-rule matching (highest-priority rule wins)
|
|
187
|
+
4. Token fallback: > 16 000 → REASON, otherwise FAST
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
signal: Describes the task to be routed.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
RouteDecision: Tier, concrete model name, reasoning, and optional
|
|
194
|
+
preferred node.
|
|
195
|
+
"""
|
|
196
|
+
# --- Privacy gate ---------------------------------------------------
|
|
197
|
+
if signal.privacy_sensitive:
|
|
198
|
+
return self._decide(
|
|
199
|
+
tier=ModelTier.LOCAL,
|
|
200
|
+
reasoning="Task marked privacy_sensitive; forced to LOCAL tier.",
|
|
201
|
+
preferred_node=None,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# --- Localhost gate --------------------------------------------------
|
|
205
|
+
if signal.requires_localhost:
|
|
206
|
+
return self._decide(
|
|
207
|
+
tier=ModelTier.LOCAL,
|
|
208
|
+
reasoning="Task requires localhost execution; forced to LOCAL tier.",
|
|
209
|
+
preferred_node="localhost",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# --- Tag-rule matching -----------------------------------------------
|
|
213
|
+
best_rule = self._best_tag_rule(signal.tags)
|
|
214
|
+
if best_rule is not None:
|
|
215
|
+
return self._decide(
|
|
216
|
+
tier=best_rule.tier,
|
|
217
|
+
reasoning=(
|
|
218
|
+
f"Tag rule matched (keywords={best_rule.keywords}, "
|
|
219
|
+
f"priority={best_rule.priority})."
|
|
220
|
+
),
|
|
221
|
+
preferred_node=None,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# --- Token-based fallback -------------------------------------------
|
|
225
|
+
if signal.estimated_tokens > _LARGE_TOKEN_THRESHOLD:
|
|
226
|
+
return self._decide(
|
|
227
|
+
tier=ModelTier.REASON,
|
|
228
|
+
reasoning=(
|
|
229
|
+
f"No tag rule matched; estimated_tokens={signal.estimated_tokens} "
|
|
230
|
+
f"exceeds {_LARGE_TOKEN_THRESHOLD}, using REASON tier."
|
|
231
|
+
),
|
|
232
|
+
preferred_node=None,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return self._decide(
|
|
236
|
+
tier=ModelTier.FAST,
|
|
237
|
+
reasoning=(
|
|
238
|
+
"No tag rule matched and token budget is small; defaulting to FAST tier."
|
|
239
|
+
),
|
|
240
|
+
preferred_node=None,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def from_config(cls, path: Path) -> "ModelRouter":
|
|
245
|
+
"""Load a :class:`ModelRouter` from a YAML configuration file.
|
|
246
|
+
|
|
247
|
+
The YAML file should serialise a :class:`ModelRouterConfig` dict, e.g.:
|
|
248
|
+
|
|
249
|
+
.. code-block:: yaml
|
|
250
|
+
|
|
251
|
+
tier_models:
|
|
252
|
+
fast: [nemotron-49b]
|
|
253
|
+
code: [devstral]
|
|
254
|
+
tag_rules:
|
|
255
|
+
- keywords: [code]
|
|
256
|
+
tier: code
|
|
257
|
+
priority: 10
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
path: Filesystem path to the YAML configuration file.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
ModelRouter: Initialised router using the loaded configuration.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
FileNotFoundError: If *path* does not exist.
|
|
267
|
+
ValueError: If the YAML content cannot be parsed into a valid config.
|
|
268
|
+
"""
|
|
269
|
+
raw: Any = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
270
|
+
config = ModelRouterConfig.model_validate(raw)
|
|
271
|
+
return cls(config=config)
|
|
272
|
+
|
|
273
|
+
# ------------------------------------------------------------------
|
|
274
|
+
# Private helpers
|
|
275
|
+
# ------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
def _best_tag_rule(self, tags: List[str]) -> Optional[TagRule]:
|
|
278
|
+
"""Return the highest-priority rule whose keywords overlap with *tags*.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
tags: List of tags from the incoming :class:`TaskSignal`.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
The matching :class:`TagRule` with the highest priority, or ``None``
|
|
285
|
+
if no rule matches.
|
|
286
|
+
"""
|
|
287
|
+
normalised = {t.lower() for t in tags}
|
|
288
|
+
best: Optional[TagRule] = None
|
|
289
|
+
for rule in self.config.tag_rules:
|
|
290
|
+
rule_keywords = {kw.lower() for kw in rule.keywords}
|
|
291
|
+
if rule_keywords & normalised: # any intersection
|
|
292
|
+
if best is None or rule.priority > best.priority:
|
|
293
|
+
best = rule
|
|
294
|
+
return best
|
|
295
|
+
|
|
296
|
+
def _decide(
|
|
297
|
+
self,
|
|
298
|
+
tier: ModelTier,
|
|
299
|
+
reasoning: str,
|
|
300
|
+
preferred_node: Optional[str],
|
|
301
|
+
) -> RouteDecision:
|
|
302
|
+
"""Build a :class:`RouteDecision` for *tier*, picking the first known model.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
tier: The selected model tier.
|
|
306
|
+
reasoning: Human-readable explanation.
|
|
307
|
+
preferred_node: Optional node constraint.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
RouteDecision: Fully populated routing decision.
|
|
311
|
+
"""
|
|
312
|
+
models = self.config.tier_models.get(tier.value, [])
|
|
313
|
+
model_name = models[0] if models else f"unknown-{tier.value}"
|
|
314
|
+
return RouteDecision(
|
|
315
|
+
tier=tier,
|
|
316
|
+
model_name=model_name,
|
|
317
|
+
reasoning=reasoning,
|
|
318
|
+
preferred_node=preferred_node,
|
|
319
|
+
)
|
package/src/skcapstone/models.py
CHANGED
|
@@ -88,6 +88,26 @@ class SyncState(BaseModel):
|
|
|
88
88
|
status: PillarStatus = PillarStatus.MISSING
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
class SkillsState(BaseModel):
|
|
92
|
+
"""SKSkills state — what the agent can DO.
|
|
93
|
+
|
|
94
|
+
Reflects the SKSkills installation at ~/.skskills/ and
|
|
95
|
+
the tools available to this agent via the skills registry.
|
|
96
|
+
Also tracks connectivity to the remote skills-registry at
|
|
97
|
+
skills.smilintux.org when available.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
installed: int = 0
|
|
101
|
+
loaded: int = 0
|
|
102
|
+
tools_available: int = 0
|
|
103
|
+
skill_names: list[str] = Field(default_factory=list)
|
|
104
|
+
skskills_home: Optional[Path] = None
|
|
105
|
+
registry_url: Optional[str] = None
|
|
106
|
+
registry_available: bool = False
|
|
107
|
+
remote_skill_count: int = 0
|
|
108
|
+
status: PillarStatus = PillarStatus.MISSING
|
|
109
|
+
|
|
110
|
+
|
|
91
111
|
class MemoryLayer(str, Enum):
|
|
92
112
|
"""Memory tier — determines retention and promotion."""
|
|
93
113
|
|
|
@@ -104,10 +124,13 @@ class MemoryEntry(BaseModel):
|
|
|
104
124
|
tags: list[str] = Field(default_factory=list)
|
|
105
125
|
source: str = "cli"
|
|
106
126
|
layer: MemoryLayer = MemoryLayer.SHORT_TERM
|
|
107
|
-
created_at: datetime = Field(
|
|
127
|
+
created_at: datetime = Field(
|
|
128
|
+
default_factory=lambda: datetime.now(__import__("datetime").timezone.utc)
|
|
129
|
+
)
|
|
108
130
|
accessed_at: Optional[datetime] = None
|
|
109
131
|
access_count: int = 0
|
|
110
132
|
importance: float = 0.5
|
|
133
|
+
soul_context: Optional[str] = None
|
|
111
134
|
metadata: dict = Field(default_factory=dict)
|
|
112
135
|
|
|
113
136
|
@property
|
|
@@ -156,6 +179,7 @@ class AgentManifest(BaseModel):
|
|
|
156
179
|
trust: TrustState = Field(default_factory=TrustState)
|
|
157
180
|
security: SecurityState = Field(default_factory=SecurityState)
|
|
158
181
|
sync: SyncState = Field(default_factory=SyncState)
|
|
182
|
+
skills: SkillsState = Field(default_factory=SkillsState)
|
|
159
183
|
|
|
160
184
|
connectors: list[ConnectorInfo] = Field(default_factory=list)
|
|
161
185
|
|
|
@@ -185,13 +209,14 @@ class AgentManifest(BaseModel):
|
|
|
185
209
|
|
|
186
210
|
@property
|
|
187
211
|
def pillar_summary(self) -> dict[str, PillarStatus]:
|
|
188
|
-
"""Quick view of all pillars including sync."""
|
|
212
|
+
"""Quick view of all pillars including sync and skills."""
|
|
189
213
|
return {
|
|
190
214
|
"identity": self.identity.status,
|
|
191
215
|
"memory": self.memory.status,
|
|
192
216
|
"trust": self.trust.status,
|
|
193
217
|
"security": self.security.status,
|
|
194
218
|
"sync": self.sync.status,
|
|
219
|
+
"skills": self.skills.status,
|
|
195
220
|
}
|
|
196
221
|
|
|
197
222
|
|
|
@@ -203,6 +228,9 @@ class SyncConfig(BaseModel):
|
|
|
203
228
|
sync_folder: Path = Path("~/.skcapstone/sync")
|
|
204
229
|
gpg_encrypt: bool = True
|
|
205
230
|
gpg_recipient: Optional[str] = None
|
|
231
|
+
# Known peer public GPG fingerprints — seeds are encrypted to all of these
|
|
232
|
+
# so each peer can independently decrypt seeds they receive.
|
|
233
|
+
peer_fingerprints: list[str] = Field(default_factory=list)
|
|
206
234
|
auto_push: bool = True
|
|
207
235
|
auto_pull: bool = True
|
|
208
236
|
syncthing_api_url: Optional[str] = None
|
|
@@ -213,11 +241,14 @@ class SyncConfig(BaseModel):
|
|
|
213
241
|
class AgentConfig(BaseModel):
|
|
214
242
|
"""Persistent configuration for the agent runtime."""
|
|
215
243
|
|
|
216
|
-
agent_name: str = "
|
|
244
|
+
agent_name: str = "sksovereign-agent"
|
|
217
245
|
auto_rehydrate: bool = True
|
|
218
246
|
auto_audit: bool = True
|
|
219
247
|
soul_path: Optional[Path] = None
|
|
220
|
-
memory_home: Path = Path("~/.
|
|
248
|
+
memory_home: Path = Path("~/.skcapstone")
|
|
221
249
|
trust_home: Path = Path("~/.cloud9")
|
|
222
250
|
default_connector: Optional[str] = None
|
|
223
251
|
sync: SyncConfig = Field(default_factory=SyncConfig)
|
|
252
|
+
capabilities: list[str] = Field(
|
|
253
|
+
default_factory=lambda: ["consciousness", "code", "chat", "memory"]
|
|
254
|
+
)
|