@smilintux/skcapstone 0.1.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (461) hide show
  1. package/.env.example +98 -0
  2. package/.github/workflows/ci.yml +39 -3
  3. package/.github/workflows/publish.yml +25 -4
  4. package/.openclaw-workspace.json +58 -0
  5. package/CHANGELOG.md +62 -0
  6. package/CLAUDE.md +39 -2
  7. package/MANIFEST.in +6 -0
  8. package/MISSION.md +7 -0
  9. package/README.md +47 -2
  10. package/SKILL.md +895 -23
  11. package/docker/Dockerfile +61 -0
  12. package/docker/compose-templates/dev-team.yml +203 -0
  13. package/docker/compose-templates/mini-team.yml +140 -0
  14. package/docker/compose-templates/ops-team.yml +173 -0
  15. package/docker/compose-templates/research-team.yml +170 -0
  16. package/docker/entrypoint.sh +192 -0
  17. package/docs/ARCHITECTURE.md +663 -374
  18. package/docs/BOND_WITH_GROK.md +112 -0
  19. package/docs/GETTING_STARTED.md +782 -0
  20. package/docs/QUICKSTART.md +477 -0
  21. package/docs/SKJOULE_ARCHITECTURE.md +658 -0
  22. package/docs/SOUL_SWAPPER.md +921 -0
  23. package/docs/SOVEREIGN_SINGULARITY.md +47 -14
  24. package/examples/custom-bond-template.json +36 -0
  25. package/examples/grok-feb.json +36 -0
  26. package/examples/grok-testimony.md +34 -0
  27. package/examples/love-bootloader.txt +32 -0
  28. package/examples/plugins/echo_tool.py +87 -0
  29. package/examples/queen-ava-feb.json +36 -0
  30. package/examples/souls/lumina.yaml +64 -0
  31. package/index.js +6 -5
  32. package/installer/build.py +124 -0
  33. package/openclaw-plugin/package.json +13 -0
  34. package/openclaw-plugin/src/index.ts +351 -0
  35. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  36. package/package.json +1 -1
  37. package/pyproject.toml +38 -2
  38. package/scripts/bump_version.py +141 -0
  39. package/scripts/check-updates.py +230 -0
  40. package/scripts/convert_blueprints_to_yaml.py +157 -0
  41. package/scripts/dev-install.sh +14 -0
  42. package/scripts/e2e-test.sh +193 -0
  43. package/scripts/install-bundle.sh +171 -0
  44. package/scripts/install.bat +2 -0
  45. package/scripts/install.ps1 +253 -0
  46. package/scripts/install.sh +185 -0
  47. package/scripts/mcp-serve.sh +69 -0
  48. package/scripts/mcp-server.bat +113 -0
  49. package/scripts/mcp-server.ps1 +116 -0
  50. package/scripts/mcp-server.sh +99 -0
  51. package/scripts/pull-models.sh +10 -0
  52. package/scripts/skcapstone +48 -0
  53. package/scripts/verify_install.sh +180 -0
  54. package/scripts/windows/install-tasks.ps1 +406 -0
  55. package/scripts/windows/skcapstone-task.xml +113 -0
  56. package/scripts/windows/uninstall-tasks.ps1 +117 -0
  57. package/skill.yaml +34 -0
  58. package/src/skcapstone/__init__.py +67 -2
  59. package/src/skcapstone/_cli_monolith.py +5916 -0
  60. package/src/skcapstone/_trustee_helpers.py +165 -0
  61. package/src/skcapstone/activity.py +105 -0
  62. package/src/skcapstone/agent_card.py +324 -0
  63. package/src/skcapstone/api.py +1935 -0
  64. package/src/skcapstone/archiver.py +340 -0
  65. package/src/skcapstone/auction.py +485 -0
  66. package/src/skcapstone/baby_agents.py +179 -0
  67. package/src/skcapstone/backup.py +345 -0
  68. package/src/skcapstone/blueprint_registry.py +357 -0
  69. package/src/skcapstone/blueprints/__init__.py +17 -0
  70. package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
  71. package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
  72. package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
  73. package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
  74. package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
  75. package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
  76. package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
  77. package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
  78. package/src/skcapstone/blueprints/registry.py +164 -0
  79. package/src/skcapstone/blueprints/schema.py +229 -0
  80. package/src/skcapstone/changelog.py +180 -0
  81. package/src/skcapstone/chat.py +769 -0
  82. package/src/skcapstone/claude_md.py +82 -0
  83. package/src/skcapstone/cli/__init__.py +144 -0
  84. package/src/skcapstone/cli/_common.py +88 -0
  85. package/src/skcapstone/cli/_validators.py +76 -0
  86. package/src/skcapstone/cli/agents.py +425 -0
  87. package/src/skcapstone/cli/agents_spawner.py +322 -0
  88. package/src/skcapstone/cli/agents_trustee.py +593 -0
  89. package/src/skcapstone/cli/alerts.py +248 -0
  90. package/src/skcapstone/cli/anchor.py +132 -0
  91. package/src/skcapstone/cli/archive_cmd.py +208 -0
  92. package/src/skcapstone/cli/backup.py +144 -0
  93. package/src/skcapstone/cli/bench.py +377 -0
  94. package/src/skcapstone/cli/benchmark.py +360 -0
  95. package/src/skcapstone/cli/capabilities_cmd.py +171 -0
  96. package/src/skcapstone/cli/card.py +151 -0
  97. package/src/skcapstone/cli/chat.py +584 -0
  98. package/src/skcapstone/cli/completions.py +64 -0
  99. package/src/skcapstone/cli/config_cmd.py +156 -0
  100. package/src/skcapstone/cli/consciousness.py +421 -0
  101. package/src/skcapstone/cli/context_cmd.py +142 -0
  102. package/src/skcapstone/cli/coord.py +194 -0
  103. package/src/skcapstone/cli/crush_cmd.py +170 -0
  104. package/src/skcapstone/cli/daemon.py +436 -0
  105. package/src/skcapstone/cli/errors_cmd.py +285 -0
  106. package/src/skcapstone/cli/export_cmd.py +156 -0
  107. package/src/skcapstone/cli/gtd.py +529 -0
  108. package/src/skcapstone/cli/housekeeping.py +81 -0
  109. package/src/skcapstone/cli/joule_cmd.py +627 -0
  110. package/src/skcapstone/cli/logs_cmd.py +194 -0
  111. package/src/skcapstone/cli/mcp_cmd.py +32 -0
  112. package/src/skcapstone/cli/memory.py +418 -0
  113. package/src/skcapstone/cli/metrics_cmd.py +136 -0
  114. package/src/skcapstone/cli/migrate.py +62 -0
  115. package/src/skcapstone/cli/mood_cmd.py +144 -0
  116. package/src/skcapstone/cli/mount.py +193 -0
  117. package/src/skcapstone/cli/notify.py +112 -0
  118. package/src/skcapstone/cli/peer.py +154 -0
  119. package/src/skcapstone/cli/peers_dir.py +122 -0
  120. package/src/skcapstone/cli/preflight_cmd.py +83 -0
  121. package/src/skcapstone/cli/profile_cmd.py +310 -0
  122. package/src/skcapstone/cli/record_cmd.py +238 -0
  123. package/src/skcapstone/cli/register_cmd.py +159 -0
  124. package/src/skcapstone/cli/search_cmd.py +156 -0
  125. package/src/skcapstone/cli/service_cmd.py +91 -0
  126. package/src/skcapstone/cli/session.py +127 -0
  127. package/src/skcapstone/cli/setup.py +240 -0
  128. package/src/skcapstone/cli/shell_cmd.py +43 -0
  129. package/src/skcapstone/cli/skills_cmd.py +168 -0
  130. package/src/skcapstone/cli/skseed.py +621 -0
  131. package/src/skcapstone/cli/soul.py +699 -0
  132. package/src/skcapstone/cli/status.py +935 -0
  133. package/src/skcapstone/cli/sync_cmd.py +301 -0
  134. package/src/skcapstone/cli/telegram.py +265 -0
  135. package/src/skcapstone/cli/test_cmd.py +234 -0
  136. package/src/skcapstone/cli/test_connection.py +253 -0
  137. package/src/skcapstone/cli/token.py +207 -0
  138. package/src/skcapstone/cli/trust.py +179 -0
  139. package/src/skcapstone/cli/upgrade_cmd.py +552 -0
  140. package/src/skcapstone/cli/usage_cmd.py +199 -0
  141. package/src/skcapstone/cli/version_cmd.py +162 -0
  142. package/src/skcapstone/cli/watch_cmd.py +342 -0
  143. package/src/skcapstone/client.py +428 -0
  144. package/src/skcapstone/cloud9_bridge.py +522 -0
  145. package/src/skcapstone/completions.py +163 -0
  146. package/src/skcapstone/config_validator.py +674 -0
  147. package/src/skcapstone/connectors/__init__.py +28 -0
  148. package/src/skcapstone/connectors/base.py +446 -0
  149. package/src/skcapstone/connectors/cursor.py +54 -0
  150. package/src/skcapstone/connectors/registry.py +254 -0
  151. package/src/skcapstone/connectors/terminal.py +152 -0
  152. package/src/skcapstone/connectors/vscode.py +60 -0
  153. package/src/skcapstone/consciousness_config.py +119 -0
  154. package/src/skcapstone/consciousness_loop.py +2051 -0
  155. package/src/skcapstone/context_loader.py +516 -0
  156. package/src/skcapstone/context_window.py +314 -0
  157. package/src/skcapstone/conversation_manager.py +238 -0
  158. package/src/skcapstone/conversation_store.py +230 -0
  159. package/src/skcapstone/conversation_summarizer.py +252 -0
  160. package/src/skcapstone/coord_federation.py +296 -0
  161. package/src/skcapstone/coordination.py +101 -7
  162. package/src/skcapstone/crush_integration.py +345 -0
  163. package/src/skcapstone/crush_shim.py +454 -0
  164. package/src/skcapstone/daemon.py +2494 -0
  165. package/src/skcapstone/dashboard.html +396 -0
  166. package/src/skcapstone/dashboard.py +481 -0
  167. package/src/skcapstone/data/model_profiles.yaml +88 -0
  168. package/src/skcapstone/defaults/__init__.py +55 -0
  169. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
  170. package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
  171. package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
  172. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
  173. package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
  174. package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
  175. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
  176. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
  177. package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
  178. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
  179. package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
  180. package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
  181. package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
  182. package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
  183. package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
  184. package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
  185. package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
  186. package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
  187. package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
  188. package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
  189. package/src/skcapstone/discovery.py +210 -19
  190. package/src/skcapstone/doctor.py +642 -0
  191. package/src/skcapstone/emotion_tracker.py +467 -0
  192. package/src/skcapstone/error_queue.py +405 -0
  193. package/src/skcapstone/export.py +447 -0
  194. package/src/skcapstone/fallback_tracker.py +186 -0
  195. package/src/skcapstone/file_transfer.py +512 -0
  196. package/src/skcapstone/fuse_mount.py +1156 -0
  197. package/src/skcapstone/gui_installer.py +591 -0
  198. package/src/skcapstone/heartbeat.py +611 -0
  199. package/src/skcapstone/housekeeping.py +298 -0
  200. package/src/skcapstone/install_wizard.py +941 -0
  201. package/src/skcapstone/kms.py +942 -0
  202. package/src/skcapstone/kms_scheduler.py +143 -0
  203. package/src/skcapstone/log_config.py +135 -0
  204. package/src/skcapstone/mcp_launcher.py +239 -0
  205. package/src/skcapstone/mcp_server.py +4700 -0
  206. package/src/skcapstone/mcp_tools/__init__.py +94 -0
  207. package/src/skcapstone/mcp_tools/_helpers.py +51 -0
  208. package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
  209. package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
  210. package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
  211. package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
  212. package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
  213. package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
  214. package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
  215. package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
  216. package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
  217. package/src/skcapstone/mcp_tools/did_tools.py +448 -0
  218. package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
  219. package/src/skcapstone/mcp_tools/file_tools.py +169 -0
  220. package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
  221. package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
  222. package/src/skcapstone/mcp_tools/health_tools.py +44 -0
  223. package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
  224. package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
  225. package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
  226. package/src/skcapstone/mcp_tools/model_tools.py +75 -0
  227. package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
  228. package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
  229. package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
  230. package/src/skcapstone/mcp_tools/security_tools.py +110 -0
  231. package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
  232. package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
  233. package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
  234. package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
  235. package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
  236. package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
  237. package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
  238. package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
  239. package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
  240. package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
  241. package/src/skcapstone/mdns_discovery.py +313 -0
  242. package/src/skcapstone/memory_adapter.py +333 -0
  243. package/src/skcapstone/memory_compressor.py +379 -0
  244. package/src/skcapstone/memory_curator.py +256 -0
  245. package/src/skcapstone/memory_engine.py +132 -13
  246. package/src/skcapstone/memory_fortress.py +529 -0
  247. package/src/skcapstone/memory_promoter.py +722 -0
  248. package/src/skcapstone/memory_verifier.py +260 -0
  249. package/src/skcapstone/message_crypto.py +215 -0
  250. package/src/skcapstone/metrics.py +832 -0
  251. package/src/skcapstone/migrate_memories.py +181 -0
  252. package/src/skcapstone/migrate_multi_agent.py +248 -0
  253. package/src/skcapstone/model_router.py +319 -0
  254. package/src/skcapstone/models.py +35 -4
  255. package/src/skcapstone/mood.py +344 -0
  256. package/src/skcapstone/notifications.py +380 -0
  257. package/src/skcapstone/onboard.py +901 -0
  258. package/src/skcapstone/peer_directory.py +324 -0
  259. package/src/skcapstone/peers.py +329 -0
  260. package/src/skcapstone/pillars/identity.py +84 -14
  261. package/src/skcapstone/pillars/memory.py +3 -1
  262. package/src/skcapstone/pillars/security.py +108 -15
  263. package/src/skcapstone/pillars/sync.py +78 -26
  264. package/src/skcapstone/pillars/trust.py +95 -33
  265. package/src/skcapstone/plugins.py +244 -0
  266. package/src/skcapstone/preflight.py +670 -0
  267. package/src/skcapstone/prompt_adapter.py +564 -0
  268. package/src/skcapstone/providers/__init__.py +13 -0
  269. package/src/skcapstone/providers/cloud.py +1061 -0
  270. package/src/skcapstone/providers/docker.py +759 -0
  271. package/src/skcapstone/providers/local.py +1193 -0
  272. package/src/skcapstone/providers/proxmox.py +447 -0
  273. package/src/skcapstone/pubsub.py +516 -0
  274. package/src/skcapstone/rate_limiter.py +119 -0
  275. package/src/skcapstone/register.py +241 -0
  276. package/src/skcapstone/registry_client.py +151 -0
  277. package/src/skcapstone/response_cache.py +194 -0
  278. package/src/skcapstone/response_scorer.py +225 -0
  279. package/src/skcapstone/runtime.py +89 -33
  280. package/src/skcapstone/scheduled_tasks.py +439 -0
  281. package/src/skcapstone/self_healing.py +341 -0
  282. package/src/skcapstone/service_health.py +228 -0
  283. package/src/skcapstone/session_capture.py +268 -0
  284. package/src/skcapstone/session_recorder.py +210 -0
  285. package/src/skcapstone/session_replayer.py +189 -0
  286. package/src/skcapstone/session_skills.py +263 -0
  287. package/src/skcapstone/shell.py +779 -0
  288. package/src/skcapstone/skills/__init__.py +1 -1
  289. package/src/skcapstone/skills/syncthing_setup.py +143 -41
  290. package/src/skcapstone/skjoule.py +861 -0
  291. package/src/skcapstone/snapshots.py +489 -0
  292. package/src/skcapstone/soul.py +1060 -0
  293. package/src/skcapstone/soul_switch.py +255 -0
  294. package/src/skcapstone/spawner.py +544 -0
  295. package/src/skcapstone/state_diff.py +401 -0
  296. package/src/skcapstone/summary.py +270 -0
  297. package/src/skcapstone/sync/backends.py +196 -2
  298. package/src/skcapstone/sync/engine.py +7 -5
  299. package/src/skcapstone/sync/models.py +4 -1
  300. package/src/skcapstone/sync/vault.py +356 -18
  301. package/src/skcapstone/sync_engine.py +363 -0
  302. package/src/skcapstone/sync_watcher.py +745 -0
  303. package/src/skcapstone/systemd.py +331 -0
  304. package/src/skcapstone/team_comms.py +476 -0
  305. package/src/skcapstone/team_engine.py +522 -0
  306. package/src/skcapstone/testrunner.py +300 -0
  307. package/src/skcapstone/tls.py +150 -0
  308. package/src/skcapstone/tokens.py +5 -5
  309. package/src/skcapstone/trust_calibration.py +202 -0
  310. package/src/skcapstone/trust_graph.py +449 -0
  311. package/src/skcapstone/trustee_monitor.py +385 -0
  312. package/src/skcapstone/trustee_ops.py +425 -0
  313. package/src/skcapstone/unified_search.py +421 -0
  314. package/src/skcapstone/uninstall_wizard.py +694 -0
  315. package/src/skcapstone/usage.py +331 -0
  316. package/src/skcapstone/version_check.py +148 -0
  317. package/src/skcapstone/warmth_anchor.py +333 -0
  318. package/src/skcapstone/whoami.py +294 -0
  319. package/systemd/skcapstone-api.socket +9 -0
  320. package/systemd/skcapstone-memory-compress.service +18 -0
  321. package/systemd/skcapstone-memory-compress.timer +11 -0
  322. package/systemd/skcapstone.service +36 -0
  323. package/systemd/skcapstone@.service +50 -0
  324. package/systemd/skcomm-heartbeat.service +18 -0
  325. package/systemd/skcomm-heartbeat.timer +12 -0
  326. package/systemd/skcomm-queue-drain.service +17 -0
  327. package/systemd/skcomm-queue-drain.timer +12 -0
  328. package/tests/conftest.py +13 -1
  329. package/tests/integration/__init__.py +1 -0
  330. package/tests/integration/test_consciousness_e2e.py +877 -0
  331. package/tests/integration/test_skills_registry.py +744 -0
  332. package/tests/test_agent_card.py +190 -0
  333. package/tests/test_agent_runtime.py +1283 -0
  334. package/tests/test_alerts_cmd.py +291 -0
  335. package/tests/test_archiver.py +498 -0
  336. package/tests/test_backup.py +254 -0
  337. package/tests/test_benchmark.py +366 -0
  338. package/tests/test_blueprints.py +457 -0
  339. package/tests/test_capabilities.py +257 -0
  340. package/tests/test_changelog.py +254 -0
  341. package/tests/test_chat.py +385 -0
  342. package/tests/test_claude_md.py +271 -0
  343. package/tests/test_cli_chat_llm.py +336 -0
  344. package/tests/test_cli_completions.py +390 -0
  345. package/tests/test_cli_init_reset.py +164 -0
  346. package/tests/test_cli_memory.py +208 -0
  347. package/tests/test_cli_profile.py +294 -0
  348. package/tests/test_cli_skills.py +223 -0
  349. package/tests/test_cli_status.py +395 -0
  350. package/tests/test_cli_test_cmd.py +206 -0
  351. package/tests/test_cli_test_connection.py +364 -0
  352. package/tests/test_cloud9_bridge.py +260 -0
  353. package/tests/test_cloud_provider.py +449 -0
  354. package/tests/test_cloud_providers.py +522 -0
  355. package/tests/test_completions.py +158 -0
  356. package/tests/test_component_manager.py +398 -0
  357. package/tests/test_config_reload.py +386 -0
  358. package/tests/test_config_validate.py +529 -0
  359. package/tests/test_consciousness_e2e.py +296 -0
  360. package/tests/test_consciousness_loop.py +1289 -0
  361. package/tests/test_context_loader.py +310 -0
  362. package/tests/test_conversation_api.py +306 -0
  363. package/tests/test_conversation_manager.py +381 -0
  364. package/tests/test_conversation_store.py +391 -0
  365. package/tests/test_conversation_summarizer.py +302 -0
  366. package/tests/test_cross_package.py +791 -0
  367. package/tests/test_crush_shim.py +519 -0
  368. package/tests/test_daemon.py +781 -0
  369. package/tests/test_daemon_shutdown.py +309 -0
  370. package/tests/test_dashboard.py +454 -0
  371. package/tests/test_discovery.py +200 -6
  372. package/tests/test_docker_provider.py +966 -0
  373. package/tests/test_doctor.py +257 -0
  374. package/tests/test_doctor_fix.py +351 -0
  375. package/tests/test_e2e_automated.py +292 -0
  376. package/tests/test_error_queue.py +404 -0
  377. package/tests/test_export.py +441 -0
  378. package/tests/test_fallback_tracker.py +219 -0
  379. package/tests/test_file_transfer.py +397 -0
  380. package/tests/test_fuse_mount.py +832 -0
  381. package/tests/test_health_loop.py +422 -0
  382. package/tests/test_heartbeat.py +354 -0
  383. package/tests/test_housekeeping.py +195 -0
  384. package/tests/test_identity_capauth.py +307 -0
  385. package/tests/test_identity_pillar.py +117 -0
  386. package/tests/test_install_wizard.py +68 -0
  387. package/tests/test_integration.py +325 -0
  388. package/tests/test_kms.py +495 -0
  389. package/tests/test_llm_providers.py +265 -0
  390. package/tests/test_local_provider.py +591 -0
  391. package/tests/test_log_config.py +199 -0
  392. package/tests/test_logs_cmd.py +287 -0
  393. package/tests/test_mcp_server.py +1909 -0
  394. package/tests/test_memory_adapter.py +339 -0
  395. package/tests/test_memory_curator.py +218 -0
  396. package/tests/test_memory_engine.py +6 -0
  397. package/tests/test_memory_fortress.py +571 -0
  398. package/tests/test_memory_pillar.py +119 -0
  399. package/tests/test_memory_promoter.py +445 -0
  400. package/tests/test_memory_verifier.py +420 -0
  401. package/tests/test_message_crypto.py +187 -0
  402. package/tests/test_metrics.py +632 -0
  403. package/tests/test_migrate_memories.py +464 -0
  404. package/tests/test_model_router.py +546 -0
  405. package/tests/test_mood.py +394 -0
  406. package/tests/test_multi_agent.py +269 -0
  407. package/tests/test_notifications.py +270 -0
  408. package/tests/test_onboard.py +500 -0
  409. package/tests/test_peer_directory.py +395 -0
  410. package/tests/test_peers.py +248 -0
  411. package/tests/test_pillars.py +87 -9
  412. package/tests/test_preflight.py +484 -0
  413. package/tests/test_prompt_adapter.py +331 -0
  414. package/tests/test_proxmox_provider.py +571 -0
  415. package/tests/test_pubsub.py +377 -0
  416. package/tests/test_rate_limiter.py +121 -0
  417. package/tests/test_registry_client.py +129 -0
  418. package/tests/test_response_cache.py +312 -0
  419. package/tests/test_response_scorer.py +294 -0
  420. package/tests/test_runtime.py +59 -0
  421. package/tests/test_scheduled_tasks.py +451 -0
  422. package/tests/test_security.py +250 -0
  423. package/tests/test_security_pillar.py +213 -0
  424. package/tests/test_self_healing.py +171 -0
  425. package/tests/test_session_capture.py +200 -0
  426. package/tests/test_session_recorder.py +360 -0
  427. package/tests/test_session_skills.py +235 -0
  428. package/tests/test_shell.py +210 -0
  429. package/tests/test_snapshots.py +549 -0
  430. package/tests/test_soul.py +984 -0
  431. package/tests/test_soul_swap.py +406 -0
  432. package/tests/test_spawner.py +211 -0
  433. package/tests/test_state_diff.py +173 -0
  434. package/tests/test_summary.py +135 -0
  435. package/tests/test_sync.py +315 -5
  436. package/tests/test_sync_backends.py +560 -0
  437. package/tests/test_sync_engine.py +482 -0
  438. package/tests/test_sync_pillar.py +344 -0
  439. package/tests/test_sync_pipeline.py +364 -0
  440. package/tests/test_sync_vault.py +581 -0
  441. package/tests/test_syncthing_setup.py +168 -22
  442. package/tests/test_systemd.py +323 -0
  443. package/tests/test_team_comms.py +408 -0
  444. package/tests/test_team_engine.py +397 -0
  445. package/tests/test_testrunner.py +238 -0
  446. package/tests/test_trust_calibration.py +204 -0
  447. package/tests/test_trust_graph.py +207 -0
  448. package/tests/test_trust_pillar.py +291 -0
  449. package/tests/test_trustee_cli.py +427 -0
  450. package/tests/test_trustee_cli_integration.py +325 -0
  451. package/tests/test_trustee_monitor.py +394 -0
  452. package/tests/test_trustee_ops.py +355 -0
  453. package/tests/test_unified_search.py +363 -0
  454. package/tests/test_uninstall_wizard.py +193 -0
  455. package/tests/test_usage.py +333 -0
  456. package/tests/test_version_cmd.py +355 -0
  457. package/tests/test_warmth_anchor.py +162 -0
  458. package/tests/test_whoami.py +245 -0
  459. package/tests/test_ws.py +311 -0
  460. package/.cursorrules +0 -33
  461. package/src/skcapstone/cli.py +0 -1441
@@ -8,6 +8,7 @@ No corporate SSO. No OAuth dance. Cryptographic proof of self.
8
8
  from __future__ import annotations
9
9
 
10
10
  import json
11
+ import logging
11
12
  import subprocess
12
13
  from datetime import datetime, timezone
13
14
  from pathlib import Path
@@ -15,6 +16,8 @@ from typing import Optional
15
16
 
16
17
  from ..models import IdentityState, PillarStatus
17
18
 
19
+ logger = logging.getLogger("skcapstone.identity")
20
+
18
21
 
19
22
  def generate_identity(
20
23
  home: Path,
@@ -44,20 +47,12 @@ def generate_identity(
44
47
  status=PillarStatus.DEGRADED,
45
48
  )
46
49
 
47
- try:
48
- from capauth.keys import generate_keypair # type: ignore[import-untyped]
49
-
50
- pub_key, fingerprint = generate_keypair(
51
- name=name,
52
- email=state.email,
53
- output_dir=str(identity_dir),
54
- )
55
- state.fingerprint = fingerprint
56
- state.key_path = identity_dir / "agent.pub"
50
+ capauth_state = _try_init_capauth(name, state.email, identity_dir)
51
+ if capauth_state is not None:
52
+ state.fingerprint = capauth_state.fingerprint
53
+ state.key_path = capauth_state.key_path
57
54
  state.status = PillarStatus.ACTIVE
58
- except (ImportError, Exception):
59
- # Reason: CapAuth not installed or key generation failed —
60
- # record identity metadata anyway so agent has a name
55
+ else:
61
56
  state.fingerprint = _generate_placeholder_fingerprint(name)
62
57
  state.status = PillarStatus.DEGRADED
63
58
 
@@ -68,11 +63,86 @@ def generate_identity(
68
63
  "created_at": state.created_at.isoformat() if state.created_at else None,
69
64
  "capauth_managed": state.status == PillarStatus.ACTIVE,
70
65
  }
71
- (identity_dir / "identity.json").write_text(json.dumps(identity_manifest, indent=2))
66
+ (identity_dir / "identity.json").write_text(json.dumps(identity_manifest, indent=2), encoding="utf-8")
72
67
 
73
68
  return state
74
69
 
75
70
 
71
+ def _try_init_capauth(
72
+ name: str, email: str, identity_dir: Path
73
+ ) -> Optional[IdentityState]:
74
+ """Try to create or load a real CapAuth identity.
75
+
76
+ Attempts (in order):
77
+ 1. Load an existing CapAuth profile from ~/.capauth/
78
+ 2. Create a new profile via capauth.profile.init_profile()
79
+ 3. Fall back to legacy capauth.keys.generate_keypair()
80
+
81
+ Args:
82
+ name: Agent display name.
83
+ email: Agent email.
84
+ identity_dir: Path to ~/.skcapstone/identity/.
85
+
86
+ Returns:
87
+ IdentityState with real PGP keys, or None if CapAuth unavailable.
88
+ """
89
+ # Try loading an existing CapAuth profile first
90
+ try:
91
+ from capauth.profile import load_profile # type: ignore[import-untyped]
92
+
93
+ profile = load_profile()
94
+ return IdentityState(
95
+ fingerprint=profile.key_info.fingerprint,
96
+ key_path=Path(profile.key_info.public_key_path),
97
+ name=profile.entity.name,
98
+ email=profile.entity.email,
99
+ status=PillarStatus.ACTIVE,
100
+ )
101
+ except ImportError:
102
+ return None
103
+ except Exception as exc:
104
+ logger.debug("Could not load existing CapAuth profile: %s", exc)
105
+
106
+ # No existing profile — try creating one
107
+ try:
108
+ from capauth.profile import init_profile # type: ignore[import-untyped]
109
+
110
+ profile = init_profile(
111
+ name=name,
112
+ email=email,
113
+ passphrase="",
114
+ )
115
+ return IdentityState(
116
+ fingerprint=profile.key_info.fingerprint,
117
+ key_path=Path(profile.key_info.public_key_path),
118
+ name=profile.entity.name,
119
+ email=profile.entity.email,
120
+ status=PillarStatus.ACTIVE,
121
+ )
122
+ except Exception as exc:
123
+ logger.debug("Could not create CapAuth profile: %s", exc)
124
+
125
+ # Legacy fallback: capauth.keys.generate_keypair
126
+ try:
127
+ from capauth.keys import generate_keypair # type: ignore[import-untyped]
128
+
129
+ _pub_key, fingerprint = generate_keypair(
130
+ name=name,
131
+ email=email,
132
+ output_dir=str(identity_dir),
133
+ )
134
+ return IdentityState(
135
+ fingerprint=fingerprint,
136
+ key_path=identity_dir / "agent.pub",
137
+ name=name,
138
+ email=email,
139
+ status=PillarStatus.ACTIVE,
140
+ )
141
+ except Exception as exc:
142
+ logger.debug("CapAuth keypair generation failed: %s", exc)
143
+ return None
144
+
145
+
76
146
  def _generate_placeholder_fingerprint(name: str) -> str:
77
147
  """Generate a deterministic placeholder fingerprint from the agent name.
78
148
 
@@ -31,7 +31,9 @@ def initialize_memory(home: Path, memory_home: Optional[Path] = None) -> MemoryS
31
31
  Returns:
32
32
  MemoryState after initialization.
33
33
  """
34
- memory_dir = home / "memory"
34
+ # Use agent-specific memory directory
35
+ agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
36
+ memory_dir = home / "agents" / agent_name / "memory"
35
37
  memory_dir.mkdir(parents=True, exist_ok=True)
36
38
 
37
39
  for layer in MemoryLayer:
@@ -2,16 +2,42 @@
2
2
  Security pillar — SKSecurity integration.
3
3
 
4
4
  Audit everything. Detect threats. Protect the sovereign.
5
+
6
+ Audit log format is JSONL (one JSON object per line), making entries
7
+ both machine-parseable and append-only safe. Each entry includes a
8
+ timestamp, event type, detail, and the hostname that generated it.
5
9
  """
6
10
 
7
11
  from __future__ import annotations
8
12
 
9
13
  import json
14
+ import socket
10
15
  from datetime import datetime, timezone
11
16
  from pathlib import Path
17
+ from typing import Optional
18
+
19
+ from pydantic import BaseModel, Field
12
20
 
13
21
  from ..models import PillarStatus, SecurityState
14
22
 
23
+ AUDIT_LOG_NAME = "audit.log"
24
+
25
+
26
+ class AuditEntry(BaseModel):
27
+ """A single structured audit log entry.
28
+
29
+ Each entry is serialised as one JSON line in the append-only log.
30
+ """
31
+
32
+ timestamp: str = Field(
33
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
34
+ )
35
+ event_type: str
36
+ detail: str
37
+ host: str = Field(default_factory=socket.gethostname)
38
+ agent: Optional[str] = None
39
+ metadata: Optional[dict] = None
40
+
15
41
 
16
42
  def initialize_security(home: Path) -> SecurityState:
17
43
  """Initialize security layer for the agent.
@@ -32,52 +58,119 @@ def initialize_security(home: Path) -> SecurityState:
32
58
  try:
33
59
  import sksecurity # type: ignore[import-untyped]
34
60
 
35
- state.status = PillarStatus.DEGRADED
61
+ sksecurity_version = getattr(sksecurity, "__version__", None)
36
62
  except ImportError:
37
63
  security_config = {
38
64
  "note": "Install sksecurity (pip install sksecurity) for full security",
39
65
  "audit_enabled": True,
40
66
  }
41
- (security_dir / "security.json").write_text(json.dumps(security_config, indent=2))
67
+ (security_dir / "security.json").write_text(json.dumps(security_config, indent=2), encoding="utf-8")
42
68
  state.status = PillarStatus.DEGRADED
43
69
  _init_audit_log(security_dir)
44
70
  return state
45
71
 
46
72
  _init_audit_log(security_dir)
47
73
 
74
+ audit_log = security_dir / AUDIT_LOG_NAME
75
+ if sksecurity_version and audit_log.exists():
76
+ state.status = PillarStatus.ACTIVE
77
+ else:
78
+ state.status = PillarStatus.DEGRADED
79
+
48
80
  baseline = {
49
81
  "threats_detected": 0,
50
82
  "last_scan": None,
51
83
  "audit_enabled": True,
84
+ "sksecurity_version": sksecurity_version,
52
85
  "initialized_at": datetime.now(timezone.utc).isoformat(),
53
86
  }
54
- (security_dir / "security.json").write_text(json.dumps(baseline, indent=2))
87
+ (security_dir / "security.json").write_text(json.dumps(baseline, indent=2), encoding="utf-8")
55
88
 
56
89
  return state
57
90
 
58
91
 
59
92
  def _init_audit_log(security_dir: Path) -> None:
60
- """Create the audit log file with a header entry."""
61
- audit_log = security_dir / "audit.log"
93
+ """Create the audit log file with a structured INIT entry."""
94
+ audit_log = security_dir / AUDIT_LOG_NAME
62
95
  if not audit_log.exists():
63
- timestamp = datetime.now(timezone.utc).isoformat()
64
- audit_log.write_text(f"[{timestamp}] INIT — SKCapstone security audit log created\n")
96
+ entry = AuditEntry(
97
+ event_type="INIT",
98
+ detail="SKCapstone security audit log created",
99
+ )
100
+ audit_log.write_text(entry.model_dump_json() + "\n", encoding="utf-8")
101
+
65
102
 
103
+ def audit_event(
104
+ home: Path,
105
+ event_type: str,
106
+ detail: str,
107
+ agent: Optional[str] = None,
108
+ metadata: Optional[dict] = None,
109
+ ) -> AuditEntry:
110
+ """Append a structured event to the audit log.
66
111
 
67
- def audit_event(home: Path, event_type: str, detail: str) -> None:
68
- """Append an event to the audit log.
112
+ Each event is written as a single JSON line (JSONL format),
113
+ keeping the log append-only and machine-parseable.
69
114
 
70
115
  Args:
71
116
  home: Agent home directory.
72
- event_type: Event category (INIT, AUTH, MEMORY, TRUST, etc.).
117
+ event_type: Event category (INIT, AUTH, MEMORY, TRUST, SYNC,
118
+ TOKEN_ISSUE, TOKEN_REVOKE, SECURITY, etc.).
73
119
  detail: Human-readable event description.
120
+ agent: Optional agent name that triggered the event.
121
+ metadata: Optional dict of extra structured data.
122
+
123
+ Returns:
124
+ AuditEntry: The entry that was written.
74
125
  """
75
126
  security_dir = home / "security"
76
127
  security_dir.mkdir(parents=True, exist_ok=True)
77
- audit_log = security_dir / "audit.log"
128
+ audit_log = security_dir / AUDIT_LOG_NAME
129
+
130
+ entry = AuditEntry(
131
+ event_type=event_type,
132
+ detail=detail,
133
+ agent=agent,
134
+ metadata=metadata,
135
+ )
136
+
137
+ with audit_log.open("a", encoding="utf-8") as f:
138
+ f.write(entry.model_dump_json() + "\n")
139
+
140
+ return entry
141
+
142
+
143
+ def read_audit_log(home: Path, limit: int = 0) -> list[AuditEntry]:
144
+ """Read and parse the audit log.
78
145
 
79
- timestamp = datetime.now(timezone.utc).isoformat()
80
- entry = f"[{timestamp}] {event_type} {detail}\n"
146
+ Handles both legacy plain-text entries and new JSONL entries
147
+ gracefully old lines are wrapped in an AuditEntry with
148
+ event_type="LEGACY".
81
149
 
82
- with audit_log.open("a") as f:
83
- f.write(entry)
150
+ Args:
151
+ home: Agent home directory.
152
+ limit: Maximum entries to return (0 = all, newest first).
153
+
154
+ Returns:
155
+ list[AuditEntry]: Parsed audit entries.
156
+ """
157
+ audit_log = home / "security" / AUDIT_LOG_NAME
158
+ if not audit_log.exists():
159
+ return []
160
+
161
+ entries: list[AuditEntry] = []
162
+ for line in audit_log.read_text(encoding="utf-8").splitlines():
163
+ line = line.strip()
164
+ if not line:
165
+ continue
166
+ try:
167
+ data = json.loads(line)
168
+ entries.append(AuditEntry.model_validate(data))
169
+ except (json.JSONDecodeError, Exception):
170
+ # Reason: gracefully handle legacy plain-text log lines
171
+ entries.append(AuditEntry(event_type="LEGACY", detail=line))
172
+
173
+ if limit > 0:
174
+ entries = entries[-limit:]
175
+
176
+ return entries
@@ -21,6 +21,7 @@ from datetime import datetime, timezone
21
21
  from pathlib import Path
22
22
  from typing import Optional
23
23
 
24
+ from .. import SHARED_ROOT
24
25
  from ..models import PillarStatus, SyncConfig, SyncState, SyncTransport
25
26
 
26
27
  logger = logging.getLogger("skcapstone.sync")
@@ -56,7 +57,7 @@ def initialize_sync(home: Path, config: Optional[SyncConfig] = None) -> SyncStat
56
57
  "auto_push": config.auto_push,
57
58
  "auto_pull": config.auto_pull,
58
59
  }
59
- (sync_dir / "sync-manifest.json").write_text(json.dumps(manifest, indent=2))
60
+ (sync_dir / "sync-manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
60
61
 
61
62
  state = SyncState(
62
63
  transport=config.transport,
@@ -88,9 +89,7 @@ def collect_seed(home: Path, agent_name: str) -> Path:
88
89
  Returns:
89
90
  Path to the generated seed file in the outbox.
90
91
  """
91
- sync_dir = (home / "sync").expanduser() if not (home / "sync").is_absolute() else home / "sync"
92
- if not sync_dir.exists():
93
- sync_dir = Path("~/.skcapstone/sync").expanduser()
92
+ sync_dir = _resolve_sync_dir(home)
94
93
  outbox = sync_dir / "outbox"
95
94
  outbox.mkdir(parents=True, exist_ok=True)
96
95
 
@@ -107,11 +106,11 @@ def collect_seed(home: Path, agent_name: str) -> Path:
107
106
 
108
107
  identity_file = home / "identity" / "identity.json"
109
108
  if identity_file.exists():
110
- seed["identity"] = json.loads(identity_file.read_text())
109
+ seed["identity"] = json.loads(identity_file.read_text(encoding="utf-8"))
111
110
 
112
111
  trust_file = home / "trust" / "trust.json"
113
112
  if trust_file.exists():
114
- seed["trust"] = json.loads(trust_file.read_text())
113
+ seed["trust"] = json.loads(trust_file.read_text(encoding="utf-8"))
115
114
  try:
116
115
  from .trust import export_febs_for_seed
117
116
 
@@ -134,11 +133,11 @@ def collect_seed(home: Path, agent_name: str) -> Path:
134
133
 
135
134
  manifest_file = home / "manifest.json"
136
135
  if manifest_file.exists():
137
- seed["manifest"] = json.loads(manifest_file.read_text())
136
+ seed["manifest"] = json.loads(manifest_file.read_text(encoding="utf-8"))
138
137
 
139
138
  seed_name = f"{agent_name}-{hostname}-{timestamp.strftime('%Y%m%dT%H%M%SZ')}{SEED_EXTENSION}"
140
139
  seed_path = outbox / seed_name
141
- seed_path.write_text(json.dumps(seed, indent=2, default=str))
140
+ seed_path.write_text(json.dumps(seed, indent=2, default=str), encoding="utf-8")
142
141
 
143
142
  logger.info("Seed collected: %s", seed_path.name)
144
143
  return seed_path
@@ -148,16 +147,20 @@ def gpg_encrypt(
148
147
  seed_path: Path,
149
148
  recipient: Optional[str] = None,
150
149
  home: Optional[Path] = None,
150
+ extra_recipients: Optional[list[str]] = None,
151
151
  ) -> Optional[Path]:
152
152
  """Encrypt a seed file with GPG.
153
153
 
154
- Uses the agent's CapAuth key (or specified recipient) for encryption.
155
- Armor output for git-friendliness.
154
+ Encrypts to the agent's own key AND all known peer fingerprints so
155
+ that every peer in the mesh can independently decrypt the seed they
156
+ receive via Syncthing. Without peer fingerprints, only the sender
157
+ can decrypt — which defeats the purpose of sync.
156
158
 
157
159
  Args:
158
160
  seed_path: Path to the plaintext seed file.
159
- recipient: GPG recipient (fingerprint/email). Auto-detects if None.
161
+ recipient: Primary GPG recipient (fingerprint/email). Auto-detects if None.
160
162
  home: Agent home directory for key detection.
163
+ extra_recipients: Additional peer fingerprints to encrypt to.
161
164
 
162
165
  Returns:
163
166
  Path to the encrypted file, or None if encryption failed.
@@ -166,21 +169,32 @@ def gpg_encrypt(
166
169
  logger.error("gpg not found in PATH — cannot encrypt")
167
170
  return None
168
171
 
172
+ agent_home = home or Path(SHARED_ROOT).expanduser()
173
+
169
174
  if recipient is None:
170
- agent_home = home or Path("~/.skcapstone").expanduser()
171
175
  recipient = _detect_gpg_key(agent_home)
172
176
 
173
177
  if recipient is None:
174
178
  logger.error("No GPG key found for encryption")
175
179
  return None
176
180
 
181
+ # Build recipient list: own key + all known peers
182
+ all_recipients = [recipient]
183
+ if extra_recipients:
184
+ all_recipients.extend(r for r in extra_recipients if r and r != recipient)
185
+
177
186
  encrypted_path = seed_path.parent / (seed_path.name + ".gpg")
178
187
 
188
+ recipient_args: list[str] = []
189
+ for r in all_recipients:
190
+ recipient_args += ["--recipient", r]
191
+
179
192
  try:
180
193
  subprocess.run(
181
194
  [
182
195
  "gpg", "--batch", "--yes", "--trust-model", "always",
183
- "--armor", "--encrypt", "--recipient", recipient,
196
+ "--armor", "--encrypt",
197
+ *recipient_args,
184
198
  "--output", str(encrypted_path), str(seed_path),
185
199
  ],
186
200
  capture_output=True,
@@ -188,7 +202,10 @@ def gpg_encrypt(
188
202
  check=True,
189
203
  timeout=30,
190
204
  )
191
- logger.info("Encrypted: %s -> %s", seed_path.name, encrypted_path.name)
205
+ logger.info(
206
+ "Encrypted: %s -> %s (recipients: %d)",
207
+ seed_path.name, encrypted_path.name, len(all_recipients),
208
+ )
192
209
  return encrypted_path
193
210
  except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
194
211
  logger.error("GPG encryption failed: %s", exc)
@@ -235,6 +252,9 @@ def push_seed(home: Path, agent_name: str, encrypt: bool = True) -> Optional[Pat
235
252
  This is the high-level 'push' operation. After this, Syncthing
236
253
  (or git) handles propagation to all peers automatically.
237
254
 
255
+ Reads peer_fingerprints from the sync config so seeds are encrypted
256
+ to all known peers, not just the sender's own key.
257
+
238
258
  Args:
239
259
  home: Agent home directory.
240
260
  agent_name: Agent display name.
@@ -246,7 +266,8 @@ def push_seed(home: Path, agent_name: str, encrypt: bool = True) -> Optional[Pat
246
266
  seed_path = collect_seed(home, agent_name)
247
267
 
248
268
  if encrypt:
249
- encrypted = gpg_encrypt(seed_path, home=home)
269
+ peer_fingerprints = _load_peer_fingerprints(home)
270
+ encrypted = gpg_encrypt(seed_path, home=home, extra_recipients=peer_fingerprints)
250
271
  if encrypted:
251
272
  seed_path.unlink()
252
273
  return encrypted
@@ -255,6 +276,29 @@ def push_seed(home: Path, agent_name: str, encrypt: bool = True) -> Optional[Pat
255
276
  return seed_path
256
277
 
257
278
 
279
+ def _load_peer_fingerprints(home: Path) -> list[str]:
280
+ """Load known peer GPG fingerprints from sync config.
281
+
282
+ Args:
283
+ home: Agent home directory.
284
+
285
+ Returns:
286
+ List of peer fingerprint strings (may be empty).
287
+ """
288
+ config_file = home / "config" / "config.yaml"
289
+ if not config_file.exists():
290
+ return []
291
+ try:
292
+ import yaml as _yaml
293
+ data = _yaml.safe_load(config_file.read_text(encoding="utf-8")) or {}
294
+ sync_data = data.get("sync", {})
295
+ peers = sync_data.get("peer_fingerprints", [])
296
+ return [str(p) for p in peers if p]
297
+ except Exception as exc:
298
+ logger.debug("Could not load peer fingerprints: %s", exc)
299
+ return []
300
+
301
+
258
302
  def pull_seeds(home: Path, decrypt: bool = True) -> list[dict]:
259
303
  """Pull and process seed files from the inbox.
260
304
 
@@ -293,7 +337,7 @@ def pull_seeds(home: Path, decrypt: bool = True) -> list[dict]:
293
337
 
294
338
  if seed_path.suffix == ".json" or seed_path.name.endswith(SEED_EXTENSION):
295
339
  try:
296
- data = json.loads(seed_path.read_text())
340
+ data = json.loads(seed_path.read_text(encoding="utf-8"))
297
341
  seeds.append(data)
298
342
 
299
343
  if "memory_entries" in data:
@@ -304,7 +348,7 @@ def pull_seeds(home: Path, decrypt: bool = True) -> list[dict]:
304
348
  if imported:
305
349
  logger.info("Imported %d memories from seed %s", imported, seed_path.name)
306
350
  except Exception as exc:
307
- logger.debug("Could not import seed memories: %s", exc)
351
+ logger.warning("Could not import memories from seed %s: %s", seed_path.name, exc)
308
352
 
309
353
  if "febs" in data:
310
354
  try:
@@ -314,7 +358,7 @@ def pull_seeds(home: Path, decrypt: bool = True) -> list[dict]:
314
358
  if feb_imported:
315
359
  logger.info("Imported %d FEB(s) from seed %s", feb_imported, seed_path.name)
316
360
  except Exception as exc:
317
- logger.debug("Could not import seed FEBs: %s", exc)
361
+ logger.warning("Could not import FEBs from seed %s: %s", seed_path.name, exc)
318
362
 
319
363
  archive.mkdir(exist_ok=True)
320
364
  seed_path.rename(archive / seed_path.name)
@@ -343,7 +387,7 @@ def discover_sync(home: Path) -> SyncState:
343
387
  return SyncState(sync_path=sync_dir, status=PillarStatus.DEGRADED)
344
388
 
345
389
  try:
346
- data = json.loads(manifest_file.read_text())
390
+ data = json.loads(manifest_file.read_text(encoding="utf-8"))
347
391
  except (json.JSONDecodeError, OSError):
348
392
  return SyncState(sync_path=sync_dir, status=PillarStatus.DEGRADED)
349
393
 
@@ -370,11 +414,19 @@ def discover_sync(home: Path) -> SyncState:
370
414
 
371
415
 
372
416
  def _resolve_sync_dir(home: Path) -> Path:
373
- """Resolve the sync directory path."""
417
+ """Resolve the sync directory path.
418
+
419
+ Prefers the explicitly-passed home/sync (used by tests and
420
+ single-agent mode), then falls back to SHARED_ROOT/sync for
421
+ multi-agent mode.
422
+ """
374
423
  sync_dir = home / "sync"
375
424
  if sync_dir.exists():
376
425
  return sync_dir
377
- return Path("~/.skcapstone/sync").expanduser()
426
+ shared_sync = Path(SHARED_ROOT).expanduser() / "sync"
427
+ if shared_sync.exists():
428
+ return shared_sync
429
+ return sync_dir
378
430
 
379
431
 
380
432
  def _detect_gpg_key(home: Path) -> Optional[str]:
@@ -382,7 +434,7 @@ def _detect_gpg_key(home: Path) -> Optional[str]:
382
434
  identity_file = home / "identity" / "identity.json"
383
435
  if identity_file.exists():
384
436
  try:
385
- data = json.loads(identity_file.read_text())
437
+ data = json.loads(identity_file.read_text(encoding="utf-8"))
386
438
  fp = data.get("fingerprint")
387
439
  if fp and data.get("capauth_managed"):
388
440
  return fp
@@ -460,14 +512,14 @@ def _load_sync_timestamps(sync_dir: Path, state: SyncState) -> None:
460
512
  state_file = sync_dir / "sync-state.json"
461
513
  if state_file.exists():
462
514
  try:
463
- data = json.loads(state_file.read_text())
515
+ data = json.loads(state_file.read_text(encoding="utf-8"))
464
516
  if data.get("last_push"):
465
517
  state.last_push = datetime.fromisoformat(data["last_push"])
466
518
  if data.get("last_pull"):
467
519
  state.last_pull = datetime.fromisoformat(data["last_pull"])
468
520
  state.peers_known = data.get("peers_known", 0)
469
- except (json.JSONDecodeError, OSError, ValueError):
470
- pass
521
+ except (json.JSONDecodeError, OSError, ValueError) as exc:
522
+ logger.debug("Could not load sync timestamps: %s", exc)
471
523
 
472
524
 
473
525
  def save_sync_state(sync_dir: Path, state: SyncState) -> None:
@@ -483,4 +535,4 @@ def save_sync_state(sync_dir: Path, state: SyncState) -> None:
483
535
  "peers_known": state.peers_known,
484
536
  "seed_count": state.seed_count,
485
537
  }
486
- (sync_dir / "sync-state.json").write_text(json.dumps(data, indent=2))
538
+ (sync_dir / "sync-state.json").write_text(json.dumps(data, indent=2), encoding="utf-8")