@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,379 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MemoryCompressor — LLM-powered compression of aged long-term memories.
|
|
3
|
+
|
|
4
|
+
Scans the long-term layer for memories older than 90 days, groups those
|
|
5
|
+
sharing common tags into sets of 5+, sends each group to the local LLM
|
|
6
|
+
for synthesis, stores the result as a single compressed memory, and
|
|
7
|
+
removes the originals.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
skcapstone memory compress # live run
|
|
11
|
+
skcapstone memory compress --dry-run # preview only, no changes
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import datetime, timedelta, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
from .memory_engine import delete, list_memories, store
|
|
23
|
+
from .models import MemoryEntry, MemoryLayer
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("skcapstone.memory_compressor")
|
|
26
|
+
|
|
27
|
+
# ── Public constants ────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
DEFAULT_AGE_DAYS: int = 90
|
|
30
|
+
DEFAULT_MIN_GROUP_SIZE: int = 5
|
|
31
|
+
COMPRESSED_TAG: str = "compressed"
|
|
32
|
+
|
|
33
|
+
_SYSTEM_PROMPT = (
|
|
34
|
+
"You are a memory synthesizer for a sovereign agent system. "
|
|
35
|
+
"You will receive a set of related memories grouped by topic. "
|
|
36
|
+
"Your task: produce a single comprehensive memory entry that preserves "
|
|
37
|
+
"all key facts, decisions, and insights from the originals. "
|
|
38
|
+
"Write as dense, continuous prose — no bullet points, no headers. "
|
|
39
|
+
"Maximum 400 words."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── Data classes ────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class CompressionGroup:
|
|
47
|
+
"""A set of long-term memories sharing a common tag.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
tag: The shared grouping tag.
|
|
51
|
+
entries: Memory entries belonging to this group.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
tag: str
|
|
55
|
+
entries: list[MemoryEntry] = field(default_factory=list)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class CompressionResult:
|
|
60
|
+
"""Outcome of a memory compression pass.
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
groups_found: Tag groups with >= min_group_size members.
|
|
64
|
+
groups_compressed: Groups successfully synthesized by LLM.
|
|
65
|
+
memories_compressed: Individual memories collapsed and removed.
|
|
66
|
+
compressed_ids: IDs of newly created synthesized memories.
|
|
67
|
+
dry_run: True when no changes were persisted.
|
|
68
|
+
errors: Per-group error messages encountered during LLM calls.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
groups_found: int = 0
|
|
72
|
+
groups_compressed: int = 0
|
|
73
|
+
memories_compressed: int = 0
|
|
74
|
+
compressed_ids: list[str] = field(default_factory=list)
|
|
75
|
+
dry_run: bool = False
|
|
76
|
+
errors: list[str] = field(default_factory=list)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── Core class ──────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
class MemoryCompressor:
|
|
82
|
+
"""Compress aged long-term memories with LLM synthesis.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
home: Agent home directory (e.g. ``~/.skcapstone``).
|
|
86
|
+
age_days: Minimum memory age in days before it is eligible
|
|
87
|
+
for compression. Default is 90 days.
|
|
88
|
+
min_group_size: Minimum group size before compression triggers.
|
|
89
|
+
Default is 5. A group = memories sharing the same tag.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
home: Path,
|
|
95
|
+
age_days: int = DEFAULT_AGE_DAYS,
|
|
96
|
+
min_group_size: int = DEFAULT_MIN_GROUP_SIZE,
|
|
97
|
+
) -> None:
|
|
98
|
+
self.home = Path(home)
|
|
99
|
+
self.age_days = age_days
|
|
100
|
+
self.min_group_size = min_group_size
|
|
101
|
+
|
|
102
|
+
# ── Public API ──────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
def compress(self, dry_run: bool = False, bridge=None) -> CompressionResult:
|
|
105
|
+
"""Run a compression pass over the long-term memory layer.
|
|
106
|
+
|
|
107
|
+
Finds memories older than ``age_days`` (excluding those already
|
|
108
|
+
tagged ``compressed``), groups them by shared tags, and for
|
|
109
|
+
each group of ``min_group_size`` or more: synthesizes a single
|
|
110
|
+
replacement memory via LLM and deletes the originals.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
dry_run: If True, report what would be done without
|
|
114
|
+
writing or deleting anything.
|
|
115
|
+
bridge: Optional pre-constructed LLMBridge instance.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
CompressionResult describing changes (or projections in
|
|
119
|
+
dry-run mode).
|
|
120
|
+
"""
|
|
121
|
+
result = CompressionResult(dry_run=dry_run)
|
|
122
|
+
|
|
123
|
+
candidates = self._find_candidates()
|
|
124
|
+
if not candidates:
|
|
125
|
+
logger.info("No long-term memories eligible for compression.")
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
groups = self._build_groups(candidates)
|
|
129
|
+
eligible = [g for g in groups if len(g.entries) >= self.min_group_size]
|
|
130
|
+
result.groups_found = len(eligible)
|
|
131
|
+
|
|
132
|
+
if not eligible:
|
|
133
|
+
logger.info(
|
|
134
|
+
"Found %d candidate memories but no tag group reached the "
|
|
135
|
+
"minimum size of %d.",
|
|
136
|
+
len(candidates),
|
|
137
|
+
self.min_group_size,
|
|
138
|
+
)
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
llm_bridge = bridge or self._make_bridge()
|
|
142
|
+
|
|
143
|
+
# Track IDs already processed so memories shared across tags aren't
|
|
144
|
+
# double-compressed when groups overlap.
|
|
145
|
+
processed_ids: set[str] = set()
|
|
146
|
+
|
|
147
|
+
# Process largest groups first for maximum coverage.
|
|
148
|
+
for group in sorted(eligible, key=lambda g: len(g.entries), reverse=True):
|
|
149
|
+
unprocessed = [e for e in group.entries if e.memory_id not in processed_ids]
|
|
150
|
+
if len(unprocessed) < self.min_group_size:
|
|
151
|
+
# After excluding already-handled memories this group is too small.
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
if dry_run:
|
|
155
|
+
result.groups_found += 0 # already counted
|
|
156
|
+
result.memories_compressed += len(unprocessed)
|
|
157
|
+
for e in unprocessed:
|
|
158
|
+
processed_ids.add(e.memory_id)
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
synthesized_id = self._compress_group(group, unprocessed, llm_bridge, result)
|
|
162
|
+
if synthesized_id:
|
|
163
|
+
result.groups_compressed += 1
|
|
164
|
+
result.memories_compressed += len(unprocessed)
|
|
165
|
+
result.compressed_ids.append(synthesized_id)
|
|
166
|
+
for e in unprocessed:
|
|
167
|
+
processed_ids.add(e.memory_id)
|
|
168
|
+
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
def find_eligible(self) -> list[CompressionGroup]:
|
|
172
|
+
"""Return tag groups eligible for compression (dry-run helper).
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
List of :class:`CompressionGroup` with >= min_group_size entries,
|
|
176
|
+
sorted largest first.
|
|
177
|
+
"""
|
|
178
|
+
candidates = self._find_candidates()
|
|
179
|
+
groups = self._build_groups(candidates)
|
|
180
|
+
eligible = [g for g in groups if len(g.entries) >= self.min_group_size]
|
|
181
|
+
return sorted(eligible, key=lambda g: len(g.entries), reverse=True)
|
|
182
|
+
|
|
183
|
+
# ── Private helpers ─────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
def _find_candidates(self) -> list[MemoryEntry]:
|
|
186
|
+
"""Load long-term memories older than age_days, skip compressed ones."""
|
|
187
|
+
cutoff = datetime.now(timezone.utc) - timedelta(days=self.age_days)
|
|
188
|
+
all_lt = list_memories(self.home, layer=MemoryLayer.LONG_TERM, limit=10000)
|
|
189
|
+
|
|
190
|
+
candidates = []
|
|
191
|
+
for entry in all_lt:
|
|
192
|
+
if COMPRESSED_TAG in entry.tags:
|
|
193
|
+
continue
|
|
194
|
+
if entry.created_at is None:
|
|
195
|
+
continue
|
|
196
|
+
# Normalize timezone: memories may be stored as naive UTC.
|
|
197
|
+
created = entry.created_at
|
|
198
|
+
if created.tzinfo is None:
|
|
199
|
+
created = created.replace(tzinfo=timezone.utc)
|
|
200
|
+
if created < cutoff:
|
|
201
|
+
candidates.append(entry)
|
|
202
|
+
|
|
203
|
+
logger.debug(
|
|
204
|
+
"Compression candidates: %d / %d long-term memories older than %d days.",
|
|
205
|
+
len(candidates),
|
|
206
|
+
len(all_lt),
|
|
207
|
+
self.age_days,
|
|
208
|
+
)
|
|
209
|
+
return candidates
|
|
210
|
+
|
|
211
|
+
def _build_groups(self, memories: list[MemoryEntry]) -> list[CompressionGroup]:
|
|
212
|
+
"""Group memories by shared tags.
|
|
213
|
+
|
|
214
|
+
Each unique tag becomes a group. A memory with multiple tags
|
|
215
|
+
may appear in several groups; overlap is resolved by
|
|
216
|
+
``compress()`` via ``processed_ids`` tracking.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
memories: Candidate memory entries.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of CompressionGroup objects.
|
|
223
|
+
"""
|
|
224
|
+
tag_map: dict[str, list[MemoryEntry]] = {}
|
|
225
|
+
for entry in memories:
|
|
226
|
+
for tag in entry.tags:
|
|
227
|
+
tag_map.setdefault(tag, []).append(entry)
|
|
228
|
+
|
|
229
|
+
return [CompressionGroup(tag=tag, entries=entries) for tag, entries in tag_map.items()]
|
|
230
|
+
|
|
231
|
+
def _compress_group(
|
|
232
|
+
self,
|
|
233
|
+
group: CompressionGroup,
|
|
234
|
+
entries: list[MemoryEntry],
|
|
235
|
+
bridge,
|
|
236
|
+
result: CompressionResult,
|
|
237
|
+
) -> Optional[str]:
|
|
238
|
+
"""Synthesize a group into one memory and delete the originals.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
group: The CompressionGroup being processed.
|
|
242
|
+
entries: The specific entries to compress (may be a subset
|
|
243
|
+
of group.entries after overlap removal).
|
|
244
|
+
bridge: LLMBridge instance.
|
|
245
|
+
result: CompressionResult to append errors to.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The new memory ID if successful, else None.
|
|
249
|
+
"""
|
|
250
|
+
prompt = self._build_prompt(group.tag, entries)
|
|
251
|
+
try:
|
|
252
|
+
synthesized_text = self._call_llm(bridge, prompt, group.tag, len(entries))
|
|
253
|
+
except Exception as exc:
|
|
254
|
+
msg = f"LLM call failed for tag '{group.tag}': {exc}"
|
|
255
|
+
logger.warning(msg)
|
|
256
|
+
result.errors.append(msg)
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
# Merge all original tags (union) and add the compressed marker.
|
|
260
|
+
merged_tags: list[str] = sorted(
|
|
261
|
+
{t for e in entries for t in e.tags} | {COMPRESSED_TAG}
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Take the highest importance from the group.
|
|
265
|
+
max_importance = max(e.importance for e in entries)
|
|
266
|
+
# Synthesized memory earns at least 0.85 since it distils many.
|
|
267
|
+
importance = max(max_importance, 0.85)
|
|
268
|
+
|
|
269
|
+
# Earliest created_at as provenance metadata.
|
|
270
|
+
oldest_ts = min(
|
|
271
|
+
(e.created_at for e in entries if e.created_at is not None),
|
|
272
|
+
default=None,
|
|
273
|
+
)
|
|
274
|
+
metadata: dict = {
|
|
275
|
+
"compressed_from": [e.memory_id for e in entries],
|
|
276
|
+
"compressed_tag": group.tag,
|
|
277
|
+
"compressed_count": len(entries),
|
|
278
|
+
}
|
|
279
|
+
if oldest_ts:
|
|
280
|
+
metadata["oldest_source_created_at"] = oldest_ts.isoformat()
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
new_entry = store(
|
|
284
|
+
home=self.home,
|
|
285
|
+
content=synthesized_text,
|
|
286
|
+
tags=merged_tags,
|
|
287
|
+
source="compressor",
|
|
288
|
+
importance=importance,
|
|
289
|
+
layer=MemoryLayer.LONG_TERM,
|
|
290
|
+
metadata=metadata,
|
|
291
|
+
)
|
|
292
|
+
except Exception as exc:
|
|
293
|
+
msg = f"Failed to store synthesized memory for tag '{group.tag}': {exc}"
|
|
294
|
+
logger.warning(msg)
|
|
295
|
+
result.errors.append(msg)
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
# Remove originals.
|
|
299
|
+
for entry in entries:
|
|
300
|
+
try:
|
|
301
|
+
delete(self.home, entry.memory_id)
|
|
302
|
+
except Exception as exc:
|
|
303
|
+
logger.warning("Failed to delete original memory %s: %s", entry.memory_id, exc)
|
|
304
|
+
|
|
305
|
+
logger.info(
|
|
306
|
+
"Compressed %d memories (tag=%s) → %s",
|
|
307
|
+
len(entries),
|
|
308
|
+
group.tag,
|
|
309
|
+
new_entry.memory_id,
|
|
310
|
+
)
|
|
311
|
+
return new_entry.memory_id
|
|
312
|
+
|
|
313
|
+
def _build_prompt(self, tag: str, entries: list[MemoryEntry]) -> str:
|
|
314
|
+
"""Format compression prompt for the LLM.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
tag: The grouping tag (context label).
|
|
318
|
+
entries: Memory entries to synthesize.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Formatted prompt string.
|
|
322
|
+
"""
|
|
323
|
+
lines = [
|
|
324
|
+
f"Compress the following {len(entries)} memories tagged '{tag}' into one:",
|
|
325
|
+
"",
|
|
326
|
+
]
|
|
327
|
+
for i, entry in enumerate(entries, start=1):
|
|
328
|
+
created_label = ""
|
|
329
|
+
if entry.created_at:
|
|
330
|
+
created_label = f" [{entry.created_at.strftime('%Y-%m-%d')}]"
|
|
331
|
+
lines.append(f"Memory {i}{created_label}:")
|
|
332
|
+
lines.append(entry.content.strip())
|
|
333
|
+
lines.append("")
|
|
334
|
+
|
|
335
|
+
lines.append(
|
|
336
|
+
"Write a single comprehensive memory that preserves all key facts and "
|
|
337
|
+
"decisions. Continuous prose, no lists or headers, max 400 words."
|
|
338
|
+
)
|
|
339
|
+
return "\n".join(lines)
|
|
340
|
+
|
|
341
|
+
def _call_llm(self, bridge, prompt: str, tag: str, n: int) -> str:
|
|
342
|
+
"""Invoke LLMBridge.generate() with the synthesis prompt.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
bridge: LLMBridge instance.
|
|
346
|
+
prompt: User prompt built by _build_prompt.
|
|
347
|
+
tag: Tag name (for TaskSignal description).
|
|
348
|
+
n: Number of memories being merged (for token estimate).
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Generated synthesized memory text.
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
from .model_router import TaskSignal
|
|
355
|
+
signal = TaskSignal(
|
|
356
|
+
description=f"Compress {n} memories tagged '{tag}'",
|
|
357
|
+
tags=["compression", "memory", tag],
|
|
358
|
+
estimated_tokens=len(prompt) // 4 + 512,
|
|
359
|
+
)
|
|
360
|
+
return bridge.generate(_SYSTEM_PROMPT, prompt, signal)
|
|
361
|
+
except ImportError:
|
|
362
|
+
# model_router not available — call bridge without signal.
|
|
363
|
+
return bridge.generate(_SYSTEM_PROMPT, prompt)
|
|
364
|
+
|
|
365
|
+
def _make_bridge(self):
|
|
366
|
+
"""Instantiate a default LLMBridge from ConsciousnessConfig.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
LLMBridge instance, or raises RuntimeError if unavailable.
|
|
370
|
+
"""
|
|
371
|
+
try:
|
|
372
|
+
from .consciousness_loop import ConsciousnessConfig, LLMBridge
|
|
373
|
+
config = ConsciousnessConfig()
|
|
374
|
+
return LLMBridge(config)
|
|
375
|
+
except ImportError as exc:
|
|
376
|
+
raise RuntimeError(
|
|
377
|
+
"LLMBridge is not available. Install the consciousness_loop "
|
|
378
|
+
"dependency or pass a bridge= argument."
|
|
379
|
+
) from exc
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory Curator — analyze, score, tag, promote, and deduplicate memories.
|
|
3
|
+
|
|
4
|
+
Runs a curation pass over the agent's memory store, identifying:
|
|
5
|
+
- Promotion candidates (short->mid, mid->long based on access/importance)
|
|
6
|
+
- Missing tags (auto-tags from content analysis)
|
|
7
|
+
- Duplicate/near-duplicate memories to merge
|
|
8
|
+
- Importance re-scoring based on current context
|
|
9
|
+
- Summary statistics for each memory layer
|
|
10
|
+
|
|
11
|
+
Tool-agnostic: works from any terminal, MCP, or the REPL shell.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
skcapstone memory curate # full curation pass
|
|
15
|
+
skcapstone memory curate --dry-run # preview without changes
|
|
16
|
+
skcapstone memory curate --promote # only run promotions
|
|
17
|
+
skcapstone memory curate --dedupe # only run deduplication
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import hashlib
|
|
23
|
+
import re
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
from .memory_engine import (
|
|
29
|
+
_entry_path,
|
|
30
|
+
_find_by_id,
|
|
31
|
+
_save_entry,
|
|
32
|
+
list_memories,
|
|
33
|
+
search,
|
|
34
|
+
)
|
|
35
|
+
from .models import MemoryEntry, MemoryLayer
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class CurationResult:
|
|
40
|
+
"""Results from a curation pass.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
promoted: Memories promoted to a higher tier.
|
|
44
|
+
tagged: Memories that received new auto-tags.
|
|
45
|
+
deduped: Memory IDs that were identified as duplicates.
|
|
46
|
+
total_scanned: Total memories examined.
|
|
47
|
+
by_layer: Count per layer after curation.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
promoted: list[str] = field(default_factory=list)
|
|
51
|
+
tagged: list[str] = field(default_factory=list)
|
|
52
|
+
deduped: list[str] = field(default_factory=list)
|
|
53
|
+
total_scanned: int = 0
|
|
54
|
+
by_layer: dict[str, int] = field(default_factory=dict)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Auto-tagging patterns (same as session_capture for consistency)
|
|
58
|
+
_TAG_PATTERNS: list[tuple[re.Pattern, str]] = [
|
|
59
|
+
(re.compile(r"\bcapauth\b", re.I), "capauth"),
|
|
60
|
+
(re.compile(r"\bskcapstone\b", re.I), "skcapstone"),
|
|
61
|
+
(re.compile(r"\bskmemory\b", re.I), "skmemory"),
|
|
62
|
+
(re.compile(r"\bskcomm\b", re.I), "skcomm"),
|
|
63
|
+
(re.compile(r"\bskchat\b", re.I), "skchat"),
|
|
64
|
+
(re.compile(r"\bsyncthing\b", re.I), "syncthing"),
|
|
65
|
+
(re.compile(r"\bMCP\b", re.I), "mcp"),
|
|
66
|
+
(re.compile(r"\bPGP\b|\bGPG\b", re.I), "pgp"),
|
|
67
|
+
(re.compile(r"\bDocker\b", re.I), "docker"),
|
|
68
|
+
(re.compile(r"\barchitect", re.I), "architecture"),
|
|
69
|
+
(re.compile(r"\bdecid", re.I), "decision"),
|
|
70
|
+
(re.compile(r"\bsecur|\bencrypt", re.I), "security"),
|
|
71
|
+
(re.compile(r"\bdeploy|\brelease", re.I), "deployment"),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class MemoryCurator:
|
|
76
|
+
"""Curates the agent's memory store.
|
|
77
|
+
|
|
78
|
+
Runs analysis passes to improve memory quality: auto-tagging,
|
|
79
|
+
promotion candidates, deduplication, and importance re-scoring.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
home: Agent home directory (~/.skcapstone).
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, home: Path) -> None:
|
|
86
|
+
self.home = home
|
|
87
|
+
|
|
88
|
+
def curate(
|
|
89
|
+
self,
|
|
90
|
+
dry_run: bool = False,
|
|
91
|
+
promote: bool = True,
|
|
92
|
+
dedupe: bool = True,
|
|
93
|
+
auto_tag: bool = True,
|
|
94
|
+
) -> CurationResult:
|
|
95
|
+
"""Run a full curation pass.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
dry_run: If True, report changes without applying them.
|
|
99
|
+
promote: Run the promotion pass.
|
|
100
|
+
dedupe: Run the deduplication pass.
|
|
101
|
+
auto_tag: Run the auto-tagging pass.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
CurationResult with all changes made (or proposed).
|
|
105
|
+
"""
|
|
106
|
+
result = CurationResult()
|
|
107
|
+
all_memories = list_memories(self.home, limit=10000)
|
|
108
|
+
result.total_scanned = len(all_memories)
|
|
109
|
+
|
|
110
|
+
for layer in MemoryLayer:
|
|
111
|
+
count = sum(1 for m in all_memories if m.layer == layer)
|
|
112
|
+
result.by_layer[layer.value] = count
|
|
113
|
+
|
|
114
|
+
if auto_tag:
|
|
115
|
+
self._pass_auto_tag(all_memories, result, dry_run)
|
|
116
|
+
|
|
117
|
+
if promote:
|
|
118
|
+
self._pass_promote(all_memories, result, dry_run)
|
|
119
|
+
|
|
120
|
+
if dedupe:
|
|
121
|
+
self._pass_dedupe(all_memories, result, dry_run)
|
|
122
|
+
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
def _pass_auto_tag(
|
|
126
|
+
self, memories: list[MemoryEntry], result: CurationResult, dry_run: bool
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Add missing tags based on content analysis."""
|
|
129
|
+
for entry in memories:
|
|
130
|
+
new_tags = _suggest_tags(entry.content, entry.tags)
|
|
131
|
+
if new_tags:
|
|
132
|
+
if not dry_run:
|
|
133
|
+
entry.tags = list(set(entry.tags + new_tags))
|
|
134
|
+
_save_entry(self.home, entry)
|
|
135
|
+
result.tagged.append(entry.memory_id)
|
|
136
|
+
|
|
137
|
+
def _pass_promote(
|
|
138
|
+
self, memories: list[MemoryEntry], result: CurationResult, dry_run: bool
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Promote memories that qualify for a higher tier."""
|
|
141
|
+
for entry in memories:
|
|
142
|
+
if not entry.should_promote:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
old_layer = entry.layer
|
|
146
|
+
if not dry_run:
|
|
147
|
+
old_path = _entry_path(self.home, entry)
|
|
148
|
+
if entry.layer == MemoryLayer.SHORT_TERM:
|
|
149
|
+
entry.layer = MemoryLayer.MID_TERM
|
|
150
|
+
elif entry.layer == MemoryLayer.MID_TERM:
|
|
151
|
+
entry.layer = MemoryLayer.LONG_TERM
|
|
152
|
+
|
|
153
|
+
if old_path.exists():
|
|
154
|
+
old_path.unlink()
|
|
155
|
+
_save_entry(self.home, entry)
|
|
156
|
+
|
|
157
|
+
result.promoted.append(entry.memory_id)
|
|
158
|
+
|
|
159
|
+
def _pass_dedupe(
|
|
160
|
+
self, memories: list[MemoryEntry], result: CurationResult, dry_run: bool
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Identify and remove near-duplicate memories."""
|
|
163
|
+
seen: dict[str, str] = {}
|
|
164
|
+
|
|
165
|
+
sorted_memories = sorted(
|
|
166
|
+
memories,
|
|
167
|
+
key=lambda m: (
|
|
168
|
+
{"long-term": 0, "mid-term": 1, "short-term": 2}.get(m.layer.value, 3),
|
|
169
|
+
-m.importance,
|
|
170
|
+
-m.access_count,
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
for entry in sorted_memories:
|
|
175
|
+
content_hash = _content_hash(entry.content)
|
|
176
|
+
|
|
177
|
+
if content_hash in seen:
|
|
178
|
+
result.deduped.append(entry.memory_id)
|
|
179
|
+
if not dry_run:
|
|
180
|
+
path = _entry_path(self.home, entry)
|
|
181
|
+
if path.exists():
|
|
182
|
+
path.unlink()
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
seen[content_hash] = entry.memory_id
|
|
186
|
+
|
|
187
|
+
def get_stats(self) -> dict:
|
|
188
|
+
"""Get curation-oriented statistics.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Dict with layer counts, tag coverage, and quality metrics.
|
|
192
|
+
"""
|
|
193
|
+
all_memories = list_memories(self.home, limit=10000)
|
|
194
|
+
total = len(all_memories)
|
|
195
|
+
if total == 0:
|
|
196
|
+
return {"total": 0, "layers": {}, "tag_coverage": 0.0, "promotion_candidates": 0}
|
|
197
|
+
|
|
198
|
+
by_layer = {}
|
|
199
|
+
for layer in MemoryLayer:
|
|
200
|
+
by_layer[layer.value] = sum(1 for m in all_memories if m.layer == layer)
|
|
201
|
+
|
|
202
|
+
tagged = sum(1 for m in all_memories if m.tags)
|
|
203
|
+
promotable = sum(1 for m in all_memories if m.should_promote)
|
|
204
|
+
avg_importance = sum(m.importance for m in all_memories) / total
|
|
205
|
+
|
|
206
|
+
top_tags: dict[str, int] = {}
|
|
207
|
+
for m in all_memories:
|
|
208
|
+
for t in m.tags:
|
|
209
|
+
top_tags[t] = top_tags.get(t, 0) + 1
|
|
210
|
+
|
|
211
|
+
sorted_tags = sorted(top_tags.items(), key=lambda x: -x[1])[:15]
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
"total": total,
|
|
215
|
+
"layers": by_layer,
|
|
216
|
+
"tag_coverage": round(tagged / total, 2),
|
|
217
|
+
"avg_importance": round(avg_importance, 2),
|
|
218
|
+
"promotion_candidates": promotable,
|
|
219
|
+
"top_tags": sorted_tags,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _suggest_tags(content: str, existing_tags: list[str]) -> list[str]:
|
|
224
|
+
"""Suggest new tags based on content analysis.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
content: Memory content text.
|
|
228
|
+
existing_tags: Already-applied tags.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
List of new tag suggestions (not already in existing_tags).
|
|
232
|
+
"""
|
|
233
|
+
suggestions: list[str] = []
|
|
234
|
+
existing_set = set(existing_tags)
|
|
235
|
+
|
|
236
|
+
for pattern, tag in _TAG_PATTERNS:
|
|
237
|
+
if tag not in existing_set and pattern.search(content):
|
|
238
|
+
suggestions.append(tag)
|
|
239
|
+
|
|
240
|
+
return suggestions
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _content_hash(content: str) -> str:
|
|
244
|
+
"""Generate a normalized hash for deduplication.
|
|
245
|
+
|
|
246
|
+
Normalizes whitespace and case before hashing to catch
|
|
247
|
+
near-identical content.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
content: Memory content text.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
MD5 hex digest of the normalized content.
|
|
254
|
+
"""
|
|
255
|
+
normalized = " ".join(content.lower().split())
|
|
256
|
+
return hashlib.md5(normalized.encode()).hexdigest()[:16]
|