rubik 0.1.0

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 (523) hide show
  1. checksums.yaml +7 -0
  2. data/ATTRIBUTION.md +139 -0
  3. data/CHANGELOG.md +236 -0
  4. data/LICENSE +21 -0
  5. data/README.md +269 -0
  6. data/bin/rubik +82 -0
  7. data/lib/rubik/adapter/base.rb +195 -0
  8. data/lib/rubik/adapter/c.rb +36 -0
  9. data/lib/rubik/adapter/crystal.rb +78 -0
  10. data/lib/rubik/adapter/generic.rb +86 -0
  11. data/lib/rubik/adapter/go.rb +36 -0
  12. data/lib/rubik/adapter/node.rb +146 -0
  13. data/lib/rubik/adapter/process.rb +78 -0
  14. data/lib/rubik/adapter/python.rb +30 -0
  15. data/lib/rubik/adapter/registry.rb +81 -0
  16. data/lib/rubik/adapter/ruby.rb +30 -0
  17. data/lib/rubik/adapter/rust.rb +36 -0
  18. data/lib/rubik/adapter/zig.rb +36 -0
  19. data/lib/rubik/adapter.rb +18 -0
  20. data/lib/rubik/ast/body_loader.rb +19 -0
  21. data/lib/rubik/ast/c_extractor.rb +114 -0
  22. data/lib/rubik/ast/cache_store.rb +191 -0
  23. data/lib/rubik/ast/extractor.rb +59 -0
  24. data/lib/rubik/ast/fallback_cleaner.rb +145 -0
  25. data/lib/rubik/ast/go_extractor.rb +148 -0
  26. data/lib/rubik/ast/graph.rb +272 -0
  27. data/lib/rubik/ast/python_extractor.rb +120 -0
  28. data/lib/rubik/ast/ruby_extractor.rb +260 -0
  29. data/lib/rubik/ast/rust_extractor.rb +127 -0
  30. data/lib/rubik/ast/symbol.rb +103 -0
  31. data/lib/rubik/ast/treesitter/base_walker.rb +109 -0
  32. data/lib/rubik/ast/treesitter/c_walker.rb +122 -0
  33. data/lib/rubik/ast/treesitter/go_walker.rb +120 -0
  34. data/lib/rubik/ast/treesitter/python_walker.rb +107 -0
  35. data/lib/rubik/ast/treesitter/rust_walker.rb +130 -0
  36. data/lib/rubik/ast/treesitter/typescript_walker.rb +182 -0
  37. data/lib/rubik/ast/treesitter/zig_walker.rb +109 -0
  38. data/lib/rubik/ast/treesitter_extractor.rb +93 -0
  39. data/lib/rubik/ast/typescript_extractor.rb +283 -0
  40. data/lib/rubik/ast/zig_extractor.rb +116 -0
  41. data/lib/rubik/ast.rb +13 -0
  42. data/lib/rubik/benchmark/runner.rb +224 -0
  43. data/lib/rubik/benchmark/synth_repo.rb +59 -0
  44. data/lib/rubik/benchmark.rb +9 -0
  45. data/lib/rubik/buffer/diff.rb +43 -0
  46. data/lib/rubik/buffer/manager.rb +427 -0
  47. data/lib/rubik/buffer/path_validator.rb +60 -0
  48. data/lib/rubik/buffer/snapshot.rb +99 -0
  49. data/lib/rubik/buffer.rb +10 -0
  50. data/lib/rubik/coeffects.rb +129 -0
  51. data/lib/rubik/concurrency/boundary.rb +74 -0
  52. data/lib/rubik/concurrency/dns_pool.rb +68 -0
  53. data/lib/rubik/concurrency/errors.rb +17 -0
  54. data/lib/rubik/concurrency/event_loop.rb +441 -0
  55. data/lib/rubik/concurrency/join.rb +55 -0
  56. data/lib/rubik/concurrency/process_waiter.rb +55 -0
  57. data/lib/rubik/concurrency/scheduler.rb +84 -0
  58. data/lib/rubik/concurrency/timers.rb +109 -0
  59. data/lib/rubik/concurrency.rb +132 -0
  60. data/lib/rubik/config/deep_ops.rb +80 -0
  61. data/lib/rubik/config/defaults.rb +248 -0
  62. data/lib/rubik/config/model_catalog.rb +93 -0
  63. data/lib/rubik/config/store.rb +155 -0
  64. data/lib/rubik/config.rb +216 -0
  65. data/lib/rubik/context/git_info.rb +84 -0
  66. data/lib/rubik/context/git_lister.rb +37 -0
  67. data/lib/rubik/context/index.rb +134 -0
  68. data/lib/rubik/context/repo_context_reader.rb +121 -0
  69. data/lib/rubik/context/scanner.rb +439 -0
  70. data/lib/rubik/context.rb +11 -0
  71. data/lib/rubik/diagnostics/collector.rb +200 -0
  72. data/lib/rubik/diagnostics/entry.rb +60 -0
  73. data/lib/rubik/diagnostics/parser.rb +107 -0
  74. data/lib/rubik/diagnostics.rb +10 -0
  75. data/lib/rubik/event_bus.rb +56 -0
  76. data/lib/rubik/kernel/dispatcher.rb +171 -0
  77. data/lib/rubik/kernel/dmesg.rb +116 -0
  78. data/lib/rubik/kernel/init.rb +177 -0
  79. data/lib/rubik/kernel/record.rb +85 -0
  80. data/lib/rubik/kernel/unit.rb +160 -0
  81. data/lib/rubik/llm/anthropic.rb +202 -0
  82. data/lib/rubik/llm/chunk_assembler.rb +285 -0
  83. data/lib/rubik/llm/client.rb +526 -0
  84. data/lib/rubik/llm/formatter.rb +450 -0
  85. data/lib/rubik/llm/message_sanitizer.rb +87 -0
  86. data/lib/rubik/llm/ollama.rb +167 -0
  87. data/lib/rubik/llm/openai.rb +21 -0
  88. data/lib/rubik/llm/openai_compatible.rb +216 -0
  89. data/lib/rubik/llm/provider.rb +125 -0
  90. data/lib/rubik/llm/registry.rb +336 -0
  91. data/lib/rubik/llm/router.rb +100 -0
  92. data/lib/rubik/llm.rb +18 -0
  93. data/lib/rubik/log.rb +38 -0
  94. data/lib/rubik/lsp/client.rb +475 -0
  95. data/lib/rubik/lsp/manager.rb +366 -0
  96. data/lib/rubik/lsp.rb +9 -0
  97. data/lib/rubik/mcp/boot_lifecycle.rb +114 -0
  98. data/lib/rubik/mcp/client.rb +169 -0
  99. data/lib/rubik/mcp/errors.rb +11 -0
  100. data/lib/rubik/mcp/manager.rb +619 -0
  101. data/lib/rubik/mcp/proxy_tool.rb +104 -0
  102. data/lib/rubik/mcp/transport/stdio.rb +255 -0
  103. data/lib/rubik/mcp.rb +12 -0
  104. data/lib/rubik/orchestrator/activity_log.rb +75 -0
  105. data/lib/rubik/orchestrator/auto_apply.rb +166 -0
  106. data/lib/rubik/orchestrator/budget_manager.rb +97 -0
  107. data/lib/rubik/orchestrator/continuation_manager.rb +161 -0
  108. data/lib/rubik/orchestrator/finish_message_selector.rb +55 -0
  109. data/lib/rubik/orchestrator/goal_ledger/drift.rb +71 -0
  110. data/lib/rubik/orchestrator/goal_ledger/ledger.rb +399 -0
  111. data/lib/rubik/orchestrator/goal_ledger/observation_projection.rb +179 -0
  112. data/lib/rubik/orchestrator/goal_ledger/patch.rb +268 -0
  113. data/lib/rubik/orchestrator/goal_ledger/schema.rb +123 -0
  114. data/lib/rubik/orchestrator/goal_ledger/validator.rb +361 -0
  115. data/lib/rubik/orchestrator/goal_ledger/verifier.rb +129 -0
  116. data/lib/rubik/orchestrator/goal_ledger/verify_spec.rb +50 -0
  117. data/lib/rubik/orchestrator/goal_ledger.rb +65 -0
  118. data/lib/rubik/orchestrator/goal_state.rb +184 -0
  119. data/lib/rubik/orchestrator/goal_wait.rb +125 -0
  120. data/lib/rubik/orchestrator/loop.rb +388 -0
  121. data/lib/rubik/orchestrator/lsp_feedback.rb +55 -0
  122. data/lib/rubik/orchestrator/maestro_gate.rb +65 -0
  123. data/lib/rubik/orchestrator/provider_recovery.rb +90 -0
  124. data/lib/rubik/orchestrator/provider_resolver.rb +69 -0
  125. data/lib/rubik/orchestrator/shell_deps.rb +85 -0
  126. data/lib/rubik/orchestrator/step_executor.rb +328 -0
  127. data/lib/rubik/orchestrator/stream_manager.rb +118 -0
  128. data/lib/rubik/orchestrator/subagent/activation.rb +65 -0
  129. data/lib/rubik/orchestrator/subagent/budget.rb +108 -0
  130. data/lib/rubik/orchestrator/subagent/completion.rb +61 -0
  131. data/lib/rubik/orchestrator/subagent/concurrency_gate.rb +37 -0
  132. data/lib/rubik/orchestrator/subagent/conversation_id.rb +67 -0
  133. data/lib/rubik/orchestrator/subagent/dependency.rb +398 -0
  134. data/lib/rubik/orchestrator/subagent/instance.rb +384 -0
  135. data/lib/rubik/orchestrator/subagent/lifecycle.rb +118 -0
  136. data/lib/rubik/orchestrator/subagent/messaging.rb +61 -0
  137. data/lib/rubik/orchestrator/subagent/narration.rb +125 -0
  138. data/lib/rubik/orchestrator/subagent/notices.rb +209 -0
  139. data/lib/rubik/orchestrator/subagent/questioner.rb +80 -0
  140. data/lib/rubik/orchestrator/subagent/reaper.rb +98 -0
  141. data/lib/rubik/orchestrator/subagent/resilience.rb +70 -0
  142. data/lib/rubik/orchestrator/subagent/reuse.rb +127 -0
  143. data/lib/rubik/orchestrator/subagent/scoped_registry.rb +79 -0
  144. data/lib/rubik/orchestrator/subagent/step_runner.rb +240 -0
  145. data/lib/rubik/orchestrator/subagent/storage_id.rb +26 -0
  146. data/lib/rubik/orchestrator/subagent/supervisor.rb +327 -0
  147. data/lib/rubik/orchestrator/subagent/transcript_formatter.rb +141 -0
  148. data/lib/rubik/orchestrator/subagent/type_registry.rb +199 -0
  149. data/lib/rubik/orchestrator/subagent_notice_inbox.rb +80 -0
  150. data/lib/rubik/orchestrator/tool_call_id.rb +47 -0
  151. data/lib/rubik/orchestrator/tool_circuit_breaker.rb +91 -0
  152. data/lib/rubik/orchestrator/tool_history.rb +39 -0
  153. data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +102 -0
  154. data/lib/rubik/orchestrator/turn_executor/user_channel.rb +172 -0
  155. data/lib/rubik/orchestrator/turn_executor.rb +376 -0
  156. data/lib/rubik/orchestrator.rb +11 -0
  157. data/lib/rubik/platform/filesystem.rb +64 -0
  158. data/lib/rubik/platform/io_drain.rb +53 -0
  159. data/lib/rubik/platform/path.rb +210 -0
  160. data/lib/rubik/platform/process.rb +38 -0
  161. data/lib/rubik/platform/sanitize.rb +157 -0
  162. data/lib/rubik/platform.rb +113 -0
  163. data/lib/rubik/plugin/dsl.rb +51 -0
  164. data/lib/rubik/plugin/manager.rb +247 -0
  165. data/lib/rubik/plugin/storage.rb +195 -0
  166. data/lib/rubik/plugin.rb +10 -0
  167. data/lib/rubik/prompt/context_builder.rb +377 -0
  168. data/lib/rubik/prompt/context_compactor.rb +326 -0
  169. data/lib/rubik/prompt/conversation_rows.rb +74 -0
  170. data/lib/rubik/prompt/doctrine.rb +160 -0
  171. data/lib/rubik/prompt/emergency_compactor.rb +81 -0
  172. data/lib/rubik/prompt/services.rb +60 -0
  173. data/lib/rubik/prompt/token_estimator.rb +63 -0
  174. data/lib/rubik/prompt/tool_teachings.rb +78 -0
  175. data/lib/rubik/prompt.rb +13 -0
  176. data/lib/rubik/repair/loop.rb +95 -0
  177. data/lib/rubik/repair.rb +8 -0
  178. data/lib/rubik/repl/ast_warmup.rb +129 -0
  179. data/lib/rubik/repl/command_context.rb +214 -0
  180. data/lib/rubik/repl/command_parser.rb +48 -0
  181. data/lib/rubik/repl/commands/agent.rb +30 -0
  182. data/lib/rubik/repl/commands/agent_context.rb +289 -0
  183. data/lib/rubik/repl/commands/agent_execution.rb +342 -0
  184. data/lib/rubik/repl/commands/agent_models.rb +212 -0
  185. data/lib/rubik/repl/commands/agent_providers.rb +232 -0
  186. data/lib/rubik/repl/commands/base.rb +55 -0
  187. data/lib/rubik/repl/commands/buffer.rb +124 -0
  188. data/lib/rubik/repl/commands/dmesg.rb +87 -0
  189. data/lib/rubik/repl/commands/lsp.rb +235 -0
  190. data/lib/rubik/repl/commands/mcp.rb +194 -0
  191. data/lib/rubik/repl/commands/plugins.rb +208 -0
  192. data/lib/rubik/repl/commands/pruner.rb +71 -0
  193. data/lib/rubik/repl/commands/session.rb +183 -0
  194. data/lib/rubik/repl/commands/skills.rb +111 -0
  195. data/lib/rubik/repl/commands/storage.rb +85 -0
  196. data/lib/rubik/repl/commands/subagents.rb +195 -0
  197. data/lib/rubik/repl/commands/system.rb +85 -0
  198. data/lib/rubik/repl/commands/tasks.rb +120 -0
  199. data/lib/rubik/repl/commands/workspace.rb +443 -0
  200. data/lib/rubik/repl/commands.rb +44 -0
  201. data/lib/rubik/repl/composition.rb +32 -0
  202. data/lib/rubik/repl/config_wiring.rb +142 -0
  203. data/lib/rubik/repl/confirmations.rb +56 -0
  204. data/lib/rubik/repl/hooks.rb +47 -0
  205. data/lib/rubik/repl/input_execution.rb +323 -0
  206. data/lib/rubik/repl/input_loop.rb +213 -0
  207. data/lib/rubik/repl/interrupt_handler.rb +217 -0
  208. data/lib/rubik/repl/lifecycle.rb +410 -0
  209. data/lib/rubik/repl/paste_support.rb +88 -0
  210. data/lib/rubik/repl/pending_notice_queue.rb +60 -0
  211. data/lib/rubik/repl/presentation.rb +132 -0
  212. data/lib/rubik/repl/pruner/brief.rb +101 -0
  213. data/lib/rubik/repl/pruner/gate.rb +279 -0
  214. data/lib/rubik/repl/pruner/harness.rb +236 -0
  215. data/lib/rubik/repl/pruner/rules.rb +316 -0
  216. data/lib/rubik/repl/pruner/service.rb +82 -0
  217. data/lib/rubik/repl/pruner.rb +39 -0
  218. data/lib/rubik/repl/service_catalog.rb +374 -0
  219. data/lib/rubik/repl/setup_factories.rb +66 -0
  220. data/lib/rubik/repl/shell.rb +231 -0
  221. data/lib/rubik/repl/step_formatter.rb +324 -0
  222. data/lib/rubik/repl/tee_io.rb +105 -0
  223. data/lib/rubik/repl/terminal_guard.rb +157 -0
  224. data/lib/rubik/repl/turn_recorder.rb +234 -0
  225. data/lib/rubik/repl.rb +14 -0
  226. data/lib/rubik/runtime/ast_service.rb +78 -0
  227. data/lib/rubik/runtime/context.rb +335 -0
  228. data/lib/rubik/runtime/diagnostic_engine.rb +188 -0
  229. data/lib/rubik/runtime/discovery.rb +342 -0
  230. data/lib/rubik/runtime/fiber.rb +142 -0
  231. data/lib/rubik/runtime/fiber_ordering.rb +52 -0
  232. data/lib/rubik/runtime/fiber_retry_gate.rb +84 -0
  233. data/lib/rubik/runtime/index_service.rb +58 -0
  234. data/lib/rubik/runtime/loop_service.rb +73 -0
  235. data/lib/rubik/runtime/manager.rb +241 -0
  236. data/lib/rubik/runtime/overlay_storage.rb +481 -0
  237. data/lib/rubik/runtime/service_definition.rb +54 -0
  238. data/lib/rubik/runtime/service_registration.rb +33 -0
  239. data/lib/rubik/runtime/task_manager.rb +302 -0
  240. data/lib/rubik/runtime.rb +19 -0
  241. data/lib/rubik/sandbox/driver/base.rb +30 -0
  242. data/lib/rubik/sandbox/driver/bubblewrap.rb +111 -0
  243. data/lib/rubik/sandbox/driver/docker.rb +113 -0
  244. data/lib/rubik/sandbox/driver/podman.rb +98 -0
  245. data/lib/rubik/sandbox/driver/process.rb +190 -0
  246. data/lib/rubik/sandbox/driver.rb +91 -0
  247. data/lib/rubik/sandbox/runner.rb +218 -0
  248. data/lib/rubik/sandbox/shell_resolver.rb +178 -0
  249. data/lib/rubik/sandbox.rb +8 -0
  250. data/lib/rubik/security/blocked_guidance.rb +187 -0
  251. data/lib/rubik/security/classifier/classifier.rb +440 -0
  252. data/lib/rubik/security/classifier/commands/agent-browser.yaml +9 -0
  253. data/lib/rubik/security/classifier/commands/apt.yaml +25 -0
  254. data/lib/rubik/security/classifier/commands/awk.yaml +10 -0
  255. data/lib/rubik/security/classifier/commands/basename.yaml +7 -0
  256. data/lib/rubik/security/classifier/commands/bash-classify.yaml +8 -0
  257. data/lib/rubik/security/classifier/commands/bash.yaml +20 -0
  258. data/lib/rubik/security/classifier/commands/black.yaml +12 -0
  259. data/lib/rubik/security/classifier/commands/brew.yaml +31 -0
  260. data/lib/rubik/security/classifier/commands/builtin.yaml +8 -0
  261. data/lib/rubik/security/classifier/commands/cargo.yaml +114 -0
  262. data/lib/rubik/security/classifier/commands/cat.yaml +7 -0
  263. data/lib/rubik/security/classifier/commands/chgrp.yaml +10 -0
  264. data/lib/rubik/security/classifier/commands/chmod.yaml +10 -0
  265. data/lib/rubik/security/classifier/commands/chown.yaml +10 -0
  266. data/lib/rubik/security/classifier/commands/column.yaml +7 -0
  267. data/lib/rubik/security/classifier/commands/comm.yaml +7 -0
  268. data/lib/rubik/security/classifier/commands/command.yaml +12 -0
  269. data/lib/rubik/security/classifier/commands/cp.yaml +7 -0
  270. data/lib/rubik/security/classifier/commands/crystal.yaml +63 -0
  271. data/lib/rubik/security/classifier/commands/curl.yaml +30 -0
  272. data/lib/rubik/security/classifier/commands/cut.yaml +7 -0
  273. data/lib/rubik/security/classifier/commands/date.yaml +10 -0
  274. data/lib/rubik/security/classifier/commands/df.yaml +7 -0
  275. data/lib/rubik/security/classifier/commands/diff.yaml +7 -0
  276. data/lib/rubik/security/classifier/commands/dig.yaml +7 -0
  277. data/lib/rubik/security/classifier/commands/dirname.yaml +7 -0
  278. data/lib/rubik/security/classifier/commands/docker.yaml +177 -0
  279. data/lib/rubik/security/classifier/commands/du.yaml +7 -0
  280. data/lib/rubik/security/classifier/commands/echo.yaml +7 -0
  281. data/lib/rubik/security/classifier/commands/env.yaml +15 -0
  282. data/lib/rubik/security/classifier/commands/eslint.yaml +12 -0
  283. data/lib/rubik/security/classifier/commands/eval.yaml +8 -0
  284. data/lib/rubik/security/classifier/commands/exec.yaml +12 -0
  285. data/lib/rubik/security/classifier/commands/expand.yaml +7 -0
  286. data/lib/rubik/security/classifier/commands/false.yaml +7 -0
  287. data/lib/rubik/security/classifier/commands/file.yaml +7 -0
  288. data/lib/rubik/security/classifier/commands/find.yaml +25 -0
  289. data/lib/rubik/security/classifier/commands/fmt.yaml +7 -0
  290. data/lib/rubik/security/classifier/commands/fold.yaml +7 -0
  291. data/lib/rubik/security/classifier/commands/free.yaml +7 -0
  292. data/lib/rubik/security/classifier/commands/gawk.yaml +10 -0
  293. data/lib/rubik/security/classifier/commands/gcloud.yaml +979 -0
  294. data/lib/rubik/security/classifier/commands/gh.yaml +615 -0
  295. data/lib/rubik/security/classifier/commands/git.yaml +524 -0
  296. data/lib/rubik/security/classifier/commands/glab-discussion.yaml +21 -0
  297. data/lib/rubik/security/classifier/commands/glab.yaml +807 -0
  298. data/lib/rubik/security/classifier/commands/go.yaml +44 -0
  299. data/lib/rubik/security/classifier/commands/gog.yaml +983 -0
  300. data/lib/rubik/security/classifier/commands/gradle.yaml +92 -0
  301. data/lib/rubik/security/classifier/commands/gradlew.yaml +92 -0
  302. data/lib/rubik/security/classifier/commands/grep.yaml +7 -0
  303. data/lib/rubik/security/classifier/commands/groups.yaml +7 -0
  304. data/lib/rubik/security/classifier/commands/gunzip.yaml +8 -0
  305. data/lib/rubik/security/classifier/commands/gws.yaml +211 -0
  306. data/lib/rubik/security/classifier/commands/gzip.yaml +7 -0
  307. data/lib/rubik/security/classifier/commands/hadolint.yaml +9 -0
  308. data/lib/rubik/security/classifier/commands/halt.yaml +8 -0
  309. data/lib/rubik/security/classifier/commands/head.yaml +7 -0
  310. data/lib/rubik/security/classifier/commands/helm.yaml +129 -0
  311. data/lib/rubik/security/classifier/commands/hostname.yaml +7 -0
  312. data/lib/rubik/security/classifier/commands/id.yaml +7 -0
  313. data/lib/rubik/security/classifier/commands/install.yaml +7 -0
  314. data/lib/rubik/security/classifier/commands/isort.yaml +14 -0
  315. data/lib/rubik/security/classifier/commands/java.yaml +35 -0
  316. data/lib/rubik/security/classifier/commands/javac.yaml +39 -0
  317. data/lib/rubik/security/classifier/commands/join.yaml +7 -0
  318. data/lib/rubik/security/classifier/commands/journalctl.yaml +14 -0
  319. data/lib/rubik/security/classifier/commands/jq.yaml +7 -0
  320. data/lib/rubik/security/classifier/commands/kill.yaml +7 -0
  321. data/lib/rubik/security/classifier/commands/killall.yaml +7 -0
  322. data/lib/rubik/security/classifier/commands/kubectl.yaml +211 -0
  323. data/lib/rubik/security/classifier/commands/less.yaml +7 -0
  324. data/lib/rubik/security/classifier/commands/ln.yaml +7 -0
  325. data/lib/rubik/security/classifier/commands/ls.yaml +7 -0
  326. data/lib/rubik/security/classifier/commands/make.yaml +21 -0
  327. data/lib/rubik/security/classifier/commands/md5sum.yaml +7 -0
  328. data/lib/rubik/security/classifier/commands/mise.yaml +579 -0
  329. data/lib/rubik/security/classifier/commands/mkdir.yaml +8 -0
  330. data/lib/rubik/security/classifier/commands/more.yaml +7 -0
  331. data/lib/rubik/security/classifier/commands/mv.yaml +7 -0
  332. data/lib/rubik/security/classifier/commands/mvn.yaml +83 -0
  333. data/lib/rubik/security/classifier/commands/mvnw.yaml +83 -0
  334. data/lib/rubik/security/classifier/commands/mypy.yaml +9 -0
  335. data/lib/rubik/security/classifier/commands/netstat.yaml +7 -0
  336. data/lib/rubik/security/classifier/commands/nice.yaml +11 -0
  337. data/lib/rubik/security/classifier/commands/nl.yaml +7 -0
  338. data/lib/rubik/security/classifier/commands/node.yaml +20 -0
  339. data/lib/rubik/security/classifier/commands/nohup.yaml +9 -0
  340. data/lib/rubik/security/classifier/commands/npm.yaml +71 -0
  341. data/lib/rubik/security/classifier/commands/npx.yaml +42 -0
  342. data/lib/rubik/security/classifier/commands/nslookup.yaml +7 -0
  343. data/lib/rubik/security/classifier/commands/paste.yaml +7 -0
  344. data/lib/rubik/security/classifier/commands/ping.yaml +7 -0
  345. data/lib/rubik/security/classifier/commands/pip.yaml +33 -0
  346. data/lib/rubik/security/classifier/commands/pip3.yaml +33 -0
  347. data/lib/rubik/security/classifier/commands/pipx.yaml +33 -0
  348. data/lib/rubik/security/classifier/commands/pkill.yaml +7 -0
  349. data/lib/rubik/security/classifier/commands/pnpm.yaml +346 -0
  350. data/lib/rubik/security/classifier/commands/poetry.yaml +97 -0
  351. data/lib/rubik/security/classifier/commands/poweroff.yaml +8 -0
  352. data/lib/rubik/security/classifier/commands/prettier.yaml +13 -0
  353. data/lib/rubik/security/classifier/commands/printenv.yaml +7 -0
  354. data/lib/rubik/security/classifier/commands/printf.yaml +7 -0
  355. data/lib/rubik/security/classifier/commands/ps.yaml +7 -0
  356. data/lib/rubik/security/classifier/commands/pup.yaml +882 -0
  357. data/lib/rubik/security/classifier/commands/pwd.yaml +7 -0
  358. data/lib/rubik/security/classifier/commands/pytest.yaml +7 -0
  359. data/lib/rubik/security/classifier/commands/python.yaml +9 -0
  360. data/lib/rubik/security/classifier/commands/python3.yaml +9 -0
  361. data/lib/rubik/security/classifier/commands/read.yaml +7 -0
  362. data/lib/rubik/security/classifier/commands/readlink.yaml +7 -0
  363. data/lib/rubik/security/classifier/commands/realpath.yaml +7 -0
  364. data/lib/rubik/security/classifier/commands/reboot.yaml +8 -0
  365. data/lib/rubik/security/classifier/commands/rev.yaml +7 -0
  366. data/lib/rubik/security/classifier/commands/rg.yaml +7 -0
  367. data/lib/rubik/security/classifier/commands/rm.yaml +8 -0
  368. data/lib/rubik/security/classifier/commands/rsync.yaml +12 -0
  369. data/lib/rubik/security/classifier/commands/rtk.yaml +140 -0
  370. data/lib/rubik/security/classifier/commands/ruff.yaml +29 -0
  371. data/lib/rubik/security/classifier/commands/rustfmt.yaml +14 -0
  372. data/lib/rubik/security/classifier/commands/scp.yaml +7 -0
  373. data/lib/rubik/security/classifier/commands/sed.yaml +14 -0
  374. data/lib/rubik/security/classifier/commands/seq.yaml +7 -0
  375. data/lib/rubik/security/classifier/commands/sh.yaml +14 -0
  376. data/lib/rubik/security/classifier/commands/sha1sum.yaml +7 -0
  377. data/lib/rubik/security/classifier/commands/sha256sum.yaml +7 -0
  378. data/lib/rubik/security/classifier/commands/shellcheck.yaml +10 -0
  379. data/lib/rubik/security/classifier/commands/shutdown.yaml +8 -0
  380. data/lib/rubik/security/classifier/commands/slack.yaml +113 -0
  381. data/lib/rubik/security/classifier/commands/sleep.yaml +8 -0
  382. data/lib/rubik/security/classifier/commands/snip.yaml +79 -0
  383. data/lib/rubik/security/classifier/commands/sort.yaml +10 -0
  384. data/lib/rubik/security/classifier/commands/ss.yaml +7 -0
  385. data/lib/rubik/security/classifier/commands/ssh.yaml +41 -0
  386. data/lib/rubik/security/classifier/commands/stat.yaml +7 -0
  387. data/lib/rubik/security/classifier/commands/stdbuf.yaml +15 -0
  388. data/lib/rubik/security/classifier/commands/strace.yaml +20 -0
  389. data/lib/rubik/security/classifier/commands/strings.yaml +11 -0
  390. data/lib/rubik/security/classifier/commands/sudo.yaml +24 -0
  391. data/lib/rubik/security/classifier/commands/systemctl.yaml +58 -0
  392. data/lib/rubik/security/classifier/commands/systemd-run.yaml +30 -0
  393. data/lib/rubik/security/classifier/commands/tac.yaml +7 -0
  394. data/lib/rubik/security/classifier/commands/tail.yaml +7 -0
  395. data/lib/rubik/security/classifier/commands/tar.yaml +25 -0
  396. data/lib/rubik/security/classifier/commands/tee.yaml +8 -0
  397. data/lib/rubik/security/classifier/commands/terraform.yaml +70 -0
  398. data/lib/rubik/security/classifier/commands/test.yaml +7 -0
  399. data/lib/rubik/security/classifier/commands/time.yaml +8 -0
  400. data/lib/rubik/security/classifier/commands/timeout.yaml +18 -0
  401. data/lib/rubik/security/classifier/commands/top.yaml +7 -0
  402. data/lib/rubik/security/classifier/commands/touch.yaml +8 -0
  403. data/lib/rubik/security/classifier/commands/tr.yaml +7 -0
  404. data/lib/rubik/security/classifier/commands/traceroute.yaml +7 -0
  405. data/lib/rubik/security/classifier/commands/true.yaml +7 -0
  406. data/lib/rubik/security/classifier/commands/tsc.yaml +16 -0
  407. data/lib/rubik/security/classifier/commands/type.yaml +7 -0
  408. data/lib/rubik/security/classifier/commands/uname.yaml +7 -0
  409. data/lib/rubik/security/classifier/commands/unexpand.yaml +7 -0
  410. data/lib/rubik/security/classifier/commands/uniq.yaml +7 -0
  411. data/lib/rubik/security/classifier/commands/unzip.yaml +10 -0
  412. data/lib/rubik/security/classifier/commands/uptime.yaml +7 -0
  413. data/lib/rubik/security/classifier/commands/uv.yaml +156 -0
  414. data/lib/rubik/security/classifier/commands/wait.yaml +11 -0
  415. data/lib/rubik/security/classifier/commands/wc.yaml +7 -0
  416. data/lib/rubik/security/classifier/commands/wget.yaml +7 -0
  417. data/lib/rubik/security/classifier/commands/whereis.yaml +7 -0
  418. data/lib/rubik/security/classifier/commands/which.yaml +7 -0
  419. data/lib/rubik/security/classifier/commands/whoami.yaml +7 -0
  420. data/lib/rubik/security/classifier/commands/xargs.yaml +26 -0
  421. data/lib/rubik/security/classifier/commands/xmllint.yaml +7 -0
  422. data/lib/rubik/security/classifier/commands/yarn.yaml +72 -0
  423. data/lib/rubik/security/classifier/commands/yes.yaml +7 -0
  424. data/lib/rubik/security/classifier/commands/yq.yaml +10 -0
  425. data/lib/rubik/security/classifier/commands/zip.yaml +8 -0
  426. data/lib/rubik/security/classifier/commands/zsh.yaml +14 -0
  427. data/lib/rubik/security/classifier/database.rb +216 -0
  428. data/lib/rubik/security/classifier/matcher/delegation.rb +190 -0
  429. data/lib/rubik/security/classifier/matcher/options.rb +245 -0
  430. data/lib/rubik/security/classifier/matcher/subcommands.rb +103 -0
  431. data/lib/rubik/security/classifier/matcher.rb +249 -0
  432. data/lib/rubik/security/classifier/models.rb +170 -0
  433. data/lib/rubik/security/classifier/parser.rb +23 -0
  434. data/lib/rubik/security/classifier/parsers/bash.rb +280 -0
  435. data/lib/rubik/security/classifier/parsers/bash_extractors.rb +221 -0
  436. data/lib/rubik/security/classifier/parsers/powershell.rb +185 -0
  437. data/lib/rubik/security/classifier/pwsh_rules.rb +224 -0
  438. data/lib/rubik/security/classifier.rb +25 -0
  439. data/lib/rubik/security/errors.rb +39 -0
  440. data/lib/rubik/security/guard.rb +954 -0
  441. data/lib/rubik/security/redactor.rb +89 -0
  442. data/lib/rubik/security.rb +11 -0
  443. data/lib/rubik/session/manager.rb +331 -0
  444. data/lib/rubik/session/recorder.rb +149 -0
  445. data/lib/rubik/session/replayer.rb +65 -0
  446. data/lib/rubik/session.rb +10 -0
  447. data/lib/rubik/skills/frontmatter.rb +41 -0
  448. data/lib/rubik/skills/manager.rb +223 -0
  449. data/lib/rubik/skills/registry.rb +64 -0
  450. data/lib/rubik/skills/skill.rb +33 -0
  451. data/lib/rubik/skills/storage.rb +77 -0
  452. data/lib/rubik/skills/validator.rb +116 -0
  453. data/lib/rubik/skills.rb +20 -0
  454. data/lib/rubik/storage/folding.rb +23 -0
  455. data/lib/rubik/storage/layout.rb +145 -0
  456. data/lib/rubik/storage/render.rb +97 -0
  457. data/lib/rubik/storage/service.rb +42 -0
  458. data/lib/rubik/storage/store.rb +514 -0
  459. data/lib/rubik/storage/transcript_indexer.rb +116 -0
  460. data/lib/rubik/storage/uri_reader.rb +168 -0
  461. data/lib/rubik/storage/vakanuvis/annal_writer.rb +92 -0
  462. data/lib/rubik/storage/vakanuvis/distill_gate.rb +323 -0
  463. data/lib/rubik/storage/vakanuvis/gate.rb +250 -0
  464. data/lib/rubik/storage/vakanuvis/harness.rb +421 -0
  465. data/lib/rubik/storage/vakanuvis/memory_steward.rb +163 -0
  466. data/lib/rubik/storage/vakanuvis/service.rb +69 -0
  467. data/lib/rubik/storage/vakanuvis/vocabulary.rb +99 -0
  468. data/lib/rubik/storage/vakanuvis/wiki_skeleton.rb +67 -0
  469. data/lib/rubik/storage/vakanuvis.rb +77 -0
  470. data/lib/rubik/storage.rb +29 -0
  471. data/lib/rubik/tools/base.rb +107 -0
  472. data/lib/rubik/tools/builtins/dmesg.rb +74 -0
  473. data/lib/rubik/tools/builtins/exec.rb +293 -0
  474. data/lib/rubik/tools/builtins/files.rb +354 -0
  475. data/lib/rubik/tools/builtins/goal_ledger.rb +89 -0
  476. data/lib/rubik/tools/builtins/grep.rb +281 -0
  477. data/lib/rubik/tools/builtins/interactive.rb +175 -0
  478. data/lib/rubik/tools/builtins/lifecycle.rb +359 -0
  479. data/lib/rubik/tools/builtins/lsp.rb +283 -0
  480. data/lib/rubik/tools/builtins/pruner.rb +88 -0
  481. data/lib/rubik/tools/builtins/runtime.rb +120 -0
  482. data/lib/rubik/tools/builtins/send_message.rb +109 -0
  483. data/lib/rubik/tools/builtins/show_file.rb +84 -0
  484. data/lib/rubik/tools/builtins/skill.rb +72 -0
  485. data/lib/rubik/tools/builtins/storage.rb +232 -0
  486. data/lib/rubik/tools/builtins/subagent.rb +356 -0
  487. data/lib/rubik/tools/builtins/subagent_lifecycle.rb +72 -0
  488. data/lib/rubik/tools/builtins/symbols.rb +209 -0
  489. data/lib/rubik/tools/builtins/synthesis.rb +314 -0
  490. data/lib/rubik/tools/builtins/task.rb +168 -0
  491. data/lib/rubik/tools/builtins/unsynthesize.rb +111 -0
  492. data/lib/rubik/tools/builtins/vakanuvis.rb +161 -0
  493. data/lib/rubik/tools/builtins.rb +30 -0
  494. data/lib/rubik/tools/catalog.rb +70 -0
  495. data/lib/rubik/tools/declaration.rb +90 -0
  496. data/lib/rubik/tools/diagnostic_reporter.rb +283 -0
  497. data/lib/rubik/tools/fuzzy_matcher.rb +172 -0
  498. data/lib/rubik/tools/native_search.rb +180 -0
  499. data/lib/rubik/tools/output.rb +54 -0
  500. data/lib/rubik/tools/preflight_runner.rb +209 -0
  501. data/lib/rubik/tools/registry.rb +247 -0
  502. data/lib/rubik/tools/result.rb +71 -0
  503. data/lib/rubik/tools/services.rb +96 -0
  504. data/lib/rubik/tools/truncator.rb +78 -0
  505. data/lib/rubik/tools/uri_resolver.rb +195 -0
  506. data/lib/rubik/tools.rb +14 -0
  507. data/lib/rubik/ui/approvals.rb +52 -0
  508. data/lib/rubik/ui/banner.rb +43 -0
  509. data/lib/rubik/ui/capability.rb +122 -0
  510. data/lib/rubik/ui/dialog.rb +106 -0
  511. data/lib/rubik/ui/diff_viewer.rb +78 -0
  512. data/lib/rubik/ui/frame.rb +95 -0
  513. data/lib/rubik/ui/markdown_stream.rb +107 -0
  514. data/lib/rubik/ui/panel.rb +98 -0
  515. data/lib/rubik/ui/renderer.rb +206 -0
  516. data/lib/rubik/ui/spinner.rb +120 -0
  517. data/lib/rubik/ui/statusline.rb +134 -0
  518. data/lib/rubik/ui/theme.rb +118 -0
  519. data/lib/rubik/ui.rb +57 -0
  520. data/lib/rubik/uris.rb +101 -0
  521. data/lib/rubik/version.rb +5 -0
  522. data/lib/rubik.rb +65 -0
  523. metadata +770 -0
