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,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+ require_relative "../../coeffects"
5
+ require_relative "../output"
6
+ require_relative "../diagnostic_reporter"
7
+ require_relative "../fuzzy_matcher"
8
+
9
+ module Rubik
10
+ module Tools
11
+ class SearchSymbolsTool < BaseTool
12
+ # Declared coeffects: hidden from schemas (PENDING) until satisfied (AGENTS.md §2).
13
+ REQUIRES = [Rubik::Coeffects::AST_GRAPH].freeze
14
+
15
+ DECLARATION = Declaration.new(
16
+ name: "search_symbols", aliases: ["find_symbols", "symbols"],
17
+ description: "Search function, class, struct definitions and call hierarchies in the AST symbol graph",
18
+ risk: :read, archetypes: [:research],
19
+ params_schema: {
20
+ type: "object",
21
+ properties: {
22
+ query: { type: "string", description: "Symbol name or partial name to search" },
23
+ smart_context: { type: "boolean", description: "Return full smart context with dependencies (optional)" }
24
+ },
25
+ required: ["query"]
26
+ }
27
+ )
28
+ def initialize(ast_graph = nil, runtime_context: nil, context: nil)
29
+ super(DECLARATION)
30
+ @ast_graph = ast_graph
31
+ @runtime_context = runtime_context || context
32
+ end
33
+
34
+ def ast_graph
35
+ Rubik::Coeffects.resolve(@runtime_context, Rubik::Coeffects::AST_GRAPH, @ast_graph)
36
+ end
37
+
38
+ def execute(args = {})
39
+ query = (args["query"] || args[:query]).to_s.strip
40
+ return ToolOutput.error("Query parameter is required.") if query.empty?
41
+
42
+ graph = ast_graph
43
+ return ToolOutput.error("AST graph is not available. Run '!context scan' to build the symbol index.") unless graph
44
+
45
+ if args["smart_context"] || args[:smart_context]
46
+ ctx = graph.smart_context_for(query)
47
+ return ctx.empty? ? "No smart context found for '#{query}'." : ctx
48
+ end
49
+
50
+ matches = if graph.respond_to?(:search_symbols)
51
+ graph.search_symbols(query)
52
+ else
53
+ graph.symbols.values.select { |sym| sym.name.downcase.include?(query.downcase) }
54
+ end
55
+
56
+ if matches.empty?
57
+ all_sym_names = graph.respond_to?(:symbols) ? graph.symbols.keys : []
58
+ candidates = FuzzyMatcher.best_matches(query, all_sym_names)
59
+ DiagnosticReporter.render_symbol_not_found(query, candidate_symbols: candidates)
60
+ else
61
+ matches.map do |sym|
62
+ "• #{sym.kind.to_s.upcase} #{sym.name} (#{sym.file}:#{sym.line_start}-#{sym.line_end})\n " \
63
+ "Signature: #{sym.signature}\n Calls: #{sym.calls.join(', ')}"
64
+ end.join("\n\n")
65
+ end
66
+ end
67
+ end
68
+
69
+ class GetOutlineTool < BaseTool
70
+ # Declared coeffects: hidden from schemas (PENDING) until satisfied (AGENTS.md §2).
71
+ REQUIRES = [Rubik::Coeffects::AST_GRAPH].freeze
72
+
73
+ DECLARATION = Declaration.new(
74
+ name: "get_outline", aliases: ["file_outline", "outline"],
75
+ description: "Get high-level semantic outline (classes, methods, functions, and line ranges) " \
76
+ "of a source file from the AST without reading entire file content. " \
77
+ "Accepts 'file_path' (or 'path').",
78
+ risk: :read, archetypes: [:research],
79
+ params_schema: {
80
+ type: "object",
81
+ properties: {
82
+ file_path: { type: "string", description: "Path to source file to inspect" }
83
+ },
84
+ required: ["file_path"]
85
+ }
86
+ )
87
+ def initialize(ast_graph = nil, runtime_context: nil, context: nil)
88
+ super(DECLARATION)
89
+ @ast_graph = ast_graph
90
+ @runtime_context = runtime_context || context
91
+ end
92
+
93
+ def ast_graph
94
+ Rubik::Coeffects.resolve(@runtime_context, Rubik::Coeffects::AST_GRAPH, @ast_graph)
95
+ end
96
+
97
+ def execute(args = {})
98
+ path = (args["file_path"] || args[:file_path] || args["path"] || args[:path]).to_s.strip
99
+ return ToolOutput.error("File path is required.") if path.empty?
100
+
101
+ graph = ast_graph
102
+ return ToolOutput.error("AST graph is not available. Run '!context scan' to build the symbol index.") unless graph
103
+
104
+ graph.outline_for(path)
105
+ end
106
+ end
107
+
108
+ class FindReferencesTool < BaseTool
109
+ # Declared coeffects: hidden from schemas (PENDING) until satisfied (AGENTS.md §2).
110
+ REQUIRES = [Rubik::Coeffects::AST_GRAPH].freeze
111
+
112
+ DECLARATION = Declaration.new(
113
+ name: "find_references", aliases: ["references", "get_callers"],
114
+ description: "Find all call sites, callers, and references of a symbol/function/class across the workspace for safe refactoring. " \
115
+ "This is based on the AST symbol graph; for a catch-all text search matching names, use grep_search. " \
116
+ "Accepts 'symbol_name' (or 'symbol').",
117
+ risk: :read, archetypes: [:research],
118
+ params_schema: {
119
+ type: "object",
120
+ properties: {
121
+ symbol_name: { type: "string", description: "Symbol name to find callers/references for" }
122
+ },
123
+ required: ["symbol_name"]
124
+ }
125
+ )
126
+ def initialize(ast_graph = nil, runtime_context: nil, context: nil)
127
+ super(DECLARATION)
128
+ @ast_graph = ast_graph
129
+ @runtime_context = runtime_context || context
130
+ end
131
+
132
+ def ast_graph
133
+ Rubik::Coeffects.resolve(@runtime_context, Rubik::Coeffects::AST_GRAPH, @ast_graph)
134
+ end
135
+
136
+ def execute(args = {})
137
+ sym_name = (args["symbol_name"] || args[:symbol_name] || args["symbol"] || args[:symbol]).to_s.strip
138
+ return ToolOutput.error("Symbol name is required.") if sym_name.empty?
139
+
140
+ graph = ast_graph
141
+ return ToolOutput.error("AST graph is not available. Run '!context scan' to build the symbol index.") unless graph
142
+
143
+ callers = graph.callers_of(sym_name)
144
+ if callers.empty?
145
+ "No references or callers found for '#{sym_name}' in AST graph."
146
+ else
147
+ lines = ["Found #{callers.size} reference(s) for '#{sym_name}':"]
148
+ callers.each do |c|
149
+ lines << " • #{c.name} (#{c.file}:#{c.line_start}-#{c.line_end}) -> #{c.signature}"
150
+ end
151
+ lines.join("\n")
152
+ end
153
+ end
154
+ end
155
+
156
+ class GetSymbolTool < BaseTool
157
+ # Declared coeffects: hidden from schemas (PENDING) until satisfied (AGENTS.md §2).
158
+ REQUIRES = [Rubik::Coeffects::AST_GRAPH].freeze
159
+
160
+ DECLARATION = Declaration.new(
161
+ name: "get_symbol", aliases: ["get_definition", "symbol_info"],
162
+ description: "Get isolated definition, signature, docstring, and body of a specific class or method from the AST. " \
163
+ "Accepts 'symbol_name' (or 'symbol').",
164
+ risk: :read, archetypes: [:research],
165
+ params_schema: {
166
+ type: "object",
167
+ properties: {
168
+ symbol_name: { type: "string", description: "Symbol name to retrieve definition for" }
169
+ },
170
+ required: ["symbol_name"]
171
+ }
172
+ )
173
+ def initialize(ast_graph = nil, runtime_context: nil, context: nil)
174
+ super(DECLARATION)
175
+ @ast_graph = ast_graph
176
+ @runtime_context = runtime_context || context
177
+ end
178
+
179
+ def ast_graph
180
+ Rubik::Coeffects.resolve(@runtime_context, Rubik::Coeffects::AST_GRAPH, @ast_graph)
181
+ end
182
+
183
+ def execute(args = {})
184
+ sym_name = (args["symbol_name"] || args[:symbol_name] || args["symbol"] || args[:symbol]).to_s.strip
185
+ return ToolOutput.error("Symbol name is required.") if sym_name.empty?
186
+
187
+ graph = ast_graph
188
+ return ToolOutput.error("AST graph is not available. Run '!context scan' to build the symbol index.") unless graph
189
+
190
+ info = graph.symbol_definition(sym_name)
191
+ unless info
192
+ all_sym_names = graph.respond_to?(:symbols) ? graph.symbols.keys : []
193
+ candidates = FuzzyMatcher.best_matches(sym_name, all_sym_names)
194
+ return ToolOutput.error(DiagnosticReporter.render_symbol_not_found(sym_name, candidate_symbols: candidates))
195
+ end
196
+
197
+ lines = [
198
+ "Symbol: #{info[:name]} [#{info[:kind].to_s.upcase}]",
199
+ "File: #{info[:file]} (lines #{info[:line_start]}-#{info[:line_end]})",
200
+ "Signature: #{info[:signature]}"
201
+ ]
202
+ lines << "Docstring: #{info[:docstring]}" if info[:docstring] && !info[:docstring].empty?
203
+ lines << "Calls: #{info[:calls].join(', ')}" unless info[:calls].empty?
204
+ lines << "\n--- Body ---\n#{info[:body]}" if info[:body] && !info[:body].empty?
205
+ lines.join("\n")
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "timeout"
5
+ require_relative "../base"
6
+ require_relative "../output"
7
+ require_relative "../preflight_runner"
8
+ require_relative "../../runtime"
9
+
10
+ module Rubik
11
+ module Tools
12
+ class SynthesizeCommandTool < BaseTool
13
+ DECLARATION = Declaration.new(
14
+ name: "synthesize_command", aliases: ["create_command", "add_command"],
15
+ description: "Dynamically create and register a new Rubik REPL bang command or tool at runtime (session, project, or global scope)",
16
+ risk: :write, maestro_blocked: false, # self-evolution plane (TOOLS.md maestro policy)
17
+ params_schema: {
18
+ type: "object",
19
+ properties: {
20
+ name: { type: "string", description: "Name of the new command without '!' (e.g. 'db_seed', 'benchmark_api')" },
21
+ description: { type: "string", description: "Short description of what the command does for !help" },
22
+ scope: { type: "string", enum: ["session", "project", "global"],
23
+ description: "Storage scope: session (in-memory only), project (.rubik/commands), global (~/.rubik/commands)" },
24
+ code: { type: "string", description: "Ruby handler code: ->(args, ctx) { ... }. For 2-arity, ctx is the OUTPUT VIEW " \
25
+ "(ctx.out / ctx.puts / ctx.print / ctx.flush). For config/services/full shell, " \
26
+ "declare 3-arity ->(args, out, shell) instead." },
27
+ test_args: { type: "object", description: "Optional sample arguments to pre-flight verify the handler before registration (recommended)" }
28
+ },
29
+ required: ["name", "code"]
30
+ }
31
+ )
32
+ def initialize(shell = nil, overlay_storage = nil, guard = nil, legacy_runtime_manager = nil, runtime_manager: nil)
33
+ super(DECLARATION)
34
+ @shell = shell
35
+ # guard, fallback overlay storage'a da geçilir: bootstrap kabul taraması
36
+ # aynı operatör-policy guard örneğini kullanır (bkz. OverlayStorage).
37
+ @guard = guard || Rubik::Security::Guard.new
38
+ @overlay_storage = overlay_storage || Rubik::Runtime::OverlayStorage.new(guard: @guard)
39
+ @runtime_manager = runtime_manager || legacy_runtime_manager
40
+ end
41
+
42
+ def execute(args = {})
43
+ name = (args["name"] || args[:name]).to_s.strip.delete_prefix("!")
44
+ description = (args["description"] || args[:description] || "Custom dynamic command").to_s.strip
45
+ scope = (args["scope"] || args[:scope] || "session").to_s.downcase.to_sym
46
+ code = (args["code"] || args[:code]).to_s.strip
47
+ test_args = args["test_args"] || args[:test_args]
48
+
49
+ return ToolOutput.error("Command name cannot be empty") if name.empty?
50
+ return ToolOutput.error("Handler code cannot be empty") if code.empty?
51
+
52
+ clean_name = name.to_s.downcase.delete_prefix("!")
53
+ previous_meta = nil
54
+ if @shell
55
+ previous_meta = @shell.commands[clean_name]
56
+ return ToolOutput.error("Cannot overwrite reserved built-in command '!#{clean_name}'") if previous_meta && !previous_meta[:custom]
57
+ end
58
+
59
+ # 1. Security & Syntax Verification
60
+ begin
61
+ Rubik::Security::Guard.run_synthesis_gate(@guard, code)
62
+ rescue Rubik::Security::BlockedWriteError => e
63
+ return ToolOutput.error("Security blocked: #{e.message}")
64
+ rescue StandardError => e
65
+ return ToolOutput.error("Security blocked: Synthesized command code contains dangerous operations (#{e.message})")
66
+ end
67
+
68
+ proc_handler = begin
69
+ eval(code, clean_binding, __FILE__, __LINE__) # rubocop:disable Security/Eval -- sentez çekirdegi; @guard.verify_synthesis_safety! ön kosulu çalisir
70
+ rescue ScriptError, StandardError => e
71
+ return ToolOutput.error("Syntax error in synthesized code: #{e.message}") # rubocop:disable Lint/NoReturnInBeginEndBlocks -- araç hata akisinda bilinçli erken dönüş
72
+ end
73
+
74
+ unless proc_handler.respond_to?(:call)
75
+ return ToolOutput.error("Code must evaluate to a callable Proc or Lambda " \
76
+ "(e.g. ->(args, ctx) { ... } or ->(args, out, shell) { ... })")
77
+ end
78
+
79
+ # 1b. Optional pre-flight execution in an isolated child process
80
+ # (AGENTS.md §3.4): verifies the handler actually runs before it is
81
+ # persisted or registered. A subprocess guarantees the deadline even
82
+ # for C extensions and blocking syscalls that Timeout.timeout cannot
83
+ # preempt; output goes to a throwaway StringIO inside the child so
84
+ # verification never writes to the user's terminal.
85
+ if test_args
86
+ preflight = PreflightRunner.run(code, test_args, mode: :command, guard: @guard)
87
+ return ToolOutput.error("Pre-flight verification failed for command '!#{name}': #{preflight}") if preflight.is_a?(String)
88
+
89
+ if preflight == :spawn_failed
90
+ sub_ctx = @runtime_manager&.context.is_a?(Rubik::Runtime::Context) ? @runtime_manager.context.extend : nil
91
+ begin
92
+ failure = PreflightRunner.verify_in_process { PreflightRunner.call_handler(proc_handler, test_args, mode: :command, shell: @shell) }
93
+ return ToolOutput.error("Pre-flight verification failed for command '!#{name}': #{failure}") if failure
94
+ ensure
95
+ sub_ctx&.dispose
96
+ end
97
+ end
98
+ end
99
+
100
+ # 2. Persist to storage if scope is project or global
101
+ if [:project, :global].include?(scope)
102
+ save_res = @overlay_storage.save_command(name, code, scope: scope, description: description)
103
+ return ToolOutput.error("Failed to persist command: #{save_res[:error]}") unless save_res[:ok]
104
+ end
105
+
106
+ # 3. Register live in REPL shell
107
+ if @shell
108
+ @shell.register_command(name, description: "#{description} [#{scope}]", custom: true) do |cmd_args, ctx|
109
+ # Arity contract (mirrored by OverlayStorage#eval_command and
110
+ # PreflightRunner): 1 → args; 2 → the CommandContext (its
111
+ # puts/print/flush delegate to the active capture IO); 3 →
112
+ # (args, out, shell) for trusted code needing the raw shell.
113
+ # Direct invocation outside dispatch (tests, replay) has no
114
+ # ctx: use the shell's factory, or its raw output when held
115
+ # shell doubles predate the CommandContext contract. The warn
116
+ # keeps a dispatch site forgetting ctx visible instead of
117
+ # silently auto-healed.
118
+ out_ctx = if ctx
119
+ ctx
120
+ elsif @shell.respond_to?(:command_context)
121
+ warn "[rubik] custom command invoked without a dispatch CommandContext" unless ENV["RUBIK_TEST"]
122
+ @shell.command_context
123
+ else
124
+ @shell.output
125
+ end
126
+ if proc_handler.arity == 1
127
+ proc_handler.call(cmd_args)
128
+ elsif proc_handler.arity == 2
129
+ proc_handler.call(cmd_args, out_ctx)
130
+ else
131
+ proc_handler.call(cmd_args, out_ctx.respond_to?(:out) ? out_ctx.out : out_ctx, @shell)
132
+ end
133
+ end
134
+
135
+ # Register inverse in Runtime Context. Sound rollback (AGENTS.md §1.A):
136
+ # a hot-updated custom command is restored to its previous handler on
137
+ # dispose instead of leaving a hole in the command table.
138
+ ctx = @runtime_manager&.context || (@shell.respond_to?(:runtime_manager) ? @shell.runtime_manager&.context : nil)
139
+ if ctx.respond_to?(:on_dispose) || ctx.respond_to?(:register_disposable)
140
+ hook = ctx.respond_to?(:on_dispose) ? :on_dispose : :register_disposable
141
+ ctx.send(hook) do
142
+ if previous_meta
143
+ @shell.register_command(clean_name, description: previous_meta[:description],
144
+ custom: previous_meta[:custom], audience: previous_meta[:audience],
145
+ &previous_meta[:handler])
146
+ else
147
+ @shell.unregister_command(name)
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ "Successfully synthesized and registered !#{name} [scope: #{scope}]#{' (pre-flight verified)' if test_args}. " \
154
+ "You can now run it using !#{name} or via exec_repl_command('!#{name}')."
155
+ end
156
+
157
+ private
158
+
159
+ def clean_binding
160
+ Object.new.instance_eval { binding }
161
+ end
162
+ end
163
+
164
+ class SynthesizeToolTool < BaseTool
165
+ DECLARATION = Declaration.new(
166
+ name: "synthesize_tool", aliases: ["create_tool", "add_tool"],
167
+ description: "Synthesize, verify, and register a new native function calling tool into the agent's live toolbelt " \
168
+ "at runtime (session, project, or global scope)",
169
+ risk: :write, maestro_blocked: false, # self-evolution plane (TOOLS.md maestro policy)
170
+ params_schema: {
171
+ type: "object",
172
+ properties: {
173
+ name: { type: "string", description: "Name of the new tool (e.g. 'calc_stats', 'inspect_ast_node')" },
174
+ description: { type: "string", description: "Clear explanation of what the tool does for model selection" },
175
+ scope: { type: "string", enum: ["session", "project", "global"],
176
+ description: "Storage scope: session (in-memory only), project (.rubik/tools), global (~/.rubik/tools)" },
177
+ risk: { type: "string", enum: ["read", "write", "execute"], default: "execute",
178
+ description: "Risk level of tool (optional; read/write/execute, default: execute)" },
179
+ params_schema: { type: "object", description: "JSON Schema definition of the tool parameters" },
180
+ code: { type: "string", description: "Ruby implementation of tool handler: ->(args) { ... } or class extending Rubik::Tools::BaseTool" },
181
+ test_args: { type: "object", description: "Optional test arguments to perform pre-flight verification in an isolated sandbox" }
182
+ },
183
+ required: ["name", "description", "code"]
184
+ }
185
+ )
186
+ def initialize(tool_registry = nil, overlay_storage = nil, guard = nil, legacy_runtime_manager = nil, runtime_manager: nil)
187
+ super(DECLARATION)
188
+ @tool_registry = tool_registry
189
+ # guard, fallback overlay storage'a da geçilir: bootstrap kabul taraması
190
+ # aynı operatör-policy guard örneğini kullanır (bkz. OverlayStorage).
191
+ @guard = guard || Rubik::Security::Guard.new
192
+ @overlay_storage = overlay_storage || Rubik::Runtime::OverlayStorage.new(guard: @guard)
193
+ @runtime_manager = runtime_manager || legacy_runtime_manager
194
+ end
195
+
196
+ def execute(args = {})
197
+ name = (args["name"] || args[:name]).to_s.strip.downcase.gsub(/[^a-z0-9_]/, "_")
198
+ description = (args["description"] || args[:description] || "Custom native tool").to_s.strip
199
+ scope = (args["scope"] || args[:scope] || "session").to_s.downcase.to_sym
200
+ risk = (args["risk"] || args[:risk] || "execute").to_s.downcase.to_sym
201
+ return ToolOutput.error("Invalid risk '#{risk}' — must be one of: read, write, execute.") unless [:read, :write, :execute].include?(risk)
202
+
203
+ schema = args["params_schema"] || args[:params_schema] || { type: "object", properties: {} }
204
+ code = (args["code"] || args[:code]).to_s.strip
205
+ test_args = args["test_args"] || args[:test_args]
206
+
207
+ return ToolOutput.error("Tool name cannot be empty") if name.empty?
208
+ return ToolOutput.error("Tool code cannot be empty") if code.empty?
209
+
210
+ # Guard against shadowing built-in tools. Registry#register silently
211
+ # overwrites, and the dispose inverse only unregisters — a synthesized
212
+ # "read_file" would leave the session without the real read_file after
213
+ # disposal. Built-ins are rejected here; overlay/MCP/synthesized tools
214
+ # may be hot-updated (previous instance is restored on dispose).
215
+ previous_tool = @tool_registry&.get(name)
216
+ if previous_tool && @tool_registry.origin(name) == :builtin
217
+ msg = "Cannot overwrite built-in tool '#{name}'. Choose a distinct name (e.g. 'my_#{name}')."
218
+ return ToolOutput.error(msg)
219
+ end
220
+
221
+ # 1. Security & Pre-flight Syntax Verification in isolated Context
222
+ begin
223
+ Rubik::Security::Guard.run_synthesis_gate(@guard, code)
224
+ rescue Rubik::Security::BlockedWriteError => e
225
+ return ToolOutput.error("Security blocked: #{e.message}")
226
+ rescue StandardError => e
227
+ return ToolOutput.error("Security blocked: Synthesized tool code contains dangerous operations (#{e.message})")
228
+ end
229
+
230
+ tool_instance = nil
231
+ begin
232
+ evaluated = eval(code, clean_binding, __FILE__, __LINE__) # rubocop:disable Security/Eval -- sentez çekirdegi
233
+ tool_instance = if evaluated.is_a?(Class) && evaluated < Rubik::Tools::BaseTool
234
+ evaluated.new
235
+ elsif evaluated.respond_to?(:call)
236
+ Class.new(Rubik::Tools::BaseTool) do
237
+ define_method(:initialize) do
238
+ super(name: name, description: "#{description} [#{scope}]", risk: risk, params_schema: schema)
239
+ @handler = evaluated
240
+ end
241
+
242
+ define_method(:execute) do |exec_args = {}|
243
+ @handler.call(exec_args)
244
+ end
245
+ end.new
246
+ else
247
+ return ToolOutput.error("Tool code must evaluate to a callable Proc (->(args) { ... }) or a BaseTool subclass.")
248
+ end
249
+
250
+ effective_test_args = test_args || {}
251
+ preflight = PreflightRunner.run(code, effective_test_args, mode: :tool, guard: @guard)
252
+ return ToolOutput.error("Pre-flight verification failed for tool '#{name}': #{preflight}") if preflight.is_a?(String)
253
+
254
+ if preflight == :spawn_failed
255
+ # Fallback: in-process Timeout (lambda form via the shared
256
+ # dispatch; BaseTool-subclass form needs the full runtime and
257
+ # always verifies in-process).
258
+ sub_ctx = @runtime_manager&.context.respond_to?(:extend) ? @runtime_manager.context.extend : nil
259
+ begin
260
+ failure = if evaluated.respond_to?(:call)
261
+ PreflightRunner.verify_in_process { PreflightRunner.call_handler(evaluated, effective_test_args, mode: :tool) }
262
+ else
263
+ PreflightRunner.verify_in_process { tool_instance.execute(effective_test_args) }
264
+ end
265
+ return ToolOutput.error("Pre-flight verification failed for tool '#{name}': #{failure}") if failure
266
+ ensure
267
+ sub_ctx&.dispose! if sub_ctx.respond_to?(:dispose!)
268
+ end
269
+ end
270
+ rescue ScriptError, StandardError => e
271
+ return ToolOutput.error("Pre-flight verification failed for tool '#{name}': #{e.message}")
272
+ end
273
+
274
+ # 2. Persist to storage if scope is project or global
275
+ if [:project, :global].include?(scope)
276
+ save_res = @overlay_storage.save_tool(name, code, scope: scope, description: description, params_schema: schema, risk: risk)
277
+ return ToolOutput.error("Failed to persist tool: #{save_res[:error]}") unless save_res[:ok]
278
+ end
279
+
280
+ # 3. Register live into active tool registry
281
+ if @tool_registry
282
+ previous_origin = previous_tool ? @tool_registry.origin(name) : nil
283
+ @tool_registry.register(tool_instance, origin: :synthesis)
284
+
285
+ # Register inverse in Runtime Context. When hot-updating an existing
286
+ # overlay/MCP/synthesized tool, the inverse restores the previous
287
+ # instance instead of leaving a hole in the toolbelt.
288
+ ctx = @runtime_manager&.context
289
+ ctx&.on_dispose do
290
+ if previous_tool && @tool_registry.respond_to?(:swap)
291
+ @tool_registry.swap(name, previous_tool, origin: previous_origin || :overlay)
292
+ else
293
+ @tool_registry.unregister(name)
294
+ end
295
+ end
296
+ end
297
+
298
+ verification_note = if test_args
299
+ "Pre-flight verified with provided test_args in an isolated sub-context."
300
+ else
301
+ "Pre-flight verified with default arguments in an isolated sub-context."
302
+ end
303
+ "🎉 Successfully synthesized and registered native tool '#{name}' [scope: #{scope}]. " \
304
+ "#{verification_note} It is now immediately available in your active toolbelt for function calling."
305
+ end
306
+
307
+ private
308
+
309
+ def clean_binding
310
+ Object.new.instance_eval { binding }
311
+ end
312
+ end
313
+ end
314
+ end