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,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../tools/base"
4
+ require_relative "../tools/result"
5
+
6
+ module Rubik
7
+ module MCP
8
+ class ProxyTool < Rubik::Tools::BaseTool
9
+ attr_reader :server_name, :original_name, :client
10
+
11
+ def initialize(server_name:, tool_def:, client:)
12
+ @server_name = server_name.to_s
13
+ @client = client
14
+ @original_name = (tool_def["name"] || tool_def[:name]).to_s
15
+
16
+ sanitized_server = @server_name.downcase.gsub(/[^a-z0-9_]/, "_")
17
+ sanitized_tool = @original_name.downcase.gsub(/[^a-z0-9_]/, "_")
18
+ tool_name = "mcp_#{sanitized_server}_#{sanitized_tool}"
19
+
20
+ raw_desc = (tool_def["description"] || tool_def[:description] || "").to_s.strip
21
+ desc = raw_desc.empty? ? "[MCP: #{@server_name}] #{@original_name}" : "[MCP: #{@server_name}] #{raw_desc}"
22
+ raw_schema = tool_def["inputSchema"] || tool_def[:inputSchema]
23
+ schema = raw_schema.is_a?(Hash) ? raw_schema.dup : {}
24
+ schema["type"] = "object" unless schema["type"].is_a?(String)
25
+ schema["properties"] = {} unless schema["properties"].is_a?(Hash)
26
+
27
+ super(
28
+ name: tool_name,
29
+ description: desc,
30
+ risk: risk_from_annotations(tool_def["annotations"] || tool_def[:annotations]),
31
+ params_schema: schema
32
+ )
33
+ end
34
+
35
+ # Maps MCP tool annotations onto Rubik's risk levels so approval
36
+ # guardrails can distinguish read-only MCP tools from destructive or
37
+ # open-world ones. Unannotated tools stay conservative (:execute).
38
+ def risk_from_annotations(annotations)
39
+ return :execute unless annotations.is_a?(Hash)
40
+
41
+ if annotations["readOnlyHint"] == true
42
+ :read
43
+ elsif annotations["destructiveHint"] == true
44
+ :write
45
+ else
46
+ :execute
47
+ end
48
+ end
49
+
50
+ def execute(args = {})
51
+ args_hash = args.is_a?(Hash) ? args : {}
52
+ res = @client.call_tool(@original_name, args_hash)
53
+
54
+ is_error = if res.respond_to?(:error?)
55
+ res.error?
56
+ elsif res.is_a?(Hash) || res.respond_to?(:[])
57
+ res["isError"] == true || res[:isError] == true
58
+ else
59
+ false
60
+ end
61
+
62
+ output_text = if res.is_a?(Hash)
63
+ if res["text"]
64
+ res["text"].to_s
65
+ elsif res["content"].is_a?(Array)
66
+ res["content"].select { |c| (c["type"] || c[:type]) == "text" }
67
+ .map { |c| (c["text"] || c[:text]).to_s }
68
+ .join("\n")
69
+ else
70
+ res.inspect
71
+ end
72
+ else
73
+ res.to_s
74
+ end
75
+
76
+ # Output size is bounded by the orchestrator's Truncator (single
77
+ # truncation point, head_tail strategy); no pre-truncation here so
78
+ # the strategy stays consistent for MCP and built-in tools alike.
79
+
80
+ if is_error
81
+ Rubik::Tools::Result.new(
82
+ tool_name: @name,
83
+ status: :error,
84
+ output: output_text,
85
+ error: output_text
86
+ )
87
+ else
88
+ Rubik::Tools::Result.new(
89
+ tool_name: @name,
90
+ status: :ok,
91
+ output: output_text
92
+ )
93
+ end
94
+ rescue StandardError => e
95
+ Rubik::Tools::Result.new(
96
+ tool_name: @name,
97
+ status: :error,
98
+ output: e.message,
99
+ error: e.message
100
+ )
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "json"
5
+ require "shellwords"
6
+ require_relative "../errors"
7
+ require_relative "../../platform"
8
+ require_relative "../../security/guard"
9
+
10
+ module Rubik
11
+ module MCP
12
+ module Transport
13
+ class Stdio
14
+ attr_reader :command, :args, :env, :root_dir, :running, :wait_thr
15
+
16
+ def initialize(command, args: [], env: {}, root_dir: Dir.pwd, guard: nil)
17
+ @command = command
18
+ @args = Array(args).map(&:to_s)
19
+ @env = env || {}
20
+ @root_dir = File.expand_path(root_dir)
21
+ @guard = guard || Rubik::Security::Guard.new
22
+ @running = false
23
+ @req_id = 0
24
+ @stdin = nil
25
+ @stdout = nil
26
+ @stderr = nil
27
+ @wait_thr = nil
28
+ @reader_thr = nil
29
+ @mutex = Mutex.new
30
+ @cv = ConditionVariable.new
31
+ @responses = {}
32
+ end
33
+
34
+ def open
35
+ return if @running
36
+
37
+ cmd_parts = if @args.empty? && @command.is_a?(String)
38
+ begin
39
+ Shellwords.split(@command)
40
+ rescue ArgumentError => e
41
+ raise ConnectionError, "Invalid command syntax: #{e.message}"
42
+ end
43
+ else
44
+ [@command.to_s] + @args
45
+ end
46
+
47
+ bin = cmd_parts.first
48
+ raise ConnectionError, "MCP command cannot be empty" if bin.nil? || bin.empty?
49
+
50
+ # Red-Team Defense: Verify command safety against destructive operations.
51
+ # Route the MCP spawn command through the same Guard#verify_command_safety!
52
+ # call that RunCommandTool uses — the narrow DANGEROUS_COMMAND_PATTERNS
53
+ # subset below was a bypass: it misses many dangerous commands the
54
+ # classifier flags. Fails closed on any dangerous/unmanaged command.
55
+ full_cmd = Shellwords.shelljoin(cmd_parts)
56
+ begin
57
+ @guard.verify_command_safety!(full_cmd)
58
+ rescue Rubik::Security::BlockedWriteError => e
59
+ # No manual audit here: verify_command_safety! already logged
60
+ # "blocked_command" with the TRUE classifier reason before raising
61
+ # — a second hardcoded entry would double-log and mislabel it.
62
+ raise ConnectionError, "Security blocked MCP process: #{e.message}"
63
+ end
64
+
65
+ resolved_bin = Rubik::Platform.which(bin)
66
+ cmd_parts[0] = resolved_bin if resolved_bin
67
+
68
+ string_env = @env.transform_keys(&:to_s).transform_values(&:to_s)
69
+
70
+ begin
71
+ @stdin, @stdout, @stderr, @wait_thr = if string_env.empty?
72
+ Open3.popen3(*cmd_parts, chdir: @root_dir)
73
+ else
74
+ Open3.popen3(string_env, *cmd_parts, chdir: @root_dir)
75
+ end
76
+ rescue SystemCallError => e
77
+ raise ConnectionError, "Failed to spawn MCP process '#{bin}': #{e.message}"
78
+ end
79
+
80
+ @running = true
81
+ @req_id = 0
82
+ @responses.clear
83
+
84
+ start_reader_thread
85
+ self
86
+ end
87
+ alias start open
88
+
89
+ def send_request(method, params = {}, timeout: 10)
90
+ raise ConnectionError, "MCP transport is not running" unless @running && @stdin
91
+
92
+ req_id = @mutex.synchronize do
93
+ @req_id += 1
94
+ @req_id
95
+ end
96
+
97
+ msg = {
98
+ "jsonrpc" => "2.0",
99
+ "id" => req_id,
100
+ "method" => method.to_s,
101
+ "params" => params || {}
102
+ }
103
+
104
+ write_message(msg)
105
+ wait_for_response(req_id, timeout: timeout)
106
+ end
107
+
108
+ def send_notification(method, params = {})
109
+ raise ConnectionError, "MCP transport is not running" unless @running && @stdin
110
+
111
+ msg = {
112
+ "jsonrpc" => "2.0",
113
+ "method" => method.to_s,
114
+ "params" => params || {}
115
+ }
116
+
117
+ write_message(msg)
118
+ end
119
+
120
+ def close
121
+ return unless @running || @wait_thr&.alive?
122
+
123
+ @running = false
124
+
125
+ begin
126
+ @reader_thr&.kill
127
+ rescue StandardError
128
+ nil
129
+ end
130
+
131
+ [@stdin, @stdout, @stderr].each do |io|
132
+ io&.close unless io&.closed?
133
+ rescue StandardError
134
+ nil
135
+ end
136
+
137
+ begin
138
+ @reader_thr&.join(0.5)
139
+ rescue StandardError
140
+ nil
141
+ ensure
142
+ @reader_thr = nil
143
+ end
144
+
145
+ if @wait_thr&.alive?
146
+ Rubik::Platform.kill_tree(@wait_thr.pid)
147
+ begin
148
+ @wait_thr.join(0.5)
149
+ rescue StandardError
150
+ nil
151
+ end
152
+ end
153
+ @wait_thr = nil
154
+
155
+ @mutex.synchronize do
156
+ @responses.clear
157
+ @cv.broadcast
158
+ end
159
+ end
160
+ alias stop close
161
+ alias dispose close
162
+
163
+ def running?
164
+ @running && (@wait_thr.nil? || @wait_thr.alive?)
165
+ end
166
+
167
+ def pid
168
+ @wait_thr&.pid
169
+ end
170
+
171
+ private
172
+
173
+ def write_message(msg_hash)
174
+ json = JSON.dump(msg_hash)
175
+ @mutex.synchronize do
176
+ raise ConnectionError, "Process stdin is closed" if @stdin.nil? || @stdin.closed?
177
+
178
+ @stdin.puts(json)
179
+ @stdin.flush
180
+ end
181
+ rescue IOError, Errno::EPIPE => e
182
+ close
183
+ raise ConnectionError, "Failed to write to MCP server: #{e.message}"
184
+ end
185
+
186
+ def wait_for_response(req_id, timeout: 10)
187
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
188
+ response = @mutex.synchronize do
189
+ loop do
190
+ match = @responses.delete(req_id) || @responses.delete(req_id.to_s) || @responses.delete(req_id.to_i)
191
+ break match if match
192
+ raise ConnectionError, "MCP transport stopped while waiting for response" unless @running
193
+
194
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
195
+ remaining = timeout - elapsed
196
+ if remaining <= 0
197
+ @responses.delete(req_id)
198
+ raise TimeoutError, "Timed out waiting for MCP response to request #{req_id} after #{timeout}s"
199
+ end
200
+
201
+ @cv.wait(@mutex, remaining)
202
+ end
203
+ end
204
+
205
+ if response["error"]
206
+ err = response["error"]
207
+ code = err.is_a?(Hash) ? err["code"] : nil
208
+ msg = err.is_a?(Hash) ? (err["message"] || err.to_s) : err.to_s
209
+ code_str = code ? " (#{code})" : ""
210
+ raise ProtocolError, "MCP error#{code_str}: #{msg}"
211
+ end
212
+
213
+ response["result"]
214
+ end
215
+
216
+ def start_reader_thread
217
+ @reader_thr = Thread.new do
218
+ while @running && @stdout && !@stdout.closed?
219
+ line = begin
220
+ @stdout.gets
221
+ rescue IOError, Errno::EBADF
222
+ nil
223
+ end
224
+ break unless line
225
+
226
+ line = line.strip
227
+ next if line.empty?
228
+
229
+ begin
230
+ msg = JSON.parse(line)
231
+ handle_incoming_message(msg)
232
+ rescue JSON::ParserError
233
+ # Ignore non-JSON server logs, keep reader alive
234
+ end
235
+ end
236
+ ensure
237
+ @mutex.synchronize do
238
+ @running = false
239
+ @cv.broadcast
240
+ end
241
+ end
242
+ end
243
+
244
+ def handle_incoming_message(msg)
245
+ return unless msg.is_a?(Hash) && msg.key?("id")
246
+
247
+ @mutex.synchronize do
248
+ @responses[msg["id"]] = msg
249
+ @cv.broadcast
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
data/lib/rubik/mcp.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mcp/errors"
4
+ require_relative "mcp/transport/stdio"
5
+ require_relative "mcp/client"
6
+ require_relative "mcp/proxy_tool"
7
+ require_relative "mcp/manager"
8
+
9
+ module Rubik
10
+ module MCP
11
+ end
12
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ # Plan 61 D-15: low-cost per-run ledger of tool usage, touched file
6
+ # paths and tool errors, rendered into a compact turn summary. Failed
7
+ # or cancelled turns persist this summary next to their [Turn ...]
8
+ # marker so the model (and !resume) know what was already attempted.
9
+ # Loop owns an instance and resets it per run; StepExecutor records.
10
+ class ActivityLog
11
+ def initialize
12
+ @tools = Hash.new(0)
13
+ @files = []
14
+ @tool_errors = 0
15
+ @entries = [] # YENİ (Plan 85 D-2): sıralı per-call kronoloji [{ tool:, status: }, ...]
16
+ end
17
+
18
+ def record(tool_name, tool_args, status)
19
+ @tools[tool_name] += 1
20
+ @entries << { tool: tool_name, status: status } # YENİ: sıra korunur
21
+ path = tool_args["path"] || tool_args[:path]
22
+ @files << path.to_s if path && @files.size < 10 && !@files.include?(path.to_s)
23
+ @tool_errors += 1 if status == :error
24
+ rescue StandardError => e
25
+ Rubik::Platform.debug_warn("ActivityLog#record failed (#{e.class}: #{e.message}); telemetry entry dropped")
26
+ nil
27
+ end
28
+
29
+ def summary(buffer_manager = nil)
30
+ parts = []
31
+ unless @tools.empty?
32
+ used = @tools.map { |name, count| count > 1 ? "#{name}(x#{count})" : name }
33
+ parts << "Tools used: #{used.join(', ')}"
34
+ end
35
+ parts << "Files touched: #{@files.join(', ')}" unless @files.empty?
36
+ parts << "Tool errors encountered: #{@tool_errors}" if @tool_errors.positive?
37
+ if buffer_manager&.dirty?
38
+ dirty = buffer_manager.dirty_files
39
+ names = dirty.is_a?(Hash) ? dirty.keys : Array(dirty)
40
+ parts << "Unapplied buffer changes: #{names.join(', ')}" unless names.empty?
41
+ end
42
+ parts.join("\n")
43
+ end
44
+
45
+ # YENİ (Plan 85 D-2): Tüm tool çağrılarını gerçek kronolojik sırada döndürür.
46
+ # Model context'ine enjekte için tasarlandı. @entries boşsa "" döner.
47
+ def chronological_summary(max_entries: 20)
48
+ return "" if @entries.empty?
49
+
50
+ lines = @entries.first(max_entries).map.with_index(1) do |entry, i|
51
+ status_tag = entry[:status] == :error ? " [FAILED]" : ""
52
+ " #{i}. #{entry[:tool]}#{status_tag}"
53
+ end
54
+ lines << " ... (#{@entries.size - max_entries} more)" if @entries.size > max_entries
55
+ lines.join("\n")
56
+ end
57
+
58
+ # Kök-turn kronik satırı için kompakt araç dökümü (root-turn planı
59
+ # 2026-10-04): alfabetik ilk max_tools araç "ad(xN)" — sıralama
60
+ # sabit olduğundan replay'de aynı dize; @tools boşsa "".
61
+ def tool_tally(max_tools: 6)
62
+ return "" if @tools.empty?
63
+
64
+ @tools.keys.sort.first(max_tools).map { |name| "#{name}(x#{@tools[name]})" }.join(" ")
65
+ end
66
+
67
+ def reset!
68
+ @tools.clear
69
+ @files.clear
70
+ @tool_errors = 0
71
+ @entries.clear # YENİ
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "subagent/notices"
4
+
5
+ module Rubik
6
+ module Orchestrator
7
+ # Deep module (2026-09-22 review card 2): the ONE home for the buffer
8
+ # auto-apply policy. Eight call sites carried private spellings of the
9
+ # same decision (StepExecutor x3, subagent StepRunner x2, Supervisor
10
+ # completion, REPL !test/!sh) and drifted: the supervisor ignored
11
+ # buffer.auto_apply, subagent finish ignored on_finish, both subagent
12
+ # sites swallowed apply failures (tests could silently run on stale
13
+ # disk), the subagent steps skipped the AST sync, and the copy
14
+ # diverged ("file(s)" vs "modified file(s)").
15
+ #
16
+ # owns : the trigger vocabulary and decision table (mode x subagent
17
+ # flag x tool class), the canonical apply+sync effect, the
18
+ # failure-notice channel, config keys/defaults, and the REPL
19
+ # copy vocabulary
20
+ # leaves : the transport at each site (on_step event, event bus, IO),
21
+ # which buffer instance is applied, the branch-workspace
22
+ # fact, and branch merge semantics (Supervisor)
23
+ #
24
+ # Subagent state never enters this module (stateless-over-owner,
25
+ # ARCHITECTURE.md §2): config and trigger symbols in, booleans and
26
+ # strings out.
27
+ module AutoApply
28
+ MODE_KEY = "buffer.auto_apply" # config/defaults.rb: "manual"
29
+ FLAG_KEY = "subagents.auto_apply_buffer" # config/defaults.rb: true
30
+
31
+ # Defensive defaults for config-less hosts (tests, embedded). The
32
+ # flag default deliberately diverges from defaults.rb's true: a nil
33
+ # config reads as OFF so a config-less Supervisor never writes disk
34
+ # (the merge-without-apply pin, test_subagent_orchestration.rb).
35
+ # Pinned by name in test_auto_apply_policy.rb — do not "unify".
36
+ DEFAULT_MODE = "manual"
37
+ DEFAULT_FLAG = false
38
+
39
+ TEST_CLASS_TOOLS = ["run_command", "run_tests", "test", "lint"].freeze
40
+
41
+ # Trigger -> modes that fire it. on_test fires at finish on purpose:
42
+ # the mode ladder ends every turn flushed (historical StepExecutor
43
+ # semantics, preserved by the 2026-09-22 reconciliation). completion
44
+ # mirrors finish for the supervisor's post-merge moment.
45
+ TRIGGER_MODES = {
46
+ pre_execution: ["on_test", "always"],
47
+ finish: ["on_finish", "on_test", "always"],
48
+ completion: ["on_finish", "on_test", "always"],
49
+ post_tool: ["always"]
50
+ }.freeze
51
+
52
+ module_function
53
+
54
+ # THE decision. subagent: true composes the subagents flag: flag on
55
+ # enables every trigger; otherwise the mode decides per trigger.
56
+ # tool classifies :pre_execution (nil = the caller already
57
+ # established the test-class context, e.g. REPL !test/!sh). mode:
58
+ # overrides config resolution entirely — the StepExecutor injection
59
+ # seam (its tests pass explicit modes positionally).
60
+ def apply?(config, trigger:, subagent: false, tool: nil, mode: nil)
61
+ return false if trigger == :pre_execution && !tool.nil? && !test_class_tool?(tool)
62
+
63
+ resolved = mode || (subagent && subagent_flag_on?(config) ? "always" : self.mode(config))
64
+ fires?(resolved, trigger)
65
+ end
66
+
67
+ # Pure predicate on a resolved mode string.
68
+ def fires?(mode, trigger)
69
+ TRIGGER_MODES.fetch(trigger).include?(mode.to_s.downcase)
70
+ end
71
+
72
+ # Raised when the disk apply succeeded but the context/AST index sync
73
+ # failed — the message says the changes ARE on disk so nobody
74
+ # "recovers" by re-applying a clean buffer.
75
+ class SyncFailedError < StandardError; end
76
+
77
+ # Canonical effect: apply the dirty buffer, then sync context/AST
78
+ # unless nothing was applied. Errors PROPAGATE (the main path
79
+ # contract); subagent callers rescue-and-report via report_failure.
80
+ def apply_and_sync(buffer_manager, shell)
81
+ return [] unless buffer_manager&.dirty?
82
+
83
+ applied = buffer_manager.apply!
84
+ begin
85
+ shell&.sync_context_and_ast(applied) unless applied.empty?
86
+ rescue StandardError => e
87
+ raise SyncFailedError, "index sync failed (changes ARE on disk): #{e.class}: #{e.message}"
88
+ end
89
+ applied
90
+ end
91
+
92
+ # Report-don't-swallow channel for the subagent paths. The trigger
93
+ # folds into reason; the cap keeps diagnostic color while preserving
94
+ # the TAIL — OS error messages carry the file path at the end, and a
95
+ # head-cut amputates exactly the actionable part. A failure channel
96
+ # must be the most defensive code on the path it protects: an emit
97
+ # that itself raises degrades to warn — raising here (from inside a
98
+ # caller's rescue) would mask the original apply error and, on the
99
+ # supervisor completion path, kill the COMPLETED_EVENT protocol.
100
+ REASON_CAP = 120
101
+
102
+ def report_failure(event_bus, id:, role:, state:, trigger:, reason:)
103
+ reason = capped_reason("#{trigger}: #{reason}")
104
+ begin
105
+ if event_bus.respond_to?(:emit)
106
+ Subagent::Notices.emit(event_bus, Subagent::Notices::AUTO_APPLY_FAILED_EVENT,
107
+ { id: id, role: role, state: state.to_s, reason: reason })
108
+ else
109
+ warn("[AutoApply] subagent '#{id}' auto-apply (#{trigger}) failed: #{reason}")
110
+ end
111
+ rescue StandardError => e
112
+ warn("[AutoApply] subagent '#{id}' auto-apply (#{trigger}) failed: #{reason} " \
113
+ "(notice emit also failed: #{e.class}: #{e.message})")
114
+ end
115
+ end
116
+
117
+ # apply! does not raise on an external-modification conflict — it
118
+ # skips the file and records it — but a skip means the next run_tests
119
+ # executes against disk that lacks the buffered edit: the same
120
+ # stale-disk failure report_failure exists for. Call immediately
121
+ # after apply_and_sync; the post-apply dirty? check distinguishes "a
122
+ # skip just happened" from "buffer was already clean" (apply! rewrites
123
+ # last_conflicts on every run).
124
+ def report_conflicts(event_bus, buffer_manager, id:, role:, state:, trigger:)
125
+ return unless buffer_manager&.dirty?
126
+
127
+ conflicts = buffer_manager.respond_to?(:last_conflicts) ? Array(buffer_manager.last_conflicts) : []
128
+ return if conflicts.empty?
129
+
130
+ report_failure(event_bus, id: id, role: role, state: state, trigger: trigger,
131
+ reason: "conflict skipped: #{conflicts.join(', ')}")
132
+ end
133
+
134
+ # -- copy vocabulary (the one home for auto-apply words) ------------
135
+
136
+ def applied_line(count, phase)
137
+ "[Rubik::Buffer] Auto-applied #{count} modified file(s) before #{phase}."
138
+ end
139
+
140
+ def unapplied_warning(subject)
141
+ "⚠️ [Rubik::Buffer] Notice: Unapplied buffer changes exist. #{subject} run against files on disk."
142
+ end
143
+
144
+ # -- one defensive spelling each (replaced eight) -------------------
145
+
146
+ def mode(config)
147
+ config.respond_to?(:fetch) ? config.fetch(MODE_KEY, DEFAULT_MODE).to_s.downcase : DEFAULT_MODE
148
+ end
149
+
150
+ def subagent_flag_on?(config)
151
+ config.respond_to?(:fetch) ? config.fetch(FLAG_KEY, DEFAULT_FLAG) == true : DEFAULT_FLAG
152
+ end
153
+
154
+ def test_class_tool?(canonical_name)
155
+ TEST_CLASS_TOOLS.include?(canonical_name.to_s)
156
+ end
157
+
158
+ def capped_reason(reason)
159
+ reason = reason.to_s
160
+ reason.length > REASON_CAP ? reason[-REASON_CAP..] : reason
161
+ end
162
+
163
+ private_class_method :capped_reason
164
+ end
165
+ end
166
+ end