@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,591 @@
1
+ """Tests for LocalProvider — local process-backed agent deployment.
2
+
3
+ All subprocess and filesystem side effects are controlled via tmp_path
4
+ and unittest.mock so no real crush/claude binary is required.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import signal
12
+ from pathlib import Path
13
+ from typing import Any, Dict
14
+ from unittest.mock import MagicMock, patch
15
+
16
+ import pytest
17
+
18
+ from skcapstone.blueprints.schema import AgentRole, AgentSpec, ModelTier, ResourceSpec
19
+ from skcapstone.providers.local import (
20
+ LocalProvider,
21
+ _build_crush_config,
22
+ _build_session_config,
23
+ _find_crush_binary,
24
+ _is_claude_binary,
25
+ _pid_is_alive,
26
+ _read_pid,
27
+ _read_session_state,
28
+ _resolve_skill_paths,
29
+ _resolve_soul_blueprint_path,
30
+ _session_state_to_agent_status,
31
+ _stub_script,
32
+ _write_session_state,
33
+ )
34
+ from skcapstone.team_engine import AgentStatus
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Helpers
39
+ # ---------------------------------------------------------------------------
40
+
41
+
42
+ def _make_spec(
43
+ role: str = "worker",
44
+ model: str = "fast",
45
+ memory: str = "2g",
46
+ cores: int = 1,
47
+ skills: list | None = None,
48
+ soul_blueprint: str | None = None,
49
+ ) -> AgentSpec:
50
+ return AgentSpec(
51
+ role=AgentRole(role),
52
+ model=ModelTier(model),
53
+ resources=ResourceSpec(memory=memory, cores=cores),
54
+ skills=skills or [],
55
+ env={},
56
+ soul_blueprint=soul_blueprint,
57
+ )
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # _find_crush_binary
62
+ # ---------------------------------------------------------------------------
63
+
64
+
65
+ class TestFindCrushBinary:
66
+ def test_returns_path_when_found(self):
67
+ with patch("shutil.which", return_value="/usr/bin/crush"):
68
+ result = _find_crush_binary()
69
+ assert result == "/usr/bin/crush"
70
+
71
+ def test_returns_none_when_not_found(self):
72
+ with patch("shutil.which", return_value=None):
73
+ result = _find_crush_binary()
74
+ assert result is None
75
+
76
+ def test_falls_back_to_claude(self):
77
+ def _which(name):
78
+ return "/usr/local/bin/claude" if name == "claude" else None
79
+
80
+ with patch("shutil.which", side_effect=_which):
81
+ result = _find_crush_binary()
82
+ assert result == "/usr/local/bin/claude"
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # _is_claude_binary
87
+ # ---------------------------------------------------------------------------
88
+
89
+
90
+ class TestIsCaudeBinary:
91
+ def test_returns_true_for_claude(self):
92
+ assert _is_claude_binary("/usr/local/bin/claude") is True
93
+
94
+ def test_returns_false_for_crush(self):
95
+ assert _is_claude_binary("/usr/bin/crush") is False
96
+
97
+ def test_returns_false_for_python(self):
98
+ assert _is_claude_binary("/usr/bin/python3") is False
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # _resolve_soul_blueprint_path
103
+ # ---------------------------------------------------------------------------
104
+
105
+
106
+ class TestResolveSoulBlueprintPath:
107
+ def test_returns_none_for_empty(self, tmp_path):
108
+ result = _resolve_soul_blueprint_path(None, tmp_path)
109
+ assert result is None
110
+
111
+ def test_returns_absolute_path_unchanged(self, tmp_path):
112
+ bp = tmp_path / "LUMINA.md"
113
+ bp.write_text("soul")
114
+ result = _resolve_soul_blueprint_path(str(bp), tmp_path)
115
+ assert result == str(bp)
116
+
117
+ def test_resolves_via_repo_root_blueprints(self, tmp_path):
118
+ bp_dir = tmp_path / "soul-blueprints" / "blueprints" / "lumina"
119
+ bp_dir.mkdir(parents=True)
120
+ result = _resolve_soul_blueprint_path("lumina", tmp_path, repo_root=tmp_path)
121
+ assert result == str(bp_dir)
122
+
123
+ def test_resolves_relative_to_work_dir(self, tmp_path):
124
+ bp = tmp_path / "my_soul.md"
125
+ bp.write_text("soul")
126
+ result = _resolve_soul_blueprint_path("my_soul.md", tmp_path)
127
+ assert result == str(bp)
128
+
129
+ def test_returns_original_when_unresolvable(self, tmp_path):
130
+ result = _resolve_soul_blueprint_path("ghost_blueprint", tmp_path)
131
+ assert result == "ghost_blueprint"
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # _resolve_skill_paths
136
+ # ---------------------------------------------------------------------------
137
+
138
+
139
+ class TestResolveSkillPaths:
140
+ def test_keeps_non_existent_as_is(self):
141
+ result = _resolve_skill_paths(["my-skill"])
142
+ assert result == ["my-skill"]
143
+
144
+ def test_resolves_absolute_existing_path(self, tmp_path):
145
+ skill_file = tmp_path / "my_skill.yaml"
146
+ skill_file.write_text("skill: true")
147
+ result = _resolve_skill_paths([str(skill_file)])
148
+ assert result == [str(skill_file)]
149
+
150
+ def test_empty_list_returns_empty(self):
151
+ assert _resolve_skill_paths([]) == []
152
+
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # _build_session_config
156
+ # ---------------------------------------------------------------------------
157
+
158
+
159
+ class TestBuildSessionConfig:
160
+ def test_returns_required_keys(self, tmp_path):
161
+ spec = _make_spec()
162
+ config = _build_session_config("agent-1", "team-a", spec, tmp_path)
163
+ assert config["agent_name"] == "agent-1"
164
+ assert config["team_name"] == "team-a"
165
+ assert config["role"] == "worker"
166
+ assert "model" in config
167
+ assert "memory_dir" in config
168
+ assert "scratch_dir" in config
169
+ assert "state_file" in config
170
+
171
+ def test_soul_blueprint_resolved(self, tmp_path):
172
+ spec = _make_spec(soul_blueprint=None)
173
+ config = _build_session_config("agent-1", "team", spec, tmp_path)
174
+ assert config["soul_blueprint"] is None
175
+
176
+ def test_model_tier_used_as_fallback(self, tmp_path):
177
+ spec = _make_spec(model="fast")
178
+ with patch(
179
+ "skcapstone.providers.local._resolve_model_via_router",
180
+ return_value="claude-haiku-4-5",
181
+ ):
182
+ config = _build_session_config("a", "t", spec, tmp_path)
183
+ assert config["model"] == "claude-haiku-4-5"
184
+
185
+
186
+ # ---------------------------------------------------------------------------
187
+ # _build_crush_config
188
+ # ---------------------------------------------------------------------------
189
+
190
+
191
+ class TestBuildCrushConfig:
192
+ def test_has_schema_key(self, tmp_path):
193
+ session_cfg = {"agent_name": "a", "soul_blueprint": None, "model": "fast", "role": "worker", "skills": []}
194
+ cfg = _build_crush_config("a", session_cfg, tmp_path)
195
+ assert "$schema" in cfg
196
+
197
+ def test_session_keys_present(self, tmp_path):
198
+ session_cfg = {
199
+ "agent_name": "bob",
200
+ "soul_blueprint": "/blueprints/lumina",
201
+ "model": "code",
202
+ "role": "coder",
203
+ "skills": ["sk1"],
204
+ "memory_dir": str(tmp_path / "memory"),
205
+ "state_file": str(tmp_path / "state.json"),
206
+ }
207
+ cfg = _build_crush_config("bob", session_cfg, tmp_path)
208
+ assert cfg["session"]["agent_name"] == "bob"
209
+ assert cfg["session"]["role"] == "coder"
210
+ assert cfg["session"]["model"] == "code"
211
+
212
+ def test_none_soul_removed_from_context_paths(self, tmp_path):
213
+ session_cfg = {"agent_name": "a", "soul_blueprint": None, "model": "fast", "role": "worker", "skills": []}
214
+ cfg = _build_crush_config("a", session_cfg, tmp_path)
215
+ assert None not in cfg["options"]["context_paths"]
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Session state helpers
220
+ # ---------------------------------------------------------------------------
221
+
222
+
223
+ class TestSessionStateHelpers:
224
+ def test_write_and_read_roundtrip(self, tmp_path):
225
+ state = {"status": "running", "pid": 1234}
226
+ _write_session_state(tmp_path, state)
227
+ result = _read_session_state(tmp_path)
228
+ assert result == state
229
+
230
+ def test_read_missing_returns_none(self, tmp_path):
231
+ result = _read_session_state(tmp_path)
232
+ assert result is None
233
+
234
+ def test_read_corrupt_returns_none(self, tmp_path):
235
+ (tmp_path / "session_state.json").write_text("not-json")
236
+ assert _read_session_state(tmp_path) is None
237
+
238
+ def test_read_pid_returns_int(self, tmp_path):
239
+ (tmp_path / "agent.pid").write_text("5678\n")
240
+ assert _read_pid(tmp_path) == 5678
241
+
242
+ def test_read_pid_missing_returns_none(self, tmp_path):
243
+ assert _read_pid(tmp_path) is None
244
+
245
+ def test_read_pid_invalid_returns_none(self, tmp_path):
246
+ (tmp_path / "agent.pid").write_text("notanumber")
247
+ assert _read_pid(tmp_path) is None
248
+
249
+
250
+ # ---------------------------------------------------------------------------
251
+ # _pid_is_alive
252
+ # ---------------------------------------------------------------------------
253
+
254
+
255
+ class TestPidIsAlive:
256
+ def test_alive_process(self):
257
+ # Use os.getpid() — current process is always alive.
258
+ assert _pid_is_alive(os.getpid()) is True
259
+
260
+ def test_dead_process(self):
261
+ with patch("os.kill", side_effect=ProcessLookupError):
262
+ assert _pid_is_alive(99999) is False
263
+
264
+ def test_permission_denied_counts_as_alive(self):
265
+ with patch("os.kill", side_effect=OSError("permission denied")):
266
+ assert _pid_is_alive(1) is True
267
+
268
+
269
+ # ---------------------------------------------------------------------------
270
+ # _session_state_to_agent_status
271
+ # ---------------------------------------------------------------------------
272
+
273
+
274
+ class TestSessionStateToAgentStatus:
275
+ def test_running_status(self):
276
+ with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
277
+ status = _session_state_to_agent_status({"status": "running", "pid": 1}, 1)
278
+ assert status == AgentStatus.RUNNING
279
+
280
+ def test_idle_status(self):
281
+ with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
282
+ status = _session_state_to_agent_status({"status": "idle", "pid": 1}, 1)
283
+ assert status == AgentStatus.RUNNING
284
+
285
+ def test_running_but_dead_pid_is_degraded(self):
286
+ with patch("skcapstone.providers.local._pid_is_alive", return_value=False):
287
+ status = _session_state_to_agent_status({"status": "running", "pid": 1}, 1)
288
+ assert status == AgentStatus.DEGRADED
289
+
290
+ def test_error_status_is_degraded(self):
291
+ status = _session_state_to_agent_status({"status": "error"}, None)
292
+ assert status == AgentStatus.DEGRADED
293
+
294
+ def test_stopped_status(self):
295
+ status = _session_state_to_agent_status({"status": "stopped"}, None)
296
+ assert status == AgentStatus.STOPPED
297
+
298
+ def test_unknown_status_is_degraded(self):
299
+ status = _session_state_to_agent_status({"status": "whatever"}, None)
300
+ assert status == AgentStatus.DEGRADED
301
+
302
+
303
+ # ---------------------------------------------------------------------------
304
+ # LocalProvider.provision
305
+ # ---------------------------------------------------------------------------
306
+
307
+
308
+ class TestLocalProviderProvision:
309
+ @pytest.fixture()
310
+ def provider(self, tmp_path):
311
+ return LocalProvider(
312
+ home=tmp_path / "home",
313
+ work_dir=tmp_path / "agents",
314
+ )
315
+
316
+ def test_creates_agent_directories(self, provider, tmp_path):
317
+ spec = _make_spec()
318
+ result = provider.provision("agent-1", spec, "team-a")
319
+ agent_dir = Path(result["work_dir"])
320
+ assert (agent_dir / "memory").is_dir()
321
+ assert (agent_dir / "scratch").is_dir()
322
+
323
+ def test_writes_session_json(self, provider):
324
+ spec = _make_spec()
325
+ result = provider.provision("agent-1", spec, "team-a")
326
+ session_file = Path(result["work_dir"]) / "session.json"
327
+ assert session_file.exists()
328
+ data = json.loads(session_file.read_text())
329
+ assert data["agent_name"] == "agent-1"
330
+ assert data["team_name"] == "team-a"
331
+
332
+ def test_host_is_localhost(self, provider):
333
+ result = provider.provision("a", _make_spec(), "t")
334
+ assert result["host"] == "localhost"
335
+
336
+ def test_returns_session_config(self, provider):
337
+ result = provider.provision("a", _make_spec(), "t")
338
+ assert "session_config" in result
339
+
340
+
341
+ # ---------------------------------------------------------------------------
342
+ # LocalProvider.configure
343
+ # ---------------------------------------------------------------------------
344
+
345
+
346
+ class TestLocalProviderConfigure:
347
+ @pytest.fixture()
348
+ def provider(self, tmp_path):
349
+ return LocalProvider(
350
+ home=tmp_path / "home",
351
+ work_dir=tmp_path / "agents",
352
+ )
353
+
354
+ def test_writes_crush_json(self, provider):
355
+ spec = _make_spec()
356
+ prov = provider.provision("agent-cfg", spec, "team")
357
+ ok = provider.configure("agent-cfg", spec, prov)
358
+ assert ok is True
359
+ crush_file = Path(prov["work_dir"]) / "crush.json"
360
+ assert crush_file.exists()
361
+
362
+ def test_configure_missing_work_dir_returns_false(self, provider):
363
+ result = provider.configure("a", _make_spec(), {})
364
+ assert result is False
365
+
366
+ def test_configure_invalid_work_dir_returns_false(self, provider):
367
+ result = provider.configure("a", _make_spec(), {"work_dir": ""})
368
+ assert result is False
369
+
370
+
371
+ # ---------------------------------------------------------------------------
372
+ # LocalProvider.start — stub path (no crush binary)
373
+ # ---------------------------------------------------------------------------
374
+
375
+
376
+ class TestLocalProviderStart:
377
+ @pytest.fixture()
378
+ def provider(self, tmp_path):
379
+ return LocalProvider(
380
+ home=tmp_path / "home",
381
+ work_dir=tmp_path / "agents",
382
+ crush_binary=None,
383
+ )
384
+
385
+ def test_start_missing_work_dir_returns_false(self, provider):
386
+ result = provider.start("a", {})
387
+ assert result is False
388
+
389
+ def test_start_with_stub_when_no_binary(self, provider):
390
+ spec = _make_spec()
391
+ prov = provider.provision("agent-stub", spec, "team")
392
+ with patch("skcapstone.providers.local._find_crush_binary", return_value=None):
393
+ ok = provider.start("agent-stub", prov)
394
+ assert ok is True
395
+ assert prov.get("pid") is not None
396
+ # Clean up the spawned stub
397
+ try:
398
+ os.kill(prov["pid"], signal.SIGTERM)
399
+ except OSError:
400
+ pass
401
+
402
+ def test_start_with_crush_binary(self, provider, tmp_path):
403
+ spec = _make_spec()
404
+ prov = provider.provision("agent-crush", spec, "team")
405
+ mock_proc = MagicMock()
406
+ mock_proc.pid = 12345
407
+ with patch("subprocess.Popen", return_value=mock_proc) as mock_popen:
408
+ with patch(
409
+ "skcapstone.providers.local._find_crush_binary",
410
+ return_value="/usr/bin/crush",
411
+ ):
412
+ ok = provider.start("agent-crush", prov)
413
+ assert ok is True
414
+ assert prov["pid"] == 12345
415
+ mock_popen.assert_called_once()
416
+
417
+ def test_start_with_claude_binary(self, provider, tmp_path):
418
+ spec = _make_spec()
419
+ prov = provider.provision("agent-claude", spec, "team")
420
+ mock_proc = MagicMock()
421
+ mock_proc.pid = 99999
422
+ with patch("subprocess.Popen", return_value=mock_proc):
423
+ with patch(
424
+ "skcapstone.providers.local._find_crush_binary",
425
+ return_value="/usr/bin/claude",
426
+ ):
427
+ ok = provider.start("agent-claude", prov)
428
+ assert ok is True
429
+ assert prov["pid"] == 99999
430
+
431
+ def test_start_crush_oserror_returns_false(self, provider, tmp_path):
432
+ spec = _make_spec()
433
+ prov = provider.provision("agent-err", spec, "team")
434
+ with patch("subprocess.Popen", side_effect=OSError("no such file")):
435
+ with patch(
436
+ "skcapstone.providers.local._find_crush_binary",
437
+ return_value="/usr/bin/crush",
438
+ ):
439
+ ok = provider.start("agent-err", prov)
440
+ assert ok is False
441
+
442
+
443
+ # ---------------------------------------------------------------------------
444
+ # LocalProvider.stop
445
+ # ---------------------------------------------------------------------------
446
+
447
+
448
+ class TestLocalProviderStop:
449
+ @pytest.fixture()
450
+ def provider(self, tmp_path):
451
+ return LocalProvider(
452
+ home=tmp_path / "home",
453
+ work_dir=tmp_path / "agents",
454
+ )
455
+
456
+ def test_stop_no_pid_returns_true(self, provider, tmp_path):
457
+ ok = provider.stop("a", {"work_dir": str(tmp_path)})
458
+ assert ok is True
459
+
460
+ def test_stop_already_dead_returns_true(self, provider, tmp_path):
461
+ with patch("skcapstone.providers.local._pid_is_alive", return_value=False):
462
+ ok = provider.stop("a", {"pid": 12345, "work_dir": str(tmp_path)})
463
+ assert ok is True
464
+
465
+ def test_stop_sends_sigterm(self, provider, tmp_path):
466
+ kill_calls = []
467
+
468
+ def fake_kill(pid, sig):
469
+ kill_calls.append((pid, sig))
470
+
471
+ # First call: process is alive (don't early-exit), then dead (exit wait loop)
472
+ alive_iter = iter([True, False])
473
+
474
+ with patch("os.kill", side_effect=fake_kill):
475
+ with patch(
476
+ "skcapstone.providers.local._pid_is_alive",
477
+ side_effect=lambda p: next(alive_iter, False),
478
+ ):
479
+ with patch("time.sleep"):
480
+ ok = provider.stop("a", {"pid": 12345, "work_dir": str(tmp_path)})
481
+ assert any(sig == signal.SIGTERM for _, sig in kill_calls)
482
+ assert ok is True
483
+
484
+ def test_stop_process_lookup_error_returns_true(self, provider, tmp_path):
485
+ with patch("os.kill", side_effect=ProcessLookupError):
486
+ with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
487
+ ok = provider.stop("a", {"pid": 12345, "work_dir": str(tmp_path)})
488
+ assert ok is True
489
+
490
+ def test_stop_sigterm_oserror_returns_false(self, provider, tmp_path):
491
+ with patch("os.kill", side_effect=OSError("permission denied")):
492
+ with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
493
+ ok = provider.stop("a", {"pid": 12345, "work_dir": str(tmp_path)})
494
+ assert ok is False
495
+
496
+
497
+ # ---------------------------------------------------------------------------
498
+ # LocalProvider.destroy
499
+ # ---------------------------------------------------------------------------
500
+
501
+
502
+ class TestLocalProviderDestroy:
503
+ def test_destroy_removes_directory(self, tmp_path):
504
+ provider = LocalProvider(
505
+ home=tmp_path / "home",
506
+ work_dir=tmp_path / "agents",
507
+ )
508
+ spec = _make_spec()
509
+ prov = provider.provision("agent-del", spec, "team")
510
+ agent_dir = Path(prov["work_dir"])
511
+ assert agent_dir.exists()
512
+
513
+ with patch.object(provider, "stop", return_value=True):
514
+ ok = provider.destroy("agent-del", prov)
515
+ assert ok is True
516
+ assert not agent_dir.exists()
517
+
518
+ def test_destroy_missing_work_dir_returns_true(self, tmp_path):
519
+ provider = LocalProvider(
520
+ home=tmp_path / "home",
521
+ work_dir=tmp_path / "agents",
522
+ )
523
+ with patch.object(provider, "stop", return_value=True):
524
+ ok = provider.destroy("a", {})
525
+ assert ok is True
526
+
527
+
528
+ # ---------------------------------------------------------------------------
529
+ # LocalProvider.health_check
530
+ # ---------------------------------------------------------------------------
531
+
532
+
533
+ class TestLocalProviderHealthCheck:
534
+ @pytest.fixture()
535
+ def provider(self, tmp_path):
536
+ return LocalProvider(
537
+ home=tmp_path / "home",
538
+ work_dir=tmp_path / "agents",
539
+ )
540
+
541
+ def test_health_running_from_state_file(self, provider, tmp_path):
542
+ work_dir = tmp_path / "agent-hc"
543
+ work_dir.mkdir()
544
+ _write_session_state(work_dir, {"status": "running", "pid": 1})
545
+ with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
546
+ status = provider.health_check("a", {"work_dir": str(work_dir), "pid": 1})
547
+ assert status == AgentStatus.RUNNING
548
+
549
+ def test_health_stopped_from_state_file(self, provider, tmp_path):
550
+ work_dir = tmp_path / "agent-stopped"
551
+ work_dir.mkdir()
552
+ _write_session_state(work_dir, {"status": "stopped"})
553
+ status = provider.health_check("a", {"work_dir": str(work_dir)})
554
+ assert status == AgentStatus.STOPPED
555
+
556
+ def test_health_no_work_dir_no_pid_returns_stopped(self, provider):
557
+ status = provider.health_check("a", {})
558
+ assert status == AgentStatus.STOPPED
559
+
560
+ def test_health_pid_alive_no_state_file(self, provider, tmp_path):
561
+ work_dir = tmp_path / "no-state"
562
+ work_dir.mkdir()
563
+ with patch("skcapstone.providers.local._pid_is_alive", return_value=True):
564
+ status = provider.health_check("a", {"work_dir": str(work_dir), "pid": 1})
565
+ assert status == AgentStatus.RUNNING
566
+
567
+ def test_health_pid_dead_no_state_file(self, provider, tmp_path):
568
+ work_dir = tmp_path / "dead-pid"
569
+ work_dir.mkdir()
570
+ with patch("skcapstone.providers.local._pid_is_alive", return_value=False):
571
+ status = provider.health_check("a", {"work_dir": str(work_dir), "pid": 1})
572
+ assert status == AgentStatus.STOPPED
573
+
574
+
575
+ # ---------------------------------------------------------------------------
576
+ # _stub_script
577
+ # ---------------------------------------------------------------------------
578
+
579
+
580
+ class TestStubScript:
581
+ def test_contains_agent_name(self):
582
+ script = _stub_script("my-agent", "/tmp/state.json")
583
+ assert "my-agent" in script
584
+
585
+ def test_contains_state_file(self):
586
+ script = _stub_script("a", "/tmp/agent_state.json")
587
+ assert "/tmp/agent_state.json" in script
588
+
589
+ def test_contains_signal_handling(self):
590
+ script = _stub_script("a", "/tmp/s.json")
591
+ assert "SIGTERM" in script