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,359 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+ require_relative "../output"
5
+ require_relative "../../runtime"
6
+
7
+ module Rubik
8
+ module Tools
9
+ class ApplyBufferTool < BaseTool
10
+ # maestro_blocked: false is DECLARED policy, not drift (TOOLS.md maestro
11
+ # paragraph): apply_buffer is the manual-mode flush of a coder branch's
12
+ # merge — the root orchestrator persists it directly, and with the gate
13
+ # off the root edits-and-flushes itself.
14
+ DECLARATION = Declaration.new(
15
+ name: "apply_buffer", aliases: ["save_buffer", "commit_changes", "save"],
16
+ description: "Persist all pending in-memory file buffer modifications to disk immediately (skips files with external conflicts unless force: true)",
17
+ risk: :write, archetypes: [:coder], write_grant: true, maestro_blocked: false,
18
+ params_schema: {
19
+ type: "object",
20
+ properties: {
21
+ reason: { type: "string", description: "Optional explanation of why buffer is being applied (e.g. 'prior to running tests')" },
22
+ force: { type: "boolean",
23
+ description: "If true, overwrites disk files even if modified externally by formatters (resolves [CONFLICT])" }
24
+ }
25
+ }
26
+ )
27
+ def initialize(buffer_manager = nil, shell = nil)
28
+ super(DECLARATION)
29
+ @buffer_manager = buffer_manager
30
+ @shell = shell
31
+ end
32
+
33
+ def execute(args = {})
34
+ return ToolOutput.error("Buffer manager is not connected.") unless @buffer_manager
35
+
36
+ if @shell&.config && @shell.config.fetch("buffer.model_can_apply", true) == false
37
+ return ToolOutput.error("Model buffer application is disabled by configuration " \
38
+ "(buffer.model_can_apply: false). The human operator will review and apply changes.")
39
+ end
40
+
41
+ force = [true, "true"].include?(args["force"]) || [true, "true"].include?(args[:force]) ||
42
+ [true, "true"].include?(args["overwrite"]) || [true, "true"].include?(args[:overwrite])
43
+ policy = force ? "overwrite" : nil
44
+
45
+ if @buffer_manager.dirty?
46
+ applied = @buffer_manager.apply!(policy: policy)
47
+ conflicts = @buffer_manager.last_conflicts || []
48
+ @shell&.sync_context_and_ast(applied) unless applied.empty?
49
+
50
+ messages = []
51
+ messages << "Successfully applied #{applied.size} modified file(s) to disk: #{applied.join(', ')}" unless applied.empty?
52
+ conflicts.each do |c|
53
+ messages << "[CONFLICT] #{c}: disk content changed since last read — skipped to prevent data loss.\n" \
54
+ "To force overwrite with your buffer changes, call: apply_buffer(force: true)\n" \
55
+ "Call read_file(path: \"#{c}\", reload: true) to inspect external changes or resolve."
56
+ end
57
+
58
+ if applied.empty? && !conflicts.empty?
59
+ ToolOutput.error(messages.join("\n"))
60
+ elsif applied.empty?
61
+ # Inside the dirty? branch with zero applies and zero conflicts,
62
+ # every buffered entry was discarded as an invalid path
63
+ # (apply!'s garbage-skip) — the edits are NOT on disk, and citing
64
+ # an earlier apply here would be a lie.
65
+ "No unapplied buffer changes to write: the buffered entries were discarded as invalid paths and are NOT on disk. " \
66
+ "Re-check the target paths and buffer them again."
67
+ else
68
+ messages.join("\n")
69
+ end
70
+ else
71
+ clean_buffer_message
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ # "Buffer is clean" alone reads as "nothing happened" when an earlier
78
+ # apply (apply_buffer itself or the auto-apply policy) already flushed
79
+ # the edits to disk. Surface the last successful apply so the model can
80
+ # verify against reality instead of re-editing or force-applying
81
+ # (issue #97 finding 6). When the mode cannot be read, the origin stays
82
+ # deliberately two-sided rather than inventing a specific cause.
83
+ def clean_buffer_message
84
+ last = @buffer_manager.last_applied
85
+ return "No unapplied buffer changes to write (buffer is clean)." unless last.is_a?(Hash) && Array(last[:paths]).any?
86
+
87
+ mode = @shell.respond_to?(:config) && @shell.config.respond_to?(:fetch) ? @shell.config.fetch("buffer.auto_apply", "manual") : nil
88
+ origin = if !mode.nil? && !mode.to_s.strip.empty? && mode.to_s.downcase != "manual"
89
+ "the auto-apply policy (buffer.auto_apply: #{mode})"
90
+ else
91
+ "an earlier apply_buffer call or the auto-apply policy"
92
+ end
93
+ "No unapplied buffer changes to write (buffer is clean): #{last[:paths].size} file(s) were already written to disk " \
94
+ "at #{last[:at].utc.iso8601} by #{origin}: #{last[:paths].join(', ')}."
95
+ end
96
+ end
97
+
98
+ class RepairRuntimeTool < BaseTool
99
+ DECLARATION = Declaration.new(
100
+ name: "repair_runtime_service", aliases: ["fix_service", "repair_lsp", "repair_runner", "repair_runtime"],
101
+ description: "Diagnose, adapt, or synthesize a replacement command/adapter for a failing runtime component " \
102
+ "(LSP, test runner, linter, or shell command) with pre-flight verification in an isolated sub-context",
103
+ risk: :execute, maestro_blocked: false, # runtime self-healing plane (TOOLS.md maestro policy)
104
+ params_schema: {
105
+ type: "object",
106
+ properties: {
107
+ target: {
108
+ type: "string",
109
+ enum: ["lsp", "test_runner", "linter", "command"],
110
+ description: "Runtime component type to repair or adapt"
111
+ },
112
+ language_or_type: {
113
+ type: "string",
114
+ description: "Language or service identifier (e.g., 'ruby', 'python', 'rust', 'go', 'zig', 'c', 'bash')"
115
+ },
116
+ proposed_command: {
117
+ type: "string",
118
+ description: "Replacement or custom command string (optional, probed automatically if auto_probe: true)"
119
+ },
120
+ auto_probe: {
121
+ type: "boolean",
122
+ description: "Whether to automatically probe system binaries and candidate commands (default: true)"
123
+ }
124
+ },
125
+ required: ["target", "language_or_type"]
126
+ }
127
+ )
128
+ def initialize(runtime_manager: nil, lsp_manager: nil, adapter_registry: nil, overlay_storage: nil, config: nil, root_dir: Dir.pwd,
129
+ guard: nil)
130
+ super(DECLARATION)
131
+ @runtime_manager = runtime_manager
132
+ @lsp_manager = lsp_manager
133
+ @adapter_registry = adapter_registry
134
+ @overlay_storage = overlay_storage
135
+ @config = config
136
+ @root_dir = File.expand_path(root_dir)
137
+ @guard = guard
138
+ end
139
+
140
+ def execute(args = {})
141
+ target = (args["target"] || args[:target]).to_s.downcase.strip
142
+ lang = (args["language_or_type"] || args[:language_or_type]).to_s.downcase.strip
143
+ proposed_cmd = (args["proposed_command"] || args[:proposed_command]).to_s.strip
144
+ auto_probe = if args["auto_probe"].nil?
145
+ args[:auto_probe].nil? || args[:auto_probe]
146
+ else
147
+ args["auto_probe"]
148
+ end
149
+
150
+ return ToolOutput.error("Target and language_or_type must be provided.") if target.empty? || lang.empty?
151
+
152
+ # 1. Automatic Probing if proposed_cmd is not specified or auto_probe is requested
153
+ if proposed_cmd.empty? && auto_probe
154
+ candidate = case target
155
+ when "lsp"
156
+ Rubik::Runtime::Discovery.probe_lsp(lang, root_dir: @root_dir)
157
+ when "test_runner"
158
+ Rubik::Runtime::Discovery.discover_test_runners(root_dir: @root_dir, lang: lang).first
159
+ when "linter"
160
+ Rubik::Runtime::Discovery.discover_formatters(root_dir: @root_dir, lang: lang).first
161
+ when "command"
162
+ Rubik::Runtime::Discovery.binary_available?(lang) ? lang : nil
163
+ end
164
+
165
+ if candidate
166
+ proposed_cmd = candidate
167
+ else
168
+ candidates = case target
169
+ when "lsp" then Rubik::Runtime::Discovery.lsp_candidates(lang)
170
+ when "test_runner" then Rubik::Runtime::Discovery.test_runner_candidates(lang, root_dir: @root_dir)
171
+ when "linter" then Rubik::Runtime::Discovery::FORMATTER_CANDIDATES[lang] || []
172
+ else []
173
+ end
174
+ cand_str = candidates.empty? ? "none" : candidates.join(", ")
175
+ return ToolOutput.error("Auto-probe failed to discover any installed candidate for #{target} (#{lang}). " \
176
+ "Candidates checked: [#{cand_str}]. Please provide a specific `proposed_command`.")
177
+ end
178
+ end
179
+
180
+ return ToolOutput.error("No command provided or discovered for #{target} (#{lang}).") if proposed_cmd.empty?
181
+
182
+ # 2. Pre-flight Verification in Isolated Sub-Context (ctx.extend())
183
+ root_ctx = @runtime_manager&.context || Rubik::Runtime::Context.new
184
+ sub_ctx = root_ctx.extend
185
+
186
+ verified = false
187
+ error_diagnostic = nil
188
+
189
+ begin
190
+ verified, error_diagnostic = verify_binary(target, lang, proposed_cmd, sub_ctx)
191
+ rescue StandardError => e
192
+ error_diagnostic = Rubik::Runtime::DiagnosticEngine.diagnose(
193
+ cmd: proposed_cmd,
194
+ exit_code: 1,
195
+ stderr: "Exception during pre-flight sandbox check: #{e.message}"
196
+ )
197
+ ensure
198
+ sub_ctx.dispose
199
+ end
200
+
201
+ return ToolOutput.error("Verification failed for #{target} (#{lang}):\n#{error_diagnostic&.format_for_llm}") unless verified
202
+
203
+ # 3. Live Context Promotion & Subsystem Configuration (Zero-Downtime Hot Swapping)
204
+ case target
205
+ when "lsp"
206
+ cfg_key = "lsp.servers.#{lang}"
207
+ prev_cmd = @config&.fetch(cfg_key)
208
+ had_client = @lsp_manager ? !@lsp_manager.client_for(lang).nil? : false
209
+ @config&.set(cfg_key, proposed_cmd)
210
+ if @lsp_manager
211
+ @lsp_manager.stop_client(lang)
212
+ start_res = @lsp_manager.start_client(lang)
213
+ unless start_res.is_a?(Hash) && start_res[:ok] == true
214
+ # Failure Reversibility (AGENTS.md §1.A): roll back the in-memory
215
+ # state; disk stays clean because persist happens only after a
216
+ # verified start (atomic promotion).
217
+ @lsp_manager.stop_client(lang)
218
+ @config&.set(cfg_key, prev_cmd)
219
+ @lsp_manager.start_client(lang) if had_client
220
+ cause = start_res.is_a?(Hash) ? start_res[:error] : "unexpected start_client response"
221
+ return ToolOutput.error("Failed to start LSP server for #{lang} ('#{proposed_cmd}'); " \
222
+ "previous configuration restored. Cause: #{cause}")
223
+ end
224
+ active_client = @lsp_manager.client_for(lang)
225
+ root_ctx.provide("lsp:#{lang}", active_client || proposed_cmd)
226
+ else
227
+ root_ctx.provide("lsp:#{lang}", proposed_cmd)
228
+ end
229
+ # Persist to project config only after the live promotion succeeded.
230
+ persist_res = @config&.set(cfg_key, proposed_cmd, persist: :project)
231
+ when "test_runner"
232
+ persist_res = @config&.set("test.runners.#{lang}", proposed_cmd, persist: :project)
233
+ if @adapter_registry
234
+ adapter = @adapter_registry.get(lang)
235
+ if adapter
236
+ adapter.config ||= @config
237
+ adapter.custom_test_cmd = proposed_cmd
238
+ else
239
+ require_relative "../../adapter/generic" unless defined?(Rubik::Adapter::GenericAdapter)
240
+ new_adapter = Rubik::Adapter::GenericAdapter.new(lang, test_cmd: proposed_cmd, guard: @guard)
241
+ new_adapter.config = @config
242
+ @adapter_registry.register(lang, new_adapter, ctx: root_ctx)
243
+ end
244
+ end
245
+ root_ctx.provide("runner:#{lang}", proposed_cmd)
246
+ when "linter"
247
+ persist_res = @config&.set("linter.commands.#{lang}", proposed_cmd, persist: :project)
248
+ if @adapter_registry
249
+ adapter = @adapter_registry.get(lang)
250
+ if adapter
251
+ adapter.config ||= @config
252
+ adapter.custom_format_cmd = proposed_cmd
253
+ else
254
+ require_relative "../../adapter/generic" unless defined?(Rubik::Adapter::GenericAdapter)
255
+ new_adapter = Rubik::Adapter::GenericAdapter.new(lang, format_cmd: proposed_cmd, guard: @guard)
256
+ new_adapter.config = @config
257
+ @adapter_registry.register(lang, new_adapter, ctx: root_ctx)
258
+ end
259
+ end
260
+ root_ctx.provide("linter:#{lang}", proposed_cmd)
261
+ when "command"
262
+ root_ctx.provide("command:#{lang}", proposed_cmd)
263
+ end
264
+
265
+ if @config.nil? && ["lsp", "test_runner", "linter"].include?(target)
266
+ return "⚠️ Promoted '#{proposed_cmd}' for #{target} (#{lang}) — no config is attached, " \
267
+ "so the command is active for this session only and was not saved."
268
+ end
269
+
270
+ if persist_res.is_a?(Hash) && !persist_res[:ok]
271
+ return "⚠️ Promoted '#{proposed_cmd}' for #{target} (#{lang}) — active in this session, " \
272
+ "but saving to the project config failed: #{persist_res[:error]}"
273
+ end
274
+
275
+ "✨ Successfully verified and promoted runtime service for #{target} (#{lang}): '#{proposed_cmd}'. " \
276
+ "Service is now active and hot-swapped in the live context with zero downtime."
277
+ end
278
+
279
+ private
280
+
281
+ def verify_binary(target, lang, proposed_cmd, sub_ctx)
282
+ case target
283
+ when "lsp"
284
+ client = Rubik::LSP::Client.new(lang, root_dir: @root_dir, server_cmd: proposed_cmd)
285
+ if client.available?
286
+ sub_ctx.provide("lsp_probe:#{lang}", client)
287
+ [true, nil]
288
+ else
289
+ [false, Rubik::Runtime::DiagnosticEngine.diagnose(
290
+ cmd: proposed_cmd,
291
+ exit_code: 127,
292
+ stderr: "Pre-flight check failed: LSP binary '#{client.binary_name}' not available on system"
293
+ )]
294
+ end
295
+ when "test_runner"
296
+ if Rubik::Runtime::Discovery.binary_available?(proposed_cmd)
297
+ sub_ctx.provide("runner_probe:#{lang}", proposed_cmd)
298
+ [true, nil]
299
+ else
300
+ [false, Rubik::Runtime::DiagnosticEngine.diagnose(
301
+ cmd: proposed_cmd,
302
+ exit_code: 127,
303
+ stderr: "Pre-flight check failed: Test runner executable not found in PATH"
304
+ )]
305
+ end
306
+ when "linter"
307
+ if Rubik::Runtime::Discovery.binary_available?(proposed_cmd)
308
+ sub_ctx.provide("linter_probe:#{lang}", proposed_cmd)
309
+ [true, nil]
310
+ else
311
+ [false, Rubik::Runtime::DiagnosticEngine.diagnose(
312
+ cmd: proposed_cmd,
313
+ exit_code: 127,
314
+ stderr: "Pre-flight check failed: Linter/formatter executable not found in PATH"
315
+ )]
316
+ end
317
+ when "command"
318
+ if Rubik::Runtime::Discovery.binary_available?(proposed_cmd)
319
+ sub_ctx.provide("command_probe:#{lang}", proposed_cmd)
320
+ [true, nil]
321
+ else
322
+ [false, Rubik::Runtime::DiagnosticEngine.diagnose(
323
+ cmd: proposed_cmd,
324
+ exit_code: 127,
325
+ stderr: "Pre-flight check failed: Command executable not found in PATH"
326
+ )]
327
+ end
328
+ else
329
+ [false, Rubik::Runtime::DiagnosticEngine.diagnose(
330
+ cmd: proposed_cmd,
331
+ exit_code: 1,
332
+ stderr: "Unknown target component type: #{target}"
333
+ )]
334
+ end
335
+ end
336
+ end
337
+
338
+ class FinishTool < BaseTool
339
+ DECLARATION = Declaration.new(
340
+ name: "finish", aliases: ["complete", "done", "task_complete", "exit"],
341
+ description: "Signal task completion with final message. Before finishing, re-verify headline claims " \
342
+ "(file lists, counts, command outcomes) against your raw command output — context " \
343
+ "compaction can blur report details, and the final report must match observed evidence.",
344
+ risk: :execute, maestro_blocked: false, # the completion signal — handled before the gate; must stay direct
345
+ params_schema: { type: "object",
346
+ properties: { message: { type: "string",
347
+ description: "Final summary message for the user describing what was accomplished (optional)" } } }
348
+ )
349
+
350
+ def initialize
351
+ super(DECLARATION)
352
+ end
353
+
354
+ def execute(args = {})
355
+ args["message"] || args[:message] || "Task finished"
356
+ end
357
+ end
358
+ end
359
+ end
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+ require_relative "../../coeffects"
5
+ require_relative "../output"
6
+
7
+ module Rubik
8
+ module Tools
9
+ class LSPDiagnosticsTool < BaseTool
10
+ attr_reader :lsp_manager, :buffer_manager
11
+
12
+ DECLARATION = Declaration.new(
13
+ name: "lsp_diagnostics", aliases: ["get_diagnostics", "diagnostics"],
14
+ description: "Get real-time compiler and linter diagnostics (errors, warnings) for a file or all active project files via LSP",
15
+ risk: :read, archetypes: [:research, :coder],
16
+ params_schema: {
17
+ type: "object",
18
+ properties: {
19
+ file_path: { type: "string", description: "Optional specific file path to query diagnostics for" }
20
+ }
21
+ }
22
+ )
23
+ def initialize(lsp_manager = nil, buffer_mgr = nil, buffer_manager: nil)
24
+ super(DECLARATION)
25
+ @lsp_manager = lsp_manager
26
+ @buffer_manager = buffer_manager || buffer_mgr
27
+ end
28
+
29
+ def effective_buffer_manager
30
+ @buffer_manager || @lsp_manager&.context&.get(Rubik::Coeffects::BUFFER_MANAGER)
31
+ end
32
+
33
+ def execute(args = {})
34
+ return ToolOutput.error("LSP manager is not connected.") unless @lsp_manager
35
+ return ToolOutput.error("LSP is currently disabled in configuration (enable via '!lsp on').") unless @lsp_manager.enabled?
36
+
37
+ path = (args["file_path"] || args[:file_path] || "").to_s.strip
38
+ bm = effective_buffer_manager
39
+ @lsp_manager.sync_buffer_file_if_needed(path, bm) if bm && @lsp_manager.respond_to?(:sync_buffer_file_if_needed)
40
+
41
+ if path.empty?
42
+ all = @lsp_manager.all_diagnostics
43
+ total_issues = all.values.flatten.size
44
+ if total_issues.zero?
45
+ "No active LSP diagnostics across workspace (all clean)."
46
+ else
47
+ lines = ["=== Active Workspace LSP Diagnostics (#{total_issues} issue(s)) ==="]
48
+ all.each do |fpath, diags|
49
+ next if diags.empty?
50
+
51
+ lines << "\nFile: #{fpath} (#{diags.size}):"
52
+ diags.each do |d|
53
+ sev = d["severity"] == 1 ? "ERROR" : "WARN"
54
+ start_line = (d.dig("range", "start", "line") || 0) + 1
55
+ lines << " • [#{sev}] Line #{start_line}: #{d['message']}"
56
+ end
57
+ end
58
+ lines.join("\n")
59
+ end
60
+ else
61
+ diags = @lsp_manager.diagnostics_for(path)
62
+ if diags.empty?
63
+ append_buffer_tip("No LSP diagnostics or errors for '#{path}'.", path)
64
+ else
65
+ lines = ["=== LSP Diagnostics for #{path} (#{diags.size} issue(s)) ==="]
66
+ diags.each do |d|
67
+ sev = case d["severity"]
68
+ when 1 then "ERROR"
69
+ when 2 then "WARN"
70
+ when 3 then "INFO"
71
+ else "HINT"
72
+ end
73
+ range = d["range"] || {}
74
+ start_line = (range.dig("start", "line") || 0) + 1
75
+ start_char = (range.dig("start", "character") || 0) + 1
76
+ lines << " • [#{sev}] Line #{start_line}:#{start_char} - #{d['message']}"
77
+ end
78
+ lines.join("\n")
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def buffer_dirty?(path)
86
+ bm = effective_buffer_manager
87
+ return false unless bm && !path.to_s.empty?
88
+
89
+ if bm.respond_to?(:dirty?)
90
+ begin
91
+ bm.method(:dirty?).arity.zero? ? bm.dirty? : bm.dirty?(path)
92
+ rescue ArgumentError
93
+ bm.dirty?
94
+ end
95
+ else
96
+ false
97
+ end
98
+ end
99
+
100
+ def append_buffer_tip(msg, path)
101
+ return msg unless buffer_dirty?(path)
102
+
103
+ tip = "\n\n💡 Tip: '#{path}' has unapplied in-memory buffer modifications. " \
104
+ "Call 'apply_buffer' to persist changes to disk for language servers requiring disk-based indexing."
105
+ "#{msg}#{tip}"
106
+ end
107
+ end
108
+
109
+ class LSPHoverTool < BaseTool
110
+ attr_reader :lsp_manager, :buffer_manager
111
+
112
+ DECLARATION = Declaration.new(
113
+ name: "lsp_hover", aliases: ["get_hover", "hover"],
114
+ description: "Get compiler-level type signature, docstring, and symbol details at a specific line and character position via LSP",
115
+ risk: :read, archetypes: [:research],
116
+ params_schema: {
117
+ type: "object",
118
+ properties: {
119
+ file_path: { type: "string", description: "Path to source file" },
120
+ line: { type: "integer", description: "1-based line number" },
121
+ character: { type: "integer", description: "1-based character position" }
122
+ },
123
+ required: ["file_path", "line", "character"]
124
+ }
125
+ )
126
+ def initialize(lsp_manager = nil, buffer_mgr = nil, buffer_manager: nil)
127
+ super(DECLARATION)
128
+ @lsp_manager = lsp_manager
129
+ @buffer_manager = buffer_manager || buffer_mgr
130
+ end
131
+
132
+ def effective_buffer_manager
133
+ @buffer_manager || @lsp_manager&.context&.get(Rubik::Coeffects::BUFFER_MANAGER)
134
+ end
135
+
136
+ def execute(args = {})
137
+ return ToolOutput.error("LSP manager is not connected.") unless @lsp_manager
138
+ return ToolOutput.error("LSP is currently disabled in configuration (enable via '!lsp on').") unless @lsp_manager.enabled?
139
+
140
+ path = (args["file_path"] || args[:file_path]).to_s.strip
141
+ line = (args["line"] || args[:line]).to_i
142
+ char = (args["character"] || args[:character] || args["col"] || args[:col]).to_i
143
+
144
+ return ToolOutput.error("File path is required.") if path.empty?
145
+ return ToolOutput.error("Line number must be >= 1.") if line < 1
146
+
147
+ bm = effective_buffer_manager
148
+ @lsp_manager.sync_buffer_file_if_needed(path, bm) if bm && @lsp_manager.respond_to?(:sync_buffer_file_if_needed)
149
+
150
+ res = @lsp_manager.hover(path, [line - 1, 0].max, [char - 1, 0].max)
151
+ if res[:ok]
152
+ contents = res.dig(:result, "contents")
153
+ if contents.nil? || contents.empty?
154
+ append_buffer_tip("No hover information available at #{path}:#{line}:#{char}", path)
155
+ elsif contents.is_a?(Hash) && contents["value"]
156
+ contents["value"]
157
+ elsif contents.is_a?(Array)
158
+ contents.map { |c| c.is_a?(Hash) ? c["value"] : c.to_s }.join("\n")
159
+ else
160
+ contents.to_s
161
+ end
162
+ else
163
+ ToolOutput.error("LSP Hover failed: #{res[:error]}")
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ def buffer_dirty?(path)
170
+ bm = effective_buffer_manager
171
+ return false unless bm && !path.to_s.empty?
172
+
173
+ if bm.respond_to?(:dirty?)
174
+ begin
175
+ bm.method(:dirty?).arity.zero? ? bm.dirty? : bm.dirty?(path)
176
+ rescue ArgumentError
177
+ bm.dirty?
178
+ end
179
+ else
180
+ false
181
+ end
182
+ end
183
+
184
+ def append_buffer_tip(msg, path)
185
+ return msg unless buffer_dirty?(path)
186
+
187
+ tip = "\n\n💡 Tip: '#{path}' has unapplied in-memory buffer modifications. " \
188
+ "Call 'apply_buffer' to persist changes to disk for language servers requiring disk-based indexing."
189
+ "#{msg}#{tip}"
190
+ end
191
+ end
192
+
193
+ class LSPDefinitionTool < BaseTool
194
+ attr_reader :lsp_manager, :buffer_manager
195
+
196
+ DECLARATION = Declaration.new(
197
+ name: "lsp_definition", aliases: ["goto_definition", "definition"],
198
+ description: "Jump to exact type-safe declaration and definition location of a symbol at line/character position via LSP",
199
+ risk: :read, archetypes: [:research],
200
+ params_schema: {
201
+ type: "object",
202
+ properties: {
203
+ file_path: { type: "string", description: "Path to source file" },
204
+ line: { type: "integer", description: "1-based line number" },
205
+ character: { type: "integer", description: "1-based character position" }
206
+ },
207
+ required: ["file_path", "line", "character"]
208
+ }
209
+ )
210
+ def initialize(lsp_manager = nil, buffer_mgr = nil, buffer_manager: nil)
211
+ super(DECLARATION)
212
+ @lsp_manager = lsp_manager
213
+ @buffer_manager = buffer_manager || buffer_mgr
214
+ end
215
+
216
+ def effective_buffer_manager
217
+ @buffer_manager || @lsp_manager&.context&.get(Rubik::Coeffects::BUFFER_MANAGER)
218
+ end
219
+
220
+ def execute(args = {})
221
+ return ToolOutput.error("LSP manager is not connected.") unless @lsp_manager
222
+ return ToolOutput.error("LSP is currently disabled in configuration (enable via '!lsp on').") unless @lsp_manager.enabled?
223
+
224
+ path = (args["file_path"] || args[:file_path]).to_s.strip
225
+ line = (args["line"] || args[:line]).to_i
226
+ char = (args["character"] || args[:character] || args["col"] || args[:col]).to_i
227
+
228
+ return ToolOutput.error("File path is required.") if path.empty?
229
+ return ToolOutput.error("Line number must be >= 1.") if line < 1
230
+
231
+ bm = effective_buffer_manager
232
+ @lsp_manager.sync_buffer_file_if_needed(path, bm) if bm && @lsp_manager.respond_to?(:sync_buffer_file_if_needed)
233
+
234
+ res = @lsp_manager.definition(path, [line - 1, 0].max, [char - 1, 0].max)
235
+ if res[:ok]
236
+ result = res[:result]
237
+ if result.nil? || (result.is_a?(Array) && result.empty?)
238
+ append_buffer_tip("No definition found for symbol at #{path}:#{line}:#{char}", path)
239
+ else
240
+ locs = result.is_a?(Array) ? result : [result]
241
+ lines = ["Found #{locs.size} definition location(s):"]
242
+ locs.each do |loc|
243
+ uri = loc["uri"] || loc["targetUri"]
244
+ clean_file = uri.to_s.sub(%r{^file://}, "")
245
+ range = loc["range"] || loc["targetSelectionRange"] || {}
246
+ s_line = (range.dig("start", "line") || 0) + 1
247
+ s_char = (range.dig("start", "character") || 0) + 1
248
+ lines << " • #{clean_file}:#{s_line}:#{s_char}"
249
+ end
250
+ lines.join("\n")
251
+ end
252
+ else
253
+ ToolOutput.error("LSP Definition failed: #{res[:error]}")
254
+ end
255
+ end
256
+
257
+ private
258
+
259
+ def buffer_dirty?(path)
260
+ bm = effective_buffer_manager
261
+ return false unless bm && !path.to_s.empty?
262
+
263
+ if bm.respond_to?(:dirty?)
264
+ begin
265
+ bm.method(:dirty?).arity.zero? ? bm.dirty? : bm.dirty?(path)
266
+ rescue ArgumentError
267
+ bm.dirty?
268
+ end
269
+ else
270
+ false
271
+ end
272
+ end
273
+
274
+ def append_buffer_tip(msg, path)
275
+ return msg unless buffer_dirty?(path)
276
+
277
+ tip = "\n\n💡 Tip: '#{path}' has unapplied in-memory buffer modifications. " \
278
+ "Call 'apply_buffer' to persist changes to disk for language servers requiring disk-based indexing."
279
+ "#{msg}#{tip}"
280
+ end
281
+ end
282
+ end
283
+ end