@@ -0,0 +1,366 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+ require_relative "client"
5
+ require_relative "../runtime/discovery"
6
+ require_relative "../runtime/diagnostic_engine"
7
+
8
+ module Rubik
9
+ module LSP
10
+ class Manager
11
+ EXTENSION_MAP = {
12
+ ".go" => "go",
13
+ ".rs" => "rust",
14
+ ".zig" => "zig",
15
+ ".c" => "c",
16
+ ".h" => "c",
17
+ ".cpp" => "c",
18
+ ".hpp" => "c",
19
+ ".py" => "python",
20
+ ".rb" => "ruby",
21
+ ".ts" => "typescript",
22
+ ".tsx" => "typescript",
23
+ ".js" => "javascript",
24
+ ".jsx" => "javascript",
25
+ ".sh" => "bash",
26
+ ".bash" => "bash",
27
+ ".cr" => "crystal",
28
+ ".gleam" => "gleam",
29
+ ".vue" => "vue"
30
+ }.freeze
31
+
32
+ MAX_CONSECUTIVE_CRASHES = 3
33
+ CIRCUIT_COOLDOWN_SECONDS = 300
34
+
35
+ attr_reader :config, :root_dir, :clients, :crash_stats
36
+ attr_accessor :runtime_manager, :context
37
+
38
+ def initialize(config:, root_dir: Dir.pwd, runtime_manager: nil, context: nil)
39
+ @config = config
40
+ @root_dir = File.expand_path(root_dir)
41
+ @runtime_manager = runtime_manager
42
+ @context = context || runtime_manager&.context
43
+ @clients = {}
44
+ @crash_stats = {}
45
+ @monitor = Monitor.new
46
+ end
47
+
48
+ def enabled?
49
+ @config.fetch("lsp.enabled", false) == true
50
+ end
51
+
52
+ def auto_diagnostics?
53
+ @config.fetch("lsp.auto_diagnostics", true) == true
54
+ end
55
+
56
+ def detect_language(file_path)
57
+ ext = File.extname(file_path).downcase
58
+ EXTENSION_MAP[ext]
59
+ end
60
+
61
+ def circuit_open?(lang)
62
+ @monitor.synchronize do
63
+ stat = @crash_stats[lang.to_s.downcase]
64
+ return false unless stat && stat[:circuit_open]
65
+
66
+ if stat[:last_crash_at] && (Time.now - stat[:last_crash_at] > CIRCUIT_COOLDOWN_SECONDS)
67
+ stat[:circuit_open] = false
68
+ stat[:count] = 0
69
+ false
70
+ else
71
+ true
72
+ end
73
+ end
74
+ end
75
+
76
+ def record_crash(lang)
77
+ @monitor.synchronize do
78
+ lang_str = lang.to_s.downcase
79
+ stat = (@crash_stats[lang_str] ||= { count: 0, last_crash_at: nil, circuit_open: false })
80
+ stat[:count] += 1
81
+ stat[:last_crash_at] = Time.now
82
+ stat[:circuit_open] = true if stat[:count] >= MAX_CONSECUTIVE_CRASHES
83
+ end
84
+ end
85
+
86
+ def reset_circuit!(lang)
87
+ @monitor.synchronize do
88
+ lang_str = lang.to_s.downcase
89
+ @crash_stats.delete(lang_str)
90
+ end
91
+ end
92
+
93
+ def ensure_healthy!(language_or_client)
94
+ return nil unless enabled?
95
+
96
+ client = language_or_client.is_a?(Client) ? language_or_client : client_for(language_or_client)
97
+ return nil unless client
98
+
99
+ if client.running && client.respond_to?(:alive?) && !client.alive?
100
+ lang = client.language
101
+ record_crash(lang)
102
+ @monitor.synchronize do
103
+ begin
104
+ client.stop
105
+ rescue
106
+ nil
107
+ end
108
+ @clients.delete(lang)
109
+ end
110
+ start_client(lang) unless circuit_open?(lang)
111
+ client = @clients[lang]
112
+ elsif !client.running && @config.fetch("lsp.auto_start", true) && client.available? && !circuit_open?(client.language)
113
+ start_client(client.language)
114
+ client = @clients[client.language]
115
+ end
116
+
117
+ client
118
+ end
119
+
120
+ def client_for(language_or_file)
121
+ lang = if language_or_file.to_s.include?(".")
122
+ detect_language(language_or_file)
123
+ else
124
+ language_or_file.to_s.downcase
125
+ end
126
+
127
+ return nil unless lang
128
+
129
+ @monitor.synchronize do
130
+ if @clients.key?(lang)
131
+ client = @clients[lang]
132
+ if client.running && client.respond_to?(:alive?) && !client.alive?
133
+ record_crash(lang)
134
+ begin
135
+ client.stop
136
+ rescue
137
+ nil
138
+ end
139
+ @clients.delete(lang)
140
+ nil # Will trigger recreate / start below
141
+ else
142
+ client
143
+ end
144
+ elsif enabled?
145
+ configured_cmd = @config.fetch("lsp.servers.#{lang}") || Client::SERVERS[lang]
146
+ client = Client.new(lang, root_dir: @root_dir, server_cmd: configured_cmd)
147
+ unless client.available?
148
+ probed_cmd = Runtime::Discovery.probe_lsp(lang, root_dir: @root_dir, custom_cmd: configured_cmd)
149
+ client = Client.new(lang, root_dir: @root_dir, server_cmd: probed_cmd) if probed_cmd
150
+ end
151
+
152
+ if client.available? && @config.fetch("lsp.auto_start", true) && !circuit_open?(lang)
153
+ if client.start
154
+ reset_circuit!(lang)
155
+ effective_ctx = @context || @runtime_manager&.context
156
+ if effective_ctx
157
+ effective_ctx.provide("lsp:#{lang}", client)
158
+ effective_ctx.register_disposable { client.stop if client.running }
159
+ end
160
+ else
161
+ record_crash(lang)
162
+ begin
163
+ client.stop
164
+ rescue StandardError
165
+ nil
166
+ end
167
+ end
168
+ end
169
+ @clients[lang] = client
170
+ end
171
+ end
172
+ end
173
+
174
+ def start_client(lang)
175
+ lang_str = lang.to_s.downcase
176
+ @monitor.synchronize do
177
+ configured_cmd = @config.fetch("lsp.servers.#{lang_str}") || Client::SERVERS[lang_str]
178
+ client = @clients[lang_str] || Client.new(lang_str, root_dir: @root_dir, server_cmd: configured_cmd)
179
+
180
+ # 1. Fallback probing if configured command binary is not available
181
+ unless client.available?
182
+ probed_cmd = Runtime::Discovery.probe_lsp(lang_str, root_dir: @root_dir, custom_cmd: configured_cmd)
183
+ client = Client.new(lang_str, root_dir: @root_dir, server_cmd: probed_cmd) if probed_cmd && probed_cmd != client.server_cmd
184
+ end
185
+
186
+ unless client.available?
187
+ failure = Runtime::DiagnosticEngine.diagnose(
188
+ cmd: client.server_cmd,
189
+ exit_code: 127,
190
+ stderr: "Server binary '#{client.server_cmd}' not found in PATH"
191
+ )
192
+ return {
193
+ ok: false,
194
+ error: "Server binary '#{client.server_cmd}' not found",
195
+ diagnostic: failure,
196
+ suggestion: failure.suggestion
197
+ }
198
+ end
199
+
200
+ if client.running
201
+ { ok: true, message: "LSP server for '#{lang_str}' is already running (PID: #{client.instance_variable_get(:@wait_thr)&.pid})" }
202
+ else
203
+ res = client.start
204
+ @clients[lang_str] = client
205
+ if res
206
+ reset_circuit!(lang_str)
207
+ effective_ctx = @context || @runtime_manager&.context
208
+ if effective_ctx
209
+ effective_ctx.provide("lsp:#{lang_str}", client)
210
+ effective_ctx.register_disposable { client.stop if client.running }
211
+ end
212
+ { ok: true, message: "Started LSP server for '#{lang_str}' (#{client.server_cmd})" }
213
+ else
214
+ record_crash(lang_str)
215
+ begin
216
+ client.stop
217
+ rescue StandardError
218
+ nil
219
+ end
220
+ failure = Runtime::DiagnosticEngine.diagnose(
221
+ cmd: client.server_cmd,
222
+ exit_code: 1,
223
+ stderr: "Failed to initialize LSP server handshake for '#{lang_str}'"
224
+ )
225
+ {
226
+ ok: false,
227
+ error: "Failed to initialize LSP server for '#{lang_str}'",
228
+ diagnostic: failure,
229
+ suggestion: failure.suggestion
230
+ }
231
+ end
232
+ end
233
+ end
234
+ end
235
+
236
+ def stop_client(lang)
237
+ lang_str = lang.to_s.downcase
238
+ @monitor.synchronize do
239
+ client = @clients[lang_str]
240
+ return { ok: false, error: "No active client for '#{lang_str}'" } unless client
241
+
242
+ client.stop
243
+ @clients.delete(lang_str)
244
+ { ok: true, message: "Stopped LSP server for '#{lang_str}'" }
245
+ end
246
+ end
247
+
248
+ def stop_all
249
+ @monitor.synchronize do
250
+ @clients.each_value(&:stop)
251
+ @clients.clear
252
+ end
253
+ end
254
+
255
+ def sync_buffer_file(file_path, content)
256
+ return false unless enabled?
257
+
258
+ client = client_for(file_path)
259
+ return false unless client&.running
260
+
261
+ # Try did_change, or did_open if not yet opened
262
+ if client.document_open?(file_path)
263
+ client.did_change(file_path, content)
264
+ else
265
+ client.did_open(file_path, content)
266
+ end
267
+ end
268
+
269
+ def sync_buffer_file_if_needed(file_path, buffer_manager)
270
+ return false unless enabled? && buffer_manager
271
+
272
+ if file_path.nil? || file_path.to_s.strip.empty?
273
+ if buffer_manager.respond_to?(:dirty_files)
274
+ buffer_manager.dirty_files.each_key do |f|
275
+ content = buffer_manager.read(f)
276
+ sync_buffer_file(f, content) if content
277
+ end
278
+ end
279
+ return true
280
+ end
281
+
282
+ path_str = file_path.to_s.strip
283
+ needs_sync = begin
284
+ if buffer_manager.respond_to?(:dirty?)
285
+ buffer_manager.method(:dirty?).arity.zero? ? buffer_manager.dirty? : buffer_manager.dirty?(path_str)
286
+ else
287
+ false
288
+ end
289
+ rescue ArgumentError
290
+ buffer_manager.dirty?
291
+ end
292
+
293
+ return unless needs_sync
294
+
295
+ content = buffer_manager.read(path_str)
296
+ sync_buffer_file(path_str, content) if content
297
+ end
298
+
299
+ def diagnostics_for(file_path)
300
+ client = client_for(file_path)
301
+ return [] unless client&.running
302
+
303
+ client.diagnostics_for(file_path)
304
+ end
305
+
306
+ def all_diagnostics
307
+ result = {}
308
+ @monitor.synchronize do
309
+ @clients.each_value do |client|
310
+ next unless client.running
311
+
312
+ result.merge!(client.all_diagnostics)
313
+ end
314
+ end
315
+ result
316
+ end
317
+
318
+ def hover(file_path, line, character)
319
+ client = client_for(file_path)
320
+ return { ok: false, error: "LSP not active for '#{file_path}'" } unless client&.running
321
+
322
+ client.hover(file_path, line, character)
323
+ end
324
+
325
+ def definition(file_path, line, character)
326
+ client = client_for(file_path)
327
+ return { ok: false, error: "LSP not active for '#{file_path}'" } unless client&.running
328
+
329
+ client.definition(file_path, line, character)
330
+ end
331
+
332
+ def references(file_path, line, character)
333
+ client = client_for(file_path)
334
+ return { ok: false, error: "LSP not active for '#{file_path}'" } unless client&.running
335
+
336
+ client.references(file_path, line, character)
337
+ end
338
+
339
+ def status_summary
340
+ client_statuses = {}
341
+ @monitor.synchronize do
342
+ Client::SERVERS.each do |lang, def_cmd|
343
+ cmd = @config.fetch("lsp.servers.#{lang}") || def_cmd
344
+ active_c = @clients[lang]
345
+ avail = active_c ? active_c.available? : Runtime::Discovery.binary_available?(cmd)
346
+ probed = avail ? cmd : Runtime::Discovery.probe_lsp(lang, root_dir: @root_dir)
347
+
348
+ client_statuses[lang] = {
349
+ server_cmd: cmd,
350
+ probed_cmd: probed,
351
+ available: avail || !probed.nil?,
352
+ running: active_c&.running || false,
353
+ diagnostics_count: active_c ? active_c.all_diagnostics.values.flatten.size : 0
354
+ }
355
+ end
356
+ end
357
+
358
+ {
359
+ enabled: enabled?,
360
+ auto_diagnostics: auto_diagnostics?,
361
+ servers: client_statuses
362
+ }
363
+ end
364
+ end
365
+ end
366
+ end
data/lib/rubik/lsp.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lsp/client"
4
+ require_relative "lsp/manager"
5
+
6
+ module Rubik
7
+ module LSP
8
+ end
9
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module MCP
5
+ # Boot state-machine vocabulary for Rubik::MCP::Manager (included there).
6
+ # Storage stays on the host: @statuses, @boot_marks, @clients and @monitor
7
+ # are Manager ivars these paths touch on `self`, always under that
8
+ # monitor (Ruby Monitor is re-entrant, so callers already holding it
9
+ # compose freely). Thread-safety fence (mcp-async-boot G5): none of these
10
+ # paths block on threads or I/O — the boot-thread join stays outside the
11
+ # monitor.
12
+ #
13
+ # Precedent: orchestrator/subagent/lifecycle.rb — a pure state machine
14
+ # deserves its own page, not headroom to grow back.
15
+ module BootLifecycle
16
+ # Raised by transition_server! on an edge outside TRANSITIONS. The
17
+ # table is derived from the actual write sites, so this firing means a
18
+ # new writer skipped the guarded seam (fail-loud tripwire).
19
+ class StateError < StandardError; end
20
+
21
+ STATES = [:disabled, :connecting, :active, :failed].freeze
22
+
23
+ # Every legal from->to edge, derived from the write sites:
24
+ # disabled <- add_server (auto_start false / disabled config),
25
+ # disable_server (user teardown wins from ANY state),
26
+ # re-add over a failed/connecting slot
27
+ # connecting <- boot phase-0 claim over :disabled, :failed (re-boot),
28
+ # or :active-with-dead-client (already_active misses)
29
+ # active <- apply success (boot path from :connecting; enable
30
+ # path from :disabled/:failed), user enable during a
31
+ # boot (:connecting)
32
+ # failed <- apply rollback, enable connect failure (from
33
+ # :disabled, :failed retry, :active dead-client,
34
+ # :connecting), boot's epoch-guarded connect-failure
35
+ # flip and D6 ensure sweep
36
+ # The table is fully connected — that IS the invariant: user decisions
37
+ # are legal from any state; boot writes are legal only from an
38
+ # epoch-owned :connecting. Server absence (remove_server) is not a
39
+ # state, it is record deletion (clear_server_record!).
40
+ TRANSITIONS = {
41
+ disabled: [:connecting, :active, :failed],
42
+ connecting: [:active, :failed, :disabled],
43
+ active: [:disabled, :connecting, :failed],
44
+ failed: [:connecting, :active, :disabled]
45
+ }.transform_values(&:freeze).freeze
46
+
47
+ # The only mutation path for @statuses. epoch: nil is the USER actor —
48
+ # the decision wins from any state (D5). Passing epoch: makes it the
49
+ # BOOT actor: it owns the slot only while the slot is still :connecting
50
+ # under ITS epoch; anything else (user disable/enable/remove, a newer
51
+ # boot's fresh mark) wins, and the call is a silent no-op returning
52
+ # nil. Idempotent on same-state (returns the current state); raises
53
+ # StateError on an edge outside TRANSITIONS. Epoch gate and flip are
54
+ # applied in the SAME monitor hold — never split this into a predicate
55
+ # read followed by an unguarded write.
56
+ def transition_server!(name, to, epoch: nil)
57
+ name = name.to_s
58
+ @monitor.synchronize do
59
+ current = @statuses[name] || :disabled
60
+ target = to.to_sym
61
+ return current if current == target
62
+
63
+ return nil if epoch && (current != :connecting || !@boot_marks[name].equal?(epoch))
64
+
65
+ raise StateError, "illegal MCP boot transition '#{name}': #{current} -> #{target}" unless TRANSITIONS.fetch(current, []).include?(target)
66
+
67
+ @statuses[name] = target
68
+ # Boot-mark debt: a mark only matters while :connecting. Leaving
69
+ # the state retires it, so marks never outlive their epoch
70
+ # (zero-restart — claim/apply and claim/remove cycles keep
71
+ # @boot_marks flat instead of growing forever).
72
+ @boot_marks.delete(name) unless target == :connecting
73
+ target
74
+ end
75
+ end
76
+
77
+ # Phase-0 claim: returns true when a live :active server already owns
78
+ # the slot (boot must not touch it); otherwise flips the slot to
79
+ # :connecting and tags it with this boot's epoch — atomically under one
80
+ # monitor hold (D5). A re-claim over a stale :connecting mark (wedged
81
+ # older boot) takes the slot over with the new epoch.
82
+ def claim_boot_slot!(name, epoch)
83
+ name = name.to_s
84
+ @monitor.synchronize do
85
+ already_active = @statuses[name] == :active && @clients[name]&.running?
86
+ unless already_active
87
+ transition_server!(name, :connecting) # re-entrant: same hold
88
+ @boot_marks[name] = epoch
89
+ end
90
+ already_active
91
+ end
92
+ end
93
+
94
+ # remove_server's record deletion: status and (possibly dead) boot mark
95
+ # retire together — deletion must not leave a mark for a slot that no
96
+ # longer exists.
97
+ def clear_server_record!(name)
98
+ name = name.to_s
99
+ @monitor.synchronize do
100
+ @statuses.delete(name)
101
+ @boot_marks.delete(name)
102
+ end
103
+ end
104
+
105
+ # Stop-debt rule, structural (was prose in perform_load_from_config
106
+ # plus the [:failed, true].include? arithmetic): an applied or
107
+ # failed-apply slot stopped its own client (context drain / rollback);
108
+ # skipped and never-ran slots still owe a stop in the ensure.
109
+ def self.owes_stop?(outcome)
110
+ !outcome.equal?(:applied) && !outcome.equal?(:failed)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "transport/stdio"
5
+ require_relative "../version"
6
+
7
+ module Rubik
8
+ module MCP
9
+ class ToolResult
10
+ attr_reader :content, :is_error, :text
11
+
12
+ def initialize(content: [], is_error: false, text: "")
13
+ @content = content
14
+ @is_error = is_error
15
+ @text = text
16
+ end
17
+
18
+ def error?
19
+ @is_error
20
+ end
21
+
22
+ def to_s
23
+ @text
24
+ end
25
+
26
+ def [](key)
27
+ case key.to_s
28
+ when "content" then @content
29
+ when "isError" then @is_error
30
+ when "text" then @text
31
+ end
32
+ end
33
+
34
+ def key?(key)
35
+ ["content", "isError", "text"].include?(key.to_s)
36
+ end
37
+
38
+ def to_h
39
+ { "content" => @content, "isError" => @is_error, "text" => @text }
40
+ end
41
+ end
42
+
43
+ class Client
44
+ DEFAULT_PROTOCOL_VERSION = "2024-11-05"
45
+ # Connect handshake (initialize + tools/list) and tools/call round-trip
46
+ # defaults. The user-facing mirrors live in config/defaults.rb as
47
+ # mcp.timeout / mcp.call_timeout (MCP::Manager resolves and passes them
48
+ # down); these constants are the last link of that fallback chain.
49
+ DEFAULT_TIMEOUT = 10
50
+ DEFAULT_CALL_TIMEOUT = 30
51
+
52
+ attr_reader :name, :transport, :server_info, :server_capabilities, :protocol_version, :initialized
53
+
54
+ def initialize(command, args: [], env: {}, name: "unnamed", root_dir: Dir.pwd, transport: nil, guard: nil,
55
+ call_timeout: nil)
56
+ @name = name.to_s
57
+ @transport = transport || Transport::Stdio.new(command, args: args, env: env, root_dir: root_dir, guard: guard)
58
+ # Session-wide tools/call timeout; nil (absent or not a positive
59
+ # number) keeps the per-call DEFAULT_CALL_TIMEOUT fallback.
60
+ @default_call_timeout = positive_number?(call_timeout) ? call_timeout : nil
61
+ @server_info = {}
62
+ @server_capabilities = {}
63
+ @protocol_version = nil
64
+ @initialized = false
65
+ end
66
+
67
+ def start!(timeout: DEFAULT_TIMEOUT)
68
+ return self if @initialized && running?
69
+
70
+ @transport.open
71
+
72
+ init_params = {
73
+ "protocolVersion" => DEFAULT_PROTOCOL_VERSION,
74
+ "capabilities" => {},
75
+ "clientInfo" => {
76
+ "name" => "Rubik",
77
+ "version" => defined?(Rubik::VERSION) ? Rubik::VERSION : "1.0.0"
78
+ }
79
+ }
80
+
81
+ result = @transport.send_request("initialize", init_params, timeout: timeout)
82
+ if result.is_a?(Hash)
83
+ @protocol_version = result["protocolVersion"]
84
+ @server_info = result["serverInfo"] || {}
85
+ @server_capabilities = result["capabilities"] || {}
86
+ end
87
+
88
+ @transport.send_notification("notifications/initialized", {})
89
+ @initialized = true
90
+ self
91
+ end
92
+ alias start start!
93
+
94
+ def list_tools(timeout: DEFAULT_TIMEOUT)
95
+ ensure_running!
96
+ res = @transport.send_request("tools/list", {}, timeout: timeout)
97
+ if res.is_a?(Hash) && res.key?("tools")
98
+ Array(res["tools"]).compact
99
+ elsif res.is_a?(Array)
100
+ res.compact
101
+ else
102
+ []
103
+ end
104
+ end
105
+
106
+ def call_tool(tool_name, arguments = {}, timeout: nil)
107
+ ensure_running!
108
+ params = {
109
+ "name" => tool_name.to_s,
110
+ "arguments" => arguments || {}
111
+ }
112
+ res = @transport.send_request("tools/call", params, timeout: resolve_call_timeout(timeout))
113
+
114
+ is_error = res.is_a?(Hash) ? (res["isError"] == true) : false
115
+ content = if res.is_a?(Hash) && res["content"].is_a?(Array)
116
+ res["content"]
117
+ elsif res.is_a?(Array)
118
+ res
119
+ else
120
+ [{ "type" => "text", "text" => res.to_s }]
121
+ end
122
+
123
+ text = content.select { |c| c.is_a?(Hash) && (c["type"] == "text" || c[:type] == "text") }
124
+ .map { |c| (c["text"] || c[:text]).to_s }
125
+ .join("\n")
126
+
127
+ text = res.to_s if text.empty? && !res.nil?
128
+
129
+ ToolResult.new(content: content, is_error: is_error, text: text)
130
+ end
131
+
132
+ def ping(timeout: 5)
133
+ ensure_running!
134
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
135
+ @transport.send_request("ping", {}, timeout: timeout)
136
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(2)
137
+ end
138
+
139
+ def stop
140
+ @initialized = false
141
+ @transport.close
142
+ end
143
+ alias close stop
144
+ alias dispose stop
145
+
146
+ def running?
147
+ @transport&.running? || false
148
+ end
149
+
150
+ private
151
+
152
+ def ensure_running!
153
+ raise ConnectionError, "MCP client '#{@name}' is not running" unless running?
154
+ end
155
+
156
+ # Per-call override first, then the session-wide constructor value,
157
+ # then the constant — so an explicit timeout argument always wins.
158
+ def resolve_call_timeout(timeout)
159
+ return timeout if positive_number?(timeout)
160
+
161
+ @default_call_timeout || DEFAULT_CALL_TIMEOUT
162
+ end
163
+
164
+ def positive_number?(value)
165
+ value.is_a?(Numeric) && value.positive?
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module MCP
5
+ class Error < StandardError; end
6
+ class ConnectionError < Error; end
7
+ class TimeoutError < Error; end
8
+ class ProtocolError < Error; end
9
+ class ToolExecutionError < Error; end
10
+ end
11
+ end