@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
@@ -0,0 +1,1193 @@
1
+ """
2
+ Local Provider — runs agent teams as local processes backed by crush/SKSkills sessions.
3
+
4
+ Each agent is spawned as a real ``crush`` subprocess that receives its full identity
5
+ via a generated session config: soul blueprint, skills list, model tier, and
6
+ coordination context. If the crush binary is not present the provider falls back to
7
+ a lightweight Python stub so development can proceed without the full runtime
8
+ installed.
9
+
10
+ Session lifecycle
11
+ -----------------
12
+ 1. ``provision()`` — create work dir, write ``config.json`` + ``session.json``
13
+ 2. ``configure()`` — resolve soul blueprint & skill paths; write ``crush.json``
14
+ 3. ``start()`` — spawn ``crush run --session session.json`` as a daemon process
15
+ 4. ``health_check()``— read session state file; fall back to PID liveness check
16
+ 5. ``stop()`` — SIGTERM → wait → SIGKILL; write tombstone
17
+ 6. ``destroy()`` — stop + remove work dir
18
+
19
+ Per hosted-agents best practice: session-isolated state, filesystem memory,
20
+ health checks via session state file then PID monitoring.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import logging
27
+ import os
28
+ import signal
29
+ import subprocess
30
+ import time
31
+ from pathlib import Path
32
+ from typing import Any, Dict, List, Optional
33
+
34
+ from ..blueprints.schema import AgentSpec, ProviderType
35
+ from ..team_engine import AgentStatus, ProviderBackend
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Constants
41
+ # ---------------------------------------------------------------------------
42
+
43
+ _CRUSH_BINARY_NAMES: List[str] = ["crush", "claude"]
44
+ _SESSION_STATE_FILE = "session_state.json"
45
+ _PID_FILE = "agent.pid"
46
+ _SESSION_CONFIG_FILE = "session.json"
47
+ _CRUSH_CONFIG_FILE = "crush.json"
48
+ _STOP_TIMEOUT_SECONDS = 15
49
+ _STOP_KILL_TIMEOUT_SECONDS = 5
50
+
51
+ # Session state values written by the crush daemon
52
+ _STATE_RUNNING = "running"
53
+ _STATE_IDLE = "idle"
54
+ _STATE_ERROR = "error"
55
+ _STATE_STOPPED = "stopped"
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Helper utilities
60
+ # ---------------------------------------------------------------------------
61
+
62
+
63
+ def _find_crush_binary() -> Optional[str]:
64
+ """Locate the crush binary on PATH, falling back to claude.
65
+
66
+ Returns:
67
+ Absolute path to the binary, or None if not found.
68
+ """
69
+ import shutil
70
+
71
+ for name in _CRUSH_BINARY_NAMES:
72
+ path = shutil.which(name)
73
+ if path:
74
+ return path
75
+ return None
76
+
77
+
78
+ def _is_claude_binary(binary: str) -> bool:
79
+ """Check whether the resolved binary is the claude CLI rather than crush.
80
+
81
+ Args:
82
+ binary: Absolute path to the binary.
83
+
84
+ Returns:
85
+ True if the binary basename is ``claude``.
86
+ """
87
+ return Path(binary).name == "claude"
88
+
89
+
90
+ def _resolve_soul_blueprint_path(
91
+ soul_blueprint: Optional[str],
92
+ work_dir: Path,
93
+ repo_root: Optional[Path] = None,
94
+ ) -> Optional[str]:
95
+ """Resolve a soul blueprint reference to an absolute path.
96
+
97
+ Checks (in order):
98
+ 1. Absolute path as-is.
99
+ 2. Relative to repo soul-blueprints/blueprints/ directory.
100
+ 3. Relative to the agent's work_dir.
101
+
102
+ Args:
103
+ soul_blueprint: Blueprint slug or path from AgentSpec.
104
+ work_dir: Agent working directory.
105
+ repo_root: Optional repo root for resolving workspace-relative paths.
106
+
107
+ Returns:
108
+ Resolved absolute path string, or the original value if unresolvable.
109
+ """
110
+ if not soul_blueprint:
111
+ return None
112
+
113
+ candidate = Path(soul_blueprint)
114
+ if candidate.is_absolute() and candidate.exists():
115
+ return str(candidate)
116
+
117
+ if repo_root:
118
+ # Try soul-blueprints/blueprints/<slug>/<LUMINA.md> style
119
+ blueprint_dir = repo_root / "soul-blueprints" / "blueprints" / soul_blueprint
120
+ if blueprint_dir.exists():
121
+ return str(blueprint_dir)
122
+ # Try soul-blueprints/<value> directly
123
+ direct = repo_root / "soul-blueprints" / soul_blueprint
124
+ if direct.exists():
125
+ return str(direct)
126
+ # Try relative to repo root
127
+ relative = repo_root / soul_blueprint
128
+ if relative.exists():
129
+ return str(relative)
130
+
131
+ # Relative to work dir
132
+ relative_to_wd = work_dir / soul_blueprint
133
+ if relative_to_wd.exists():
134
+ return str(relative_to_wd)
135
+
136
+ # Return original value; crush may resolve it itself
137
+ return soul_blueprint
138
+
139
+
140
+ def _resolve_skill_paths(
141
+ skills: List[str],
142
+ repo_root: Optional[Path] = None,
143
+ agent: str = "global",
144
+ ) -> List[str]:
145
+ """Resolve skill names to absolute paths where possible.
146
+
147
+ Uses the session_skills bridge to check the SKSkills registry.
148
+
149
+ Args:
150
+ skills: List of skill names or paths from AgentSpec.
151
+ repo_root: Optional repo root for resolving workspace-relative paths.
152
+ agent: Agent namespace for SKSkills per-agent lookup.
153
+
154
+ Returns:
155
+ List of resolved paths (unresolvable names kept as-is).
156
+ """
157
+ try:
158
+ from ..session_skills import resolve_skill_paths_with_skskills
159
+ return resolve_skill_paths_with_skskills(skills, agent=agent, repo_root=repo_root)
160
+ except ImportError:
161
+ pass
162
+
163
+ # Fallback: basic resolution without skskills installed
164
+ resolved: List[str] = []
165
+ for skill in skills:
166
+ path = Path(skill)
167
+ if path.is_absolute() and path.exists():
168
+ resolved.append(str(path))
169
+ else:
170
+ resolved.append(skill)
171
+
172
+ return resolved
173
+
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # Session config builders
177
+ # ---------------------------------------------------------------------------
178
+
179
+
180
+ def _resolve_model_via_router(
181
+ spec: AgentSpec,
182
+ description: str = "",
183
+ ) -> str:
184
+ """Resolve the concrete model name using the Model Router.
185
+
186
+ If the spec has an explicit model_name, that takes priority.
187
+ Otherwise, the router selects based on the model tier and task context.
188
+
189
+ Args:
190
+ spec: Agent specification containing model tier and optional model name.
191
+ description: Task/role description for tag-based routing.
192
+
193
+ Returns:
194
+ Concrete model name string.
195
+ """
196
+ if spec.model_name:
197
+ return spec.model_name
198
+
199
+ try:
200
+ from ..model_router import ModelRouter, TaskSignal
201
+
202
+ router = ModelRouter()
203
+ signal = TaskSignal(
204
+ description=description or spec.description or f"{spec.role.value} agent",
205
+ tags=[spec.role.value, spec.model.value],
206
+ )
207
+ decision = router.route(signal)
208
+ logger.debug(
209
+ "Model router: tier=%s model=%s reason=%s",
210
+ decision.tier.value, decision.model_name, decision.reasoning,
211
+ )
212
+ return decision.model_name
213
+ except ImportError:
214
+ return spec.model.value
215
+
216
+
217
+ def _build_session_config(
218
+ agent_name: str,
219
+ team_name: str,
220
+ spec: AgentSpec,
221
+ work_dir: Path,
222
+ repo_root: Optional[Path] = None,
223
+ ) -> Dict[str, Any]:
224
+ """Build the session.json payload for a crush agent session.
225
+
226
+ Uses the Model Router to resolve the model tier to a concrete model name
227
+ when no explicit model_name is set in the agent spec.
228
+
229
+ Args:
230
+ agent_name: Unique agent instance name.
231
+ team_name: Parent team name.
232
+ spec: Agent specification containing soul, skills, model, role.
233
+ work_dir: Agent working directory (used for memory/scratch paths).
234
+ repo_root: Optional repo root for path resolution.
235
+
236
+ Returns:
237
+ Dictionary ready to be serialised as session.json.
238
+ """
239
+ soul_path = _resolve_soul_blueprint_path(
240
+ spec.soul_blueprint, work_dir, repo_root
241
+ )
242
+ skill_paths = _resolve_skill_paths(spec.skills, repo_root, agent=agent_name)
243
+ model = _resolve_model_via_router(spec, f"{agent_name} in team {team_name}")
244
+
245
+ config: Dict[str, Any] = {
246
+ "agent_name": agent_name,
247
+ "team_name": team_name,
248
+ "role": spec.role.value,
249
+ "model": model,
250
+ "model_tier": spec.model.value,
251
+ "soul_blueprint": soul_path,
252
+ "skills": skill_paths,
253
+ "memory_dir": str(work_dir / "memory"),
254
+ "scratch_dir": str(work_dir / "scratch"),
255
+ "state_file": str(work_dir / _SESSION_STATE_FILE),
256
+ "env": spec.env,
257
+ }
258
+ return config
259
+
260
+
261
+ def _build_crush_config(
262
+ agent_name: str,
263
+ session_config: Dict[str, Any],
264
+ work_dir: Path,
265
+ ) -> Dict[str, Any]:
266
+ """Build the crush.json that the crush daemon reads on startup.
267
+
268
+ Mirrors the structure of the project-level crush.json but scoped to a
269
+ single agent session.
270
+
271
+ Args:
272
+ agent_name: Agent instance name.
273
+ session_config: Output of _build_session_config().
274
+ work_dir: Agent working directory.
275
+
276
+ Returns:
277
+ Dictionary ready to be serialised as crush.json.
278
+ """
279
+ crush_cfg: Dict[str, Any] = {
280
+ "$schema": "https://charm.land/crush.json",
281
+ "options": {
282
+ "initialize_as": session_config.get("soul_blueprint", "AGENTS.md"),
283
+ "context_paths": [
284
+ session_config.get("soul_blueprint"),
285
+ ],
286
+ "skills_paths": [
287
+ str(work_dir / "skills"),
288
+ "~/.config/crush/skills",
289
+ "~/.skskills/installed",
290
+ ],
291
+ "debug": False,
292
+ "disabled_tools": [],
293
+ },
294
+ "permissions": {
295
+ "allowed_tools": [
296
+ "view",
297
+ "ls",
298
+ "grep",
299
+ "edit",
300
+ "mcp_skcapstone_agent_status",
301
+ "mcp_skcapstone_memory_store",
302
+ "mcp_skcapstone_memory_recall",
303
+ "mcp_skcapstone_coord_status",
304
+ "mcp_skcapstone_coord_claim",
305
+ ]
306
+ },
307
+ "session": {
308
+ "agent_name": agent_name,
309
+ "model": session_config.get("model", "fast"),
310
+ "role": session_config.get("role", "worker"),
311
+ "skills": session_config.get("skills", []),
312
+ "memory_dir": session_config.get("memory_dir"),
313
+ "state_file": session_config.get("state_file"),
314
+ },
315
+ }
316
+ # Remove None values from context_paths
317
+ crush_cfg["options"]["context_paths"] = [
318
+ p for p in crush_cfg["options"]["context_paths"] if p is not None
319
+ ]
320
+ return crush_cfg
321
+
322
+
323
+ # ---------------------------------------------------------------------------
324
+ # Session state helpers
325
+ # ---------------------------------------------------------------------------
326
+
327
+
328
+ def _read_session_state(work_dir: Path) -> Optional[Dict[str, Any]]:
329
+ """Read session state written by the crush daemon.
330
+
331
+ Args:
332
+ work_dir: Agent working directory.
333
+
334
+ Returns:
335
+ Parsed state dict, or None if missing/corrupt.
336
+ """
337
+ state_file = work_dir / _SESSION_STATE_FILE
338
+ if not state_file.exists():
339
+ return None
340
+ try:
341
+ return json.loads(state_file.read_text(encoding="utf-8"))
342
+ except (json.JSONDecodeError, OSError):
343
+ return None
344
+
345
+
346
+ def _write_session_state(work_dir: Path, state: Dict[str, Any]) -> None:
347
+ """Write session state to disk.
348
+
349
+ Args:
350
+ work_dir: Agent working directory.
351
+ state: State dictionary to persist.
352
+ """
353
+ state_file = work_dir / _SESSION_STATE_FILE
354
+ state_file.write_text(json.dumps(state, indent=2), encoding="utf-8")
355
+
356
+
357
+ def _read_pid(work_dir: Path) -> Optional[int]:
358
+ """Read the PID from the agent PID file.
359
+
360
+ Args:
361
+ work_dir: Agent working directory.
362
+
363
+ Returns:
364
+ PID integer or None.
365
+ """
366
+ pid_file = work_dir / _PID_FILE
367
+ if not pid_file.exists():
368
+ return None
369
+ try:
370
+ return int(pid_file.read_text(encoding="utf-8").strip())
371
+ except (ValueError, OSError):
372
+ return None
373
+
374
+
375
+ def _pid_is_alive(pid: int) -> bool:
376
+ """Check whether a process is alive via signal 0.
377
+
378
+ Args:
379
+ pid: Process ID to check.
380
+
381
+ Returns:
382
+ True if the process exists and is accessible.
383
+ """
384
+ try:
385
+ os.kill(pid, 0)
386
+ return True
387
+ except ProcessLookupError:
388
+ return False
389
+ except OSError:
390
+ # Permission denied means process exists but we can't signal it
391
+ return True
392
+
393
+
394
+ # ---------------------------------------------------------------------------
395
+ # Provider
396
+ # ---------------------------------------------------------------------------
397
+
398
+
399
+ class LocalProvider(ProviderBackend):
400
+ """Deploy agents as local processes backed by crush/SKSkills sessions.
401
+
402
+ Each agent is given its own working directory containing:
403
+ - ``config.json`` — human-readable agent configuration
404
+ - ``session.json`` — crush session payload (soul, skills, model)
405
+ - ``crush.json`` — crush daemon config (written during configure())
406
+ - ``agent.pid`` — PID of the crush daemon process
407
+ - ``session_state.json`` — live state written by the crush daemon
408
+ - ``memory/`` — persistent memory directory
409
+ - ``scratch/`` — ephemeral scratch space
410
+
411
+ Args:
412
+ home: Agent home directory (default: ``~/.skcapstone``).
413
+ work_dir: Root directory for agent working dirs.
414
+ repo_root: Workspace root for resolving soul/skill paths.
415
+ crush_binary: Explicit path to crush binary (auto-detected from PATH
416
+ if not provided).
417
+ """
418
+
419
+ provider_type = ProviderType.LOCAL
420
+
421
+ def __init__(
422
+ self,
423
+ home: Optional[Path] = None,
424
+ work_dir: Optional[Path] = None,
425
+ repo_root: Optional[Path] = None,
426
+ crush_binary: Optional[str] = None,
427
+ ) -> None:
428
+ self._home = (home or Path("~/.skcapstone")).expanduser()
429
+ self._work_dir = work_dir or (self._home / "agents" / "local")
430
+ self._work_dir.mkdir(parents=True, exist_ok=True)
431
+ self._repo_root = repo_root
432
+ self._crush_binary = crush_binary # None = auto-detect at start time
433
+
434
+ # ------------------------------------------------------------------
435
+ # provision
436
+ # ------------------------------------------------------------------
437
+
438
+ def provision(
439
+ self,
440
+ agent_name: str,
441
+ spec: AgentSpec,
442
+ team_name: str,
443
+ ) -> Dict[str, Any]:
444
+ """Create the agent working directory and write session config.
445
+
446
+ Args:
447
+ agent_name: Unique agent instance name.
448
+ spec: Agent specification.
449
+ team_name: Parent team name.
450
+
451
+ Returns:
452
+ Dict with ``work_dir``, ``host``, and session configuration fields.
453
+ """
454
+ agent_dir = self._work_dir / agent_name
455
+ agent_dir.mkdir(parents=True, exist_ok=True)
456
+ (agent_dir / "memory").mkdir(exist_ok=True)
457
+ (agent_dir / "scratch").mkdir(exist_ok=True)
458
+ (agent_dir / "skills").mkdir(exist_ok=True)
459
+
460
+ session_config = _build_session_config(
461
+ agent_name=agent_name,
462
+ team_name=team_name,
463
+ spec=spec,
464
+ work_dir=agent_dir,
465
+ repo_root=self._repo_root,
466
+ )
467
+
468
+ # Human-readable summary
469
+ (agent_dir / "config.json").write_text(
470
+ json.dumps(session_config, indent=2), encoding="utf-8"
471
+ )
472
+
473
+ # Crush session payload
474
+ (agent_dir / _SESSION_CONFIG_FILE).write_text(
475
+ json.dumps(session_config, indent=2), encoding="utf-8"
476
+ )
477
+
478
+ # Wire SKSkills into the session
479
+ skill_result = self._prepare_skskills(
480
+ agent_name, session_config.get("skills", []), agent_dir
481
+ )
482
+ if skill_result and skill_result.get("skills_loaded", 0) > 0:
483
+ try:
484
+ from ..session_skills import enrich_session_config
485
+ enrich_session_config(session_config, skill_result)
486
+ except ImportError:
487
+ pass
488
+
489
+ logger.info(
490
+ "Provisioned agent %s (role=%s model=%s soul=%s skills=%s skskills=%d)",
491
+ agent_name,
492
+ spec.role.value,
493
+ session_config["model"],
494
+ session_config.get("soul_blueprint"),
495
+ session_config.get("skills"),
496
+ skill_result.get("skills_loaded", 0) if skill_result else 0,
497
+ )
498
+
499
+ return {
500
+ "host": "localhost",
501
+ "work_dir": str(agent_dir),
502
+ "session_config": session_config,
503
+ "skill_result": skill_result,
504
+ }
505
+
506
+ def _prepare_skskills(
507
+ self,
508
+ agent_name: str,
509
+ skills: List[str],
510
+ work_dir: Path,
511
+ ) -> Optional[Dict[str, Any]]:
512
+ """Prepare SKSkills for an agent session.
513
+
514
+ Args:
515
+ agent_name: Agent instance name.
516
+ skills: Resolved skill paths.
517
+ work_dir: Agent working directory.
518
+
519
+ Returns:
520
+ Skill preparation result dict, or None if skskills unavailable.
521
+ """
522
+ try:
523
+ from ..session_skills import prepare_session_skills
524
+ return prepare_session_skills(agent_name, skills, work_dir)
525
+ except ImportError:
526
+ return None
527
+
528
+ # ------------------------------------------------------------------
529
+ # configure
530
+ # ------------------------------------------------------------------
531
+
532
+ def configure(
533
+ self,
534
+ agent_name: str,
535
+ spec: AgentSpec,
536
+ provision_result: Dict[str, Any],
537
+ ) -> bool:
538
+ """Write the crush.json daemon config into the agent's work directory.
539
+
540
+ Args:
541
+ agent_name: Agent instance name.
542
+ spec: Agent specification.
543
+ provision_result: Output from provision().
544
+
545
+ Returns:
546
+ True if configuration succeeded, False on error.
547
+ """
548
+ work_dir_str = provision_result.get("work_dir", "")
549
+ if not work_dir_str:
550
+ logger.error("configure: missing work_dir for %s", agent_name)
551
+ return False
552
+
553
+ work_dir = Path(work_dir_str)
554
+ session_config = provision_result.get("session_config", {})
555
+
556
+ crush_cfg = _build_crush_config(agent_name, session_config, work_dir)
557
+
558
+ # Enrich crush config with SKSkills MCP server entry
559
+ skill_result = provision_result.get("skill_result")
560
+ if skill_result:
561
+ try:
562
+ from ..session_skills import enrich_crush_config
563
+ enrich_crush_config(crush_cfg, skill_result)
564
+ except ImportError:
565
+ pass
566
+
567
+ try:
568
+ (work_dir / _CRUSH_CONFIG_FILE).write_text(
569
+ json.dumps(crush_cfg, indent=2), encoding="utf-8"
570
+ )
571
+ except OSError as exc:
572
+ logger.error(
573
+ "configure: failed to write crush.json for %s: %s", agent_name, exc
574
+ )
575
+ return False
576
+
577
+ logger.debug("Configured crush session for %s at %s", agent_name, work_dir)
578
+ return True
579
+
580
+ # ------------------------------------------------------------------
581
+ # start
582
+ # ------------------------------------------------------------------
583
+
584
+ def start(
585
+ self,
586
+ agent_name: str,
587
+ provision_result: Dict[str, Any],
588
+ ) -> bool:
589
+ """Spawn a crush session for the agent.
590
+
591
+ Attempts to launch the crush binary found on PATH. If crush is not
592
+ installed, falls back to a lightweight Python stub process that writes
593
+ the required session state so the rest of the engine can proceed.
594
+
595
+ The session receives:
596
+ - ``--session session.json`` — full agent identity config
597
+ - ``--config crush.json`` — crush daemon config
598
+ - ``--headless`` — non-interactive daemon mode
599
+
600
+ Environment variables passed to the process:
601
+ - ``AGENT_NAME``, ``TEAM_NAME``, ``SOUL_BLUEPRINT``
602
+ - ``AGENT_MODEL``, ``AGENT_ROLE``, ``AGENT_SKILLS``
603
+ - ``SKCAPSTONE_HOME``, ``AGENT_MEMORY_DIR``
604
+ - Any extra env vars from ``AgentSpec.env``
605
+
606
+ Args:
607
+ agent_name: Agent instance name.
608
+ provision_result: Output from provision().
609
+
610
+ Returns:
611
+ True if the process started successfully.
612
+ """
613
+ work_dir_str = provision_result.get("work_dir", "")
614
+ if not work_dir_str:
615
+ logger.error("start: missing work_dir for %s", agent_name)
616
+ return False
617
+
618
+ work_dir = Path(work_dir_str)
619
+ session_config = provision_result.get("session_config", {})
620
+
621
+ env = self._build_process_env(session_config)
622
+ binary = self._crush_binary or _find_crush_binary()
623
+
624
+ if binary:
625
+ if _is_claude_binary(binary):
626
+ return self._start_claude_session(
627
+ agent_name, work_dir, binary, env, provision_result
628
+ )
629
+ return self._start_crush_session(
630
+ agent_name, work_dir, binary, env, provision_result
631
+ )
632
+ else:
633
+ logger.warning(
634
+ "crush binary not found on PATH; using stub for %s", agent_name
635
+ )
636
+ return self._start_stub_session(
637
+ agent_name, work_dir, env, provision_result
638
+ )
639
+
640
+ def _build_process_env(self, session_config: Dict[str, Any]) -> Dict[str, str]:
641
+ """Build the environment dict for the crush/stub subprocess.
642
+
643
+ Args:
644
+ session_config: Agent session configuration dict.
645
+
646
+ Returns:
647
+ Environment variable dict (inherits current process env).
648
+ """
649
+ env = os.environ.copy()
650
+ env.update({
651
+ "AGENT_NAME": session_config.get("agent_name", ""),
652
+ "TEAM_NAME": session_config.get("team_name", ""),
653
+ "SOUL_BLUEPRINT": session_config.get("soul_blueprint") or "",
654
+ "AGENT_MODEL": session_config.get("model", ""),
655
+ "AGENT_MODEL_TIER": session_config.get("model_tier", ""),
656
+ "AGENT_ROLE": session_config.get("role", ""),
657
+ "AGENT_SKILLS": json.dumps(session_config.get("skills", [])),
658
+ "SKCAPSTONE_HOME": str(self._home),
659
+ "AGENT_MEMORY_DIR": session_config.get("memory_dir", ""),
660
+ "AGENT_SCRATCH_DIR": session_config.get("scratch_dir", ""),
661
+ "AGENT_STATE_FILE": session_config.get("state_file", ""),
662
+ })
663
+ # Merge spec-level env overrides
664
+ extra_env = session_config.get("env", {}) or {}
665
+ env.update({k: str(v) for k, v in extra_env.items()})
666
+ return env
667
+
668
+ def _start_crush_session(
669
+ self,
670
+ agent_name: str,
671
+ work_dir: Path,
672
+ binary: str,
673
+ env: Dict[str, str],
674
+ provision_result: Dict[str, Any],
675
+ ) -> bool:
676
+ """Launch a real crush daemon subprocess.
677
+
678
+ Args:
679
+ agent_name: Agent instance name.
680
+ work_dir: Agent working directory.
681
+ binary: Absolute path to crush binary.
682
+ env: Environment variables for the subprocess.
683
+ provision_result: Mutated in-place with the spawned PID.
684
+
685
+ Returns:
686
+ True if the process started without error.
687
+ """
688
+ cmd = [
689
+ binary,
690
+ "run",
691
+ "--session", str(work_dir / _SESSION_CONFIG_FILE),
692
+ "--config", str(work_dir / _CRUSH_CONFIG_FILE),
693
+ "--headless",
694
+ "--state-file", str(work_dir / _SESSION_STATE_FILE),
695
+ ]
696
+ log_file = work_dir / "agent.log"
697
+
698
+ try:
699
+ with open(log_file, "ab") as log_fh:
700
+ proc = subprocess.Popen(
701
+ cmd,
702
+ cwd=str(work_dir),
703
+ env=env,
704
+ stdout=log_fh,
705
+ stderr=log_fh,
706
+ start_new_session=True, # detach from parent's process group
707
+ )
708
+ except OSError as exc:
709
+ logger.error(
710
+ "start: failed to launch crush for %s: %s", agent_name, exc
711
+ )
712
+ return False
713
+
714
+ pid = proc.pid
715
+ (work_dir / _PID_FILE).write_text(str(pid), encoding="utf-8")
716
+ provision_result["pid"] = pid
717
+
718
+ _write_session_state(work_dir, {
719
+ "status": _STATE_RUNNING,
720
+ "pid": pid,
721
+ "agent_name": agent_name,
722
+ "started_at": _now_iso(),
723
+ "binary": binary,
724
+ })
725
+
726
+ logger.info(
727
+ "Started crush session for %s (pid=%d binary=%s)",
728
+ agent_name, pid, binary,
729
+ )
730
+ return True
731
+
732
+ def _start_claude_session(
733
+ self,
734
+ agent_name: str,
735
+ work_dir: Path,
736
+ binary: str,
737
+ env: Dict[str, str],
738
+ provision_result: Dict[str, Any],
739
+ ) -> bool:
740
+ """Launch a claude CLI session as the agent runtime.
741
+
742
+ Constructs a ``claude -p`` invocation that receives the agent's full
743
+ identity (soul blueprint, model, skills) and streams JSON output.
744
+ An MCP config temp file is written from the crush.json mcpServers
745
+ section when available.
746
+
747
+ Args:
748
+ agent_name: Agent instance name.
749
+ work_dir: Agent working directory.
750
+ binary: Absolute path to the ``claude`` CLI binary.
751
+ env: Environment variables for the subprocess.
752
+ provision_result: Mutated in-place with the spawned PID.
753
+
754
+ Returns:
755
+ True if the process started without error.
756
+ """
757
+ import hashlib
758
+ import uuid
759
+
760
+ session_config = provision_result.get("session_config", {})
761
+ model = session_config.get("model", "fast")
762
+
763
+ # Build system prompt from soul blueprint + agent context
764
+ soul_path = session_config.get("soul_blueprint")
765
+ system_prompt_parts: List[str] = []
766
+ if soul_path:
767
+ soul_file = Path(soul_path)
768
+ if soul_file.is_file():
769
+ try:
770
+ system_prompt_parts.append(
771
+ soul_file.read_text(encoding="utf-8")
772
+ )
773
+ except OSError:
774
+ system_prompt_parts.append(f"Soul blueprint: {soul_path}")
775
+ elif soul_file.is_dir():
776
+ # Look for a markdown file inside the soul blueprint directory
777
+ for ext in ("*.md", "*.txt", "*.yaml"):
778
+ for f in sorted(soul_file.glob(ext)):
779
+ try:
780
+ system_prompt_parts.append(
781
+ f.read_text(encoding="utf-8")
782
+ )
783
+ except OSError:
784
+ pass
785
+ if not system_prompt_parts:
786
+ system_prompt_parts.append(f"Soul blueprint: {soul_path}")
787
+ else:
788
+ system_prompt_parts.append(f"Soul blueprint: {soul_path}")
789
+
790
+ system_prompt_parts.append(
791
+ f"\nAgent: {agent_name}\n"
792
+ f"Role: {session_config.get('role', 'worker')}\n"
793
+ f"Team: {session_config.get('team_name', '')}\n"
794
+ f"Skills: {json.dumps(session_config.get('skills', []))}\n"
795
+ )
796
+ system_prompt = "\n".join(system_prompt_parts)
797
+
798
+ # Deterministic session ID from agent name
799
+ session_id = str(
800
+ uuid.UUID(
801
+ hashlib.md5(agent_name.encode()).hexdigest() # noqa: S324
802
+ )
803
+ )
804
+
805
+ cmd: List[str] = [
806
+ binary,
807
+ "-p",
808
+ "--model", model,
809
+ "--system-prompt", system_prompt,
810
+ "--output-format", "stream-json",
811
+ "--session-id", session_id,
812
+ "--dangerously-skip-permissions",
813
+ ]
814
+
815
+ # Write MCP config from crush.json mcpServers if present
816
+ crush_config_path = work_dir / _CRUSH_CONFIG_FILE
817
+ if crush_config_path.exists():
818
+ try:
819
+ crush_data = json.loads(
820
+ crush_config_path.read_text(encoding="utf-8")
821
+ )
822
+ mcp_servers = crush_data.get("mcpServers")
823
+ if mcp_servers:
824
+ mcp_config = {"mcpServers": mcp_servers}
825
+ mcp_config_file = work_dir / "mcp_config.json"
826
+ mcp_config_file.write_text(
827
+ json.dumps(mcp_config, indent=2), encoding="utf-8"
828
+ )
829
+ cmd.extend(["--mcp-config", str(mcp_config_file)])
830
+ except (json.JSONDecodeError, OSError):
831
+ pass
832
+
833
+ # Initial prompt
834
+ initial_prompt = (
835
+ f"You are agent '{agent_name}'. "
836
+ f"Check your inbox at ~/.skcapstone/comms/"
837
+ f"{session_config.get('team_name', 'default')}/{agent_name}/inbox/ "
838
+ f"for tasks. Process any pending work and report results to your outbox."
839
+ )
840
+ cmd.append(initial_prompt)
841
+
842
+ log_file = work_dir / "agent.log"
843
+
844
+ try:
845
+ with open(log_file, "ab") as log_fh:
846
+ proc = subprocess.Popen(
847
+ cmd,
848
+ cwd=str(work_dir),
849
+ env=env,
850
+ stdout=log_fh,
851
+ stderr=log_fh,
852
+ start_new_session=True,
853
+ )
854
+ except OSError as exc:
855
+ logger.error(
856
+ "start: failed to launch claude session for %s: %s",
857
+ agent_name, exc,
858
+ )
859
+ return False
860
+
861
+ pid = proc.pid
862
+ (work_dir / _PID_FILE).write_text(str(pid), encoding="utf-8")
863
+ provision_result["pid"] = pid
864
+
865
+ _write_session_state(work_dir, {
866
+ "status": _STATE_RUNNING,
867
+ "pid": pid,
868
+ "agent_name": agent_name,
869
+ "started_at": _now_iso(),
870
+ "binary": binary,
871
+ "session_id": session_id,
872
+ "backend": "claude",
873
+ })
874
+
875
+ logger.info(
876
+ "Started claude session for %s (pid=%d session_id=%s)",
877
+ agent_name, pid, session_id,
878
+ )
879
+ return True
880
+
881
+ def _start_stub_session(
882
+ self,
883
+ agent_name: str,
884
+ work_dir: Path,
885
+ env: Dict[str, str],
886
+ provision_result: Dict[str, Any],
887
+ ) -> bool:
888
+ """Launch a Python stub process when crush is not available.
889
+
890
+ The stub writes a running state file and then sleeps until signalled,
891
+ giving the engine a real process to monitor without a full AI runtime.
892
+
893
+ Args:
894
+ agent_name: Agent instance name.
895
+ work_dir: Agent working directory.
896
+ env: Environment variables for the subprocess.
897
+ provision_result: Mutated in-place with the spawned PID.
898
+
899
+ Returns:
900
+ True if the stub started successfully.
901
+ """
902
+ state_file = str(work_dir / _SESSION_STATE_FILE)
903
+ stub_script = _stub_script(agent_name, state_file)
904
+
905
+ try:
906
+ proc = subprocess.Popen(
907
+ [os.sys.executable, "-c", stub_script],
908
+ cwd=str(work_dir),
909
+ env=env,
910
+ stdout=subprocess.DEVNULL,
911
+ stderr=subprocess.DEVNULL,
912
+ start_new_session=True,
913
+ )
914
+ except OSError as exc:
915
+ logger.error(
916
+ "start: failed to launch stub for %s: %s", agent_name, exc
917
+ )
918
+ return False
919
+
920
+ pid = proc.pid
921
+ (work_dir / _PID_FILE).write_text(str(pid), encoding="utf-8")
922
+ provision_result["pid"] = pid
923
+
924
+ _write_session_state(work_dir, {
925
+ "status": _STATE_RUNNING,
926
+ "pid": pid,
927
+ "agent_name": agent_name,
928
+ "started_at": _now_iso(),
929
+ "binary": "python-stub",
930
+ })
931
+
932
+ logger.info(
933
+ "Started stub session for %s (pid=%d)", agent_name, pid
934
+ )
935
+ return True
936
+
937
+ # ------------------------------------------------------------------
938
+ # stop
939
+ # ------------------------------------------------------------------
940
+
941
+ def stop(
942
+ self,
943
+ agent_name: str,
944
+ provision_result: Dict[str, Any],
945
+ ) -> bool:
946
+ """Gracefully stop the agent session (SIGTERM → wait → SIGKILL).
947
+
948
+ Args:
949
+ agent_name: Agent instance name.
950
+ provision_result: Output from provision() / start().
951
+
952
+ Returns:
953
+ True if the process is no longer running.
954
+ """
955
+ work_dir_str = provision_result.get("work_dir", "")
956
+ work_dir = Path(work_dir_str) if work_dir_str else None
957
+
958
+ pid = provision_result.get("pid")
959
+ if pid is None and work_dir:
960
+ pid = _read_pid(work_dir)
961
+
962
+ if not pid:
963
+ logger.debug("stop: no pid for %s — already stopped", agent_name)
964
+ self._write_stopped_state(agent_name, work_dir)
965
+ return True
966
+
967
+ if not _pid_is_alive(pid):
968
+ logger.debug(
969
+ "stop: pid %d for %s is already dead", pid, agent_name
970
+ )
971
+ self._write_stopped_state(agent_name, work_dir)
972
+ return True
973
+
974
+ # SIGTERM — polite shutdown
975
+ try:
976
+ os.kill(pid, signal.SIGTERM)
977
+ except ProcessLookupError:
978
+ self._write_stopped_state(agent_name, work_dir)
979
+ return True
980
+ except OSError as exc:
981
+ logger.warning(
982
+ "stop: SIGTERM failed for %s (pid %d): %s", agent_name, pid, exc
983
+ )
984
+ return False
985
+
986
+ # Wait for graceful shutdown
987
+ deadline = time.time() + _STOP_TIMEOUT_SECONDS
988
+ while time.time() < deadline:
989
+ if not _pid_is_alive(pid):
990
+ break
991
+ time.sleep(0.5)
992
+
993
+ if _pid_is_alive(pid):
994
+ # Escalate to SIGKILL
995
+ logger.warning(
996
+ "stop: %s (pid %d) did not exit after %ds, sending SIGKILL",
997
+ agent_name, pid, _STOP_TIMEOUT_SECONDS,
998
+ )
999
+ try:
1000
+ os.kill(pid, signal.SIGKILL)
1001
+ except OSError:
1002
+ pass
1003
+
1004
+ kill_deadline = time.time() + _STOP_KILL_TIMEOUT_SECONDS
1005
+ while time.time() < kill_deadline:
1006
+ if not _pid_is_alive(pid):
1007
+ break
1008
+ time.sleep(0.2)
1009
+
1010
+ stopped = not _pid_is_alive(pid)
1011
+ self._write_stopped_state(agent_name, work_dir)
1012
+
1013
+ # Clean up SKSkills session resources
1014
+ if work_dir:
1015
+ try:
1016
+ from ..session_skills import cleanup_session_skills
1017
+ cleanup_session_skills(agent_name, work_dir)
1018
+ except ImportError:
1019
+ pass
1020
+
1021
+ logger.info("Stopped agent %s (pid=%d ok=%s)", agent_name, pid, stopped)
1022
+ return stopped
1023
+
1024
+ def _write_stopped_state(
1025
+ self, agent_name: str, work_dir: Optional[Path]
1026
+ ) -> None:
1027
+ """Write a STOPPED tombstone to the session state file.
1028
+
1029
+ Args:
1030
+ agent_name: Agent instance name.
1031
+ work_dir: Agent working directory (may be None).
1032
+ """
1033
+ if work_dir and work_dir.exists():
1034
+ _write_session_state(work_dir, {
1035
+ "status": _STATE_STOPPED,
1036
+ "agent_name": agent_name,
1037
+ "stopped_at": _now_iso(),
1038
+ })
1039
+
1040
+ # ------------------------------------------------------------------
1041
+ # destroy
1042
+ # ------------------------------------------------------------------
1043
+
1044
+ def destroy(
1045
+ self,
1046
+ agent_name: str,
1047
+ provision_result: Dict[str, Any],
1048
+ ) -> bool:
1049
+ """Stop the session and remove all agent files.
1050
+
1051
+ Args:
1052
+ agent_name: Agent instance name.
1053
+ provision_result: Output from provision().
1054
+
1055
+ Returns:
1056
+ True if cleanup succeeded.
1057
+ """
1058
+ self.stop(agent_name, provision_result)
1059
+
1060
+ work_dir_str = provision_result.get("work_dir", "")
1061
+ if work_dir_str:
1062
+ import shutil
1063
+
1064
+ work_dir_path = Path(work_dir_str)
1065
+ if work_dir_path.exists():
1066
+ shutil.rmtree(work_dir_path)
1067
+ logger.info("Destroyed agent directory: %s", work_dir_path)
1068
+
1069
+ return True
1070
+
1071
+ # ------------------------------------------------------------------
1072
+ # health_check
1073
+ # ------------------------------------------------------------------
1074
+
1075
+ def health_check(
1076
+ self,
1077
+ agent_name: str,
1078
+ provision_result: Dict[str, Any],
1079
+ ) -> AgentStatus:
1080
+ """Check agent health via session state file, then PID liveness.
1081
+
1082
+ Primary check: read ``session_state.json`` written by the crush daemon.
1083
+ Fallback: raw PID liveness check (signal 0).
1084
+
1085
+ Args:
1086
+ agent_name: Agent instance name.
1087
+ provision_result: Output from provision() / start().
1088
+
1089
+ Returns:
1090
+ AgentStatus based on session state and process liveness.
1091
+ """
1092
+ work_dir_str = provision_result.get("work_dir", "")
1093
+ work_dir = Path(work_dir_str) if work_dir_str else None
1094
+
1095
+ pid = provision_result.get("pid")
1096
+ if pid is None and work_dir:
1097
+ pid = _read_pid(work_dir)
1098
+
1099
+ # --- Primary: session state file ---
1100
+ if work_dir:
1101
+ state = _read_session_state(work_dir)
1102
+ if state:
1103
+ return _session_state_to_agent_status(state, pid)
1104
+
1105
+ # --- Fallback: raw PID check ---
1106
+ if not pid:
1107
+ return AgentStatus.STOPPED
1108
+
1109
+ if _pid_is_alive(pid):
1110
+ return AgentStatus.RUNNING
1111
+
1112
+ return AgentStatus.STOPPED
1113
+
1114
+
1115
+ # ---------------------------------------------------------------------------
1116
+ # Internal helpers
1117
+ # ---------------------------------------------------------------------------
1118
+
1119
+
1120
+ def _session_state_to_agent_status(
1121
+ state: Dict[str, Any], pid: Optional[int]
1122
+ ) -> AgentStatus:
1123
+ """Map a session state dict to an AgentStatus value.
1124
+
1125
+ Args:
1126
+ state: Dictionary read from session_state.json.
1127
+ pid: Current known PID for corroboration.
1128
+
1129
+ Returns:
1130
+ Appropriate AgentStatus.
1131
+ """
1132
+ raw_status = state.get("status", "").lower()
1133
+
1134
+ if raw_status in (_STATE_RUNNING, _STATE_IDLE):
1135
+ # Corroborate with PID liveness if we have a PID
1136
+ state_pid = state.get("pid") or pid
1137
+ if state_pid and not _pid_is_alive(int(state_pid)):
1138
+ return AgentStatus.DEGRADED
1139
+ return AgentStatus.RUNNING
1140
+
1141
+ if raw_status == _STATE_ERROR:
1142
+ return AgentStatus.DEGRADED
1143
+
1144
+ if raw_status == _STATE_STOPPED:
1145
+ return AgentStatus.STOPPED
1146
+
1147
+ # Unknown state value
1148
+ return AgentStatus.DEGRADED
1149
+
1150
+
1151
+ def _stub_script(agent_name: str, state_file: str) -> str:
1152
+ """Return Python source for the lightweight stub process.
1153
+
1154
+ The stub writes a running state, then sleeps until SIGTERM/SIGINT.
1155
+
1156
+ Args:
1157
+ agent_name: Agent instance name.
1158
+ state_file: Absolute path to the session state file.
1159
+
1160
+ Returns:
1161
+ Python source string safe for ``python -c``.
1162
+ """
1163
+ # Reason: single-quoted string avoids shell escaping issues; json import
1164
+ # and signal handling give the stub a clean lifecycle for testing.
1165
+ return (
1166
+ "import json, os, signal, sys, time\n"
1167
+ f"state_file = {repr(state_file)}\n"
1168
+ f"agent_name = {repr(agent_name)}\n"
1169
+ "running = True\n"
1170
+ "def _stop(sig, frame):\n"
1171
+ " global running\n"
1172
+ " running = False\n"
1173
+ "signal.signal(signal.SIGTERM, _stop)\n"
1174
+ "signal.signal(signal.SIGINT, _stop)\n"
1175
+ "with open(state_file, 'w') as f:\n"
1176
+ " json.dump({'status': 'running', 'pid': os.getpid(), "
1177
+ " 'agent_name': agent_name}, f)\n"
1178
+ "while running:\n"
1179
+ " time.sleep(1)\n"
1180
+ "with open(state_file, 'w') as f:\n"
1181
+ " json.dump({'status': 'stopped', 'agent_name': agent_name}, f)\n"
1182
+ )
1183
+
1184
+
1185
+ def _now_iso() -> str:
1186
+ """Return the current UTC time as an ISO 8601 string.
1187
+
1188
+ Returns:
1189
+ ISO 8601 timestamp string.
1190
+ """
1191
+ from datetime import datetime, timezone
1192
+
1193
+ return datetime.now(timezone.utc).isoformat()