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,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "yaml/store"
5
+ require "fileutils"
6
+
7
+ module Rubik
8
+ class Config
9
+ module Store
10
+ def global_config_path
11
+ File.join(@home_dir, ".rubik", "config.yml")
12
+ end
13
+
14
+ def project_config_path
15
+ dot_rubik_config = File.join(@root_dir, ".rubik", "config.yml")
16
+ return dot_rubik_config if File.exist?(dot_rubik_config)
17
+
18
+ root_rubik_yml = File.join(@root_dir, "rubik.yml")
19
+ return root_rubik_yml if File.exist?(root_rubik_yml)
20
+
21
+ dot_rubik_config
22
+ end
23
+
24
+ def store_for(scope = :project)
25
+ scope_sym = scope.to_s.downcase.to_sym
26
+ path = case scope_sym
27
+ when :project
28
+ project_config_path
29
+ when :global
30
+ global_config_path
31
+ else
32
+ scope.to_s
33
+ end
34
+
35
+ @stores ||= {}
36
+ @stores[path] ||= begin
37
+ begin
38
+ FileUtils.mkdir_p(File.dirname(path))
39
+ rescue SystemCallError => e
40
+ warn "[WARN] Failed to create config directory #{File.dirname(path)}: #{e.message}" if ENV["RUBIK_DEBUG"]
41
+ end
42
+ YAML::Store.new(path, true)
43
+ end
44
+ end
45
+
46
+ def transaction(scope = :project, read_only: false, &)
47
+ store = store_for(scope)
48
+ store.transaction(read_only, &)
49
+ end
50
+
51
+ # Persists configuration to the targeted scope (:global or :project) on disk.
52
+ # Semantics of deep_merge: in-memory @data is deep_merged over disk YAML to keep
53
+ # unmanaged keys, while provider sections ("models"/"model") are overwritten to
54
+ # clear stale specs. Both source and merged data undergo schema validation.
55
+ # `deletions` are dot-paths removed from the merged snapshot after the merge —
56
+ # deep_merge deliberately keeps disk-only keys, so a nested delete must be
57
+ # replayed here or the disk copy would resurrect it.
58
+ def save!(scope = :global, deletions: [])
59
+ return self if scope.nil? || scope == false || scope.to_s.downcase == "session" || scope.to_s.downcase == "none"
60
+
61
+ scope_sym = scope.to_s.downcase.to_sym == :project ? :project : :global
62
+ target_path = scope_sym == :project ? project_config_path : global_config_path
63
+ begin
64
+ FileUtils.mkdir_p(File.dirname(target_path))
65
+ rescue SystemCallError
66
+ nil
67
+ end
68
+ store = store_for(scope_sym)
69
+
70
+ validation_error = nil
71
+ store.transaction do
72
+ # Read existing data on disk
73
+ existing = store.roots.to_h { |k| [k, store[k]] }
74
+ merged = deep_merge(existing, deep_stringify_keys(@data))
75
+
76
+ # Explicitly overwrite model and role specs so stale keys from prior providers are not retained
77
+ if @data["models"].is_a?(Hash)
78
+ merged["models"] ||= {}
79
+ @data["models"].each do |r_name, r_spec|
80
+ merged["models"][r_name.to_s] = deep_dup(r_spec)
81
+ end
82
+ end
83
+ merged["model"] = deep_dup(@data["model"]) if @data["model"].is_a?(Hash)
84
+
85
+ # Replay nested deletions so validate_schema sees what will be written.
86
+ # A blocked path means the deletion cannot reach disk and would
87
+ # silently resurrect on the next load — fail the save like a
88
+ # schema violation.
89
+ unapplied = Array(deletions).select { |path| deep_delete(merged, path) == :blocked }
90
+ validation_error = "could not apply deletion(s): #{unapplied.join(', ')}" if unapplied.any?
91
+
92
+ # Schema validation
93
+ unless validation_error
94
+ begin
95
+ validate_schema(@data)
96
+ validate_schema(merged)
97
+ rescue ArgumentError => e
98
+ validation_error = e.message
99
+ end
100
+ end
101
+ store.abort if validation_error
102
+
103
+ # Remove deleted roots
104
+ (store.roots - merged.keys).each { |r| store.delete(r) }
105
+
106
+ # Save merged entries
107
+ merged.each { |k, v| store[k] = v }
108
+ end
109
+
110
+ if validation_error
111
+ { ok: false, error: validation_error }
112
+ else
113
+ if File.exist?(target_path)
114
+ begin
115
+ File.chmod(0o600, target_path)
116
+ rescue
117
+ nil
118
+ end
119
+ end
120
+ { ok: true, path: target_path, scope: scope_sym }
121
+ end
122
+ rescue StandardError => e
123
+ { ok: false, error: e.message }
124
+ end
125
+
126
+ private
127
+
128
+ def ensure_rubik_directories
129
+ rubik_home = File.join(@home_dir, ".rubik")
130
+ ["snapshots", "sessions", "commands", "tools"].each do |sub|
131
+ FileUtils.mkdir_p(File.join(rubik_home, sub))
132
+ end
133
+ rescue SystemCallError => e
134
+ @warnings << "Could not create ~/.rubik directory: #{e.message}"
135
+ end
136
+
137
+ def load_yaml(path)
138
+ return {} unless File.exist?(path)
139
+
140
+ store = store_for(path)
141
+ data = store.transaction(true) do
142
+ store.roots.to_h { |root| [root, store[root]] }
143
+ end
144
+ data.is_a?(Hash) ? deep_stringify_keys(data) : {}
145
+ rescue Psych::Exception, SystemCallError => e
146
+ # Psych::Exception, not just SyntaxError: YAML::Store#load safe_loads
147
+ # without aliases or Date, so a hand-edited "x: 2026-01-01" raises
148
+ # Psych::DisallowedClass and must still boot with defaults + warning.
149
+ warn_msg = "Cannot load config because #{e.message}. To fix, verify valid YAML syntax in #{path}."
150
+ @warnings << warn_msg
151
+ {}
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config/defaults"
4
+ require_relative "config/deep_ops"
5
+ require_relative "config/store"
6
+ require_relative "config/model_catalog"
7
+
8
+ module Rubik
9
+ class Config
10
+ include Defaults
11
+ include DeepOps
12
+ include Store
13
+ include ModelCatalog
14
+
15
+ DEFAULT_CONFIG = Defaults::DEFAULT_CONFIG
16
+
17
+ attr_reader :data, :warnings, :root_dir, :home_dir
18
+ attr_accessor :event_bus
19
+
20
+ def initialize(root_dir: Dir.pwd, home_dir: nil)
21
+ @root_dir = File.expand_path(root_dir)
22
+ effective_home = home_dir || (if Rubik.const_defined?(:Platform)
23
+ Rubik::Platform.home_dir
24
+ else
25
+ begin
26
+ Dir.home
27
+ rescue ArgumentError
28
+ (ENV["USERPROFILE"] || ENV["HOME"] || ".")
29
+ end
30
+ end)
31
+ @home_dir = File.expand_path(effective_home)
32
+ @warnings = []
33
+ @stores = {}
34
+ @listeners = []
35
+ @event_bus = nil
36
+ ensure_rubik_directories
37
+ @data = load_merged_config
38
+ end
39
+
40
+ def on_change(prefix = nil, &block)
41
+ return proc {} unless block_given?
42
+
43
+ norm_prefix = prefix&.to_s&.sub(/\.\z/, "")
44
+ entry = { prefix: norm_prefix, callback: block }
45
+ @listeners << entry
46
+ proc { @listeners.delete(entry) }
47
+ end
48
+
49
+ # Retrieves a configuration value by dot-separated key path.
50
+ def fetch(key, default = nil)
51
+ keys = key.to_s.split(".")
52
+ keys.reduce(@data) do |acc, k|
53
+ return default unless acc.is_a?(Hash) && acc.key?(k)
54
+
55
+ acc[k]
56
+ end
57
+ end
58
+
59
+ def [](key)
60
+ fetch(key)
61
+ end
62
+
63
+ # Sets a configuration value at a dot-separated key path, optionally
64
+ # persisting to disk. Returns the persist outcome — {ok: true, path:,
65
+ # scope:} after a disk write, {ok: true} for session-only changes,
66
+ # {ok: false, error:, reason: :persist_failed} when the write failed —
67
+ # so callers report honestly instead of assuming success.
68
+ def set(key, value, persist: nil)
69
+ old_val = fetch(key)
70
+ typed_val = parse_scalar_value(value)
71
+ keys = key.to_s.split(".")
72
+ last_key = keys.pop
73
+ target = keys.reduce(@data) do |acc, k|
74
+ acc[k] ||= {}
75
+ acc[k]
76
+ end
77
+ target[last_key] = typed_val
78
+
79
+ res = persisting?(persist) ? persist_result(save!(persist)) : { ok: true }
80
+ res[:reason] = :persist_failed unless res[:ok]
81
+ notify_change(key, typed_val, old_val) if old_val != typed_val
82
+ res
83
+ end
84
+
85
+ # Removes the dot-path key from configuration, persisting like set.
86
+ # Deleting an absent key is a successful no-op. Returns the persist
87
+ # outcome (see set); notifies no listener — deletion callers refresh
88
+ # the runtime explicitly.
89
+ def delete(key, persist: nil)
90
+ blocked = deep_delete(@data, key) == :blocked
91
+ return { ok: false, error: "cannot delete '#{key}': path traverses a non-hash value" } if blocked
92
+
93
+ res = persisting?(persist) ? persist_result(save!(persist, deletions: [key])) : { ok: true }
94
+ res[:reason] = :persist_failed unless res[:ok]
95
+ res
96
+ end
97
+
98
+ # True only for scopes save! writes to disk; nil/false/:session/"session"/
99
+ # "none" (downcased — save! itself is case-sensitive, this is not) never
100
+ # touch disk. Public so consumers wiring persist guards agree with Config.
101
+ def persisting?(scope)
102
+ return false if scope.nil? || scope == false
103
+
104
+ !["session", "none"].include?(scope.to_s.downcase)
105
+ end
106
+
107
+ def parse_scalar_value(val)
108
+ return val unless val.is_a?(String)
109
+
110
+ trimmed = val.strip
111
+ case trimmed.downcase
112
+ when "true" then true
113
+ when "false" then false
114
+ when "nil", "null" then nil
115
+ else
116
+ if (trimmed.start_with?("[") && trimmed.end_with?("]")) || (trimmed.start_with?("{") && trimmed.end_with?("}"))
117
+ begin
118
+ parsed = YAML.safe_load(trimmed, permitted_classes: [Symbol], aliases: true)
119
+ return parsed if parsed.is_a?(Array) || parsed.is_a?(Hash)
120
+ rescue Psych::Exception
121
+ # The whole safe_load failure family: SyntaxError for malformed
122
+ # input, DisallowedClass for e.g. "[2026-01-01]" (Date unpermitted),
123
+ # Alias errors — all mean "not a typed collection" → raw string.
124
+ end
125
+ end
126
+
127
+ if trimmed =~ /^-?\d+$/
128
+ trimmed.to_i
129
+ elsif trimmed =~ /^-?\d+\.\d+$/
130
+ trimmed.to_f
131
+ else
132
+ val
133
+ end
134
+ end
135
+ end
136
+
137
+ def model_spec(role = :default)
138
+ role_str = role.to_s.downcase
139
+ spec = fetch("models.#{role_str}")
140
+ return spec if spec.is_a?(Hash) && spec["provider"] && spec["name"]
141
+
142
+ {
143
+ "provider" => fetch("model.provider", "ollama"),
144
+ "name" => fetch("model.name", "qwen2.5-coder:7b"),
145
+ "endpoint" => fetch("model.endpoint") || fetch("model.host", "http://localhost:11434")
146
+ }
147
+ end
148
+
149
+ def verbosity
150
+ fetch("ui.verbosity") || fetch("agent.verbosity", "normal")
151
+ end
152
+
153
+ def verbosity_level
154
+ case verbosity.to_s.downcase.strip
155
+ when "quiet", "0" then 0
156
+ when "verbose", "2" then 2
157
+ when "debug", "trace", "3" then 3
158
+ else 1
159
+ end
160
+ end
161
+
162
+ private
163
+
164
+ # save! returns itself for no-write scopes; persist outcomes are hashes.
165
+ def persist_result(res)
166
+ res.is_a?(Hash) ? res : { ok: true }
167
+ end
168
+
169
+ def notify_change(key, new_val, old_val)
170
+ key_str = key.to_s
171
+ @listeners.dup.each do |entry|
172
+ prefix = entry[:prefix]
173
+ matches = prefix.nil? || prefix.empty? || key_str == prefix || key_str.start_with?("#{prefix}.")
174
+ next unless matches
175
+
176
+ begin
177
+ entry[:callback].call(key_str, new_val, old_val)
178
+ rescue StandardError => e
179
+ warn "[WARN] Config listener failed for '#{key_str}': #{e.message}"
180
+ @event_bus&.emit(:config_listener_error, key: key_str, error: e.message)
181
+ end
182
+ end
183
+ @event_bus&.emit(:config_changed, key: key_str, value: new_val, old_val: old_val)
184
+ end
185
+
186
+ def load_merged_config
187
+ global_file = global_config_path
188
+ project_file = project_config_path
189
+
190
+ global_config = load_yaml(global_file)
191
+ project_config = load_yaml(project_file)
192
+
193
+ merged = deep_merge(deep_dup(DEFAULT_CONFIG), global_config)
194
+ merged = deep_merge(merged, project_config)
195
+
196
+ # If global or project config explicitly defines model roles, ensure stale default hosts are not retained
197
+ [global_config, project_config].each do |cfg|
198
+ next unless cfg.is_a?(Hash)
199
+
200
+ if cfg["models"].is_a?(Hash)
201
+ cfg["models"].each do |role, spec|
202
+ if spec.is_a?(Hash) && spec["provider"] && spec["name"]
203
+ merged["models"] ||= {}
204
+ merged["models"][role.to_s] = deep_dup(spec)
205
+ end
206
+ end
207
+ end
208
+
209
+ merged["model"] = deep_dup(cfg["model"]) if cfg["model"].is_a?(Hash) && cfg["model"]["provider"] && cfg["model"]["name"]
210
+ end
211
+
212
+ validate_schema(merged)
213
+ merged
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "shellwords"
5
+
6
+ module Rubik
7
+ module Context
8
+ class GitInfo
9
+ attr_reader :root_dir
10
+
11
+ def initialize(root_dir: Dir.pwd, process_runner: nil)
12
+ @root_dir = File.expand_path(root_dir)
13
+ @process_runner = process_runner || method(:default_runner)
14
+ end
15
+
16
+ def git_repo?
17
+ !git_dir.nil?
18
+ end
19
+
20
+ def active_branch
21
+ dir = git_dir
22
+ return nil unless dir
23
+
24
+ head_file = File.join(dir, "HEAD")
25
+ return nil unless File.exist?(head_file)
26
+
27
+ content = Rubik::Platform.read_text(head_file).strip
28
+ return nil if content.empty?
29
+
30
+ if content.start_with?("ref: refs/heads/")
31
+ content.sub("ref: refs/heads/", "")
32
+ else
33
+ content[0..6]
34
+ end
35
+ rescue SystemCallError, ArgumentError
36
+ # ArgumentError: expand_path over hostile .git-file content (NUL, bad tilde)
37
+ nil
38
+ end
39
+
40
+ def status
41
+ return { git: false } unless git_repo?
42
+
43
+ out, code = run_git("status --porcelain")
44
+ return { git: false } unless code.zero?
45
+
46
+ dirty = !out.strip.empty?
47
+ {
48
+ git: true,
49
+ branch: active_branch,
50
+ dirty: dirty
51
+ }
52
+ end
53
+
54
+ private
55
+
56
+ # The .git entry can be a directory (normal clone) or a FILE holding
57
+ # "gitdir: <path>" (linked worktree / submodule). Returns the directory
58
+ # that owns HEAD either way, or nil when the root is not a checkout.
59
+ def git_dir
60
+ dot_git = File.join(@root_dir, ".git")
61
+ if File.directory?(dot_git)
62
+ dot_git
63
+ elsif File.file?(dot_git)
64
+ target = Rubik::Platform.read_text(dot_git).strip.delete_prefix("gitdir:").strip
65
+ File.expand_path(target, @root_dir) unless target.empty?
66
+ end
67
+ rescue SystemCallError, ArgumentError
68
+ nil
69
+ end
70
+
71
+ def default_runner(cmd)
72
+ args = cmd.is_a?(Array) ? cmd : Shellwords.split(cmd.to_s)
73
+ out, status = Open3.capture2e("git", "-C", @root_dir, *args)
74
+ [out, status.exitstatus || 1]
75
+ rescue StandardError
76
+ ["", 1]
77
+ end
78
+
79
+ def run_git(cmd)
80
+ @process_runner.call(cmd)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Rubik
6
+ module Context
7
+ # Fast file listing via `git ls-files` (C-speed walk + gitignore handling).
8
+ # Returns nil whenever git cannot answer (not a repo, spawn failure) so
9
+ # the caller falls back to its own walker — a missing git must degrade,
10
+ # never raise. Spawn is call-scoped: no persistent process, thread, or
11
+ # listener is kept alive.
12
+ module GitLister
13
+ module_function
14
+
15
+ # @return [Array<String>, nil] POSIX-style paths relative to root_dir,
16
+ # or nil when git is unusable.
17
+ def files(root_dir)
18
+ out, _err, status = Open3.capture3(
19
+ "git", "-C", root_dir.to_s,
20
+ "ls-files", "-z", "--cached", "--others", "--exclude-standard"
21
+ )
22
+ return nil unless status.success?
23
+
24
+ out.split("\x00", -1).reject(&:empty?).map { |p| p.tr("\\", "/") }.uniq
25
+ rescue StandardError
26
+ nil
27
+ end
28
+
29
+ # Switch resolution shared with the scanner: ENV beats config, anything
30
+ # other than "off" (auto/on/unknown) attempts the fast path first.
31
+ def enabled?(config_value, env: ENV)
32
+ mode = env["RUBIK_NATIVE"] || config_value || "auto"
33
+ mode.to_s.strip.downcase != "off"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require_relative "scanner"
5
+
6
+ module Rubik
7
+ module Context
8
+ class Index
9
+ attr_reader :root_dir, :files, :languages, :last_scan_time, :scanner
10
+
11
+ def initialize(root_dir: Dir.pwd, scanner: nil, additional_roots: [], includes: [], excludes: [], native: nil)
12
+ @root_dir = File.expand_path(root_dir)
13
+ # Serialises mutations to @files / @languages. The index is driven both
14
+ # by synchronous REPL turns and by an async background AST indexer, so
15
+ # without a shared lock concurrent incremental_scan / update_file /
16
+ # full_scan / detect_languages calls race on the same hash (ruby/ruby#
17
+ # 17006) and can corrupt it. Scanning itself is not changed — only the
18
+ # shared-state mutation is made thread-safe.
19
+ @mutex = Mutex.new
20
+ @scanner = scanner || Scanner.new(
21
+ root_dir: @root_dir,
22
+ additional_roots: additional_roots,
23
+ includes: includes,
24
+ excludes: excludes,
25
+ native: native
26
+ )
27
+ @files = {}
28
+ @languages = []
29
+ @last_scan_time = nil
30
+ end
31
+
32
+ def update_file_filters(includes: nil, excludes: nil)
33
+ @scanner.custom_includes = Array(includes).compact unless includes.nil?
34
+ @scanner.custom_excludes = Array(excludes).compact unless excludes.nil?
35
+ full_scan
36
+ end
37
+
38
+ def additional_roots
39
+ @scanner.additional_roots
40
+ end
41
+
42
+ def additional_roots=(roots)
43
+ @scanner.additional_roots = roots
44
+ end
45
+
46
+ def full_scan
47
+ start_time = Time.now
48
+ files = @scanner.scan
49
+ languages = @scanner.detect_languages(files)
50
+ @mutex.synchronize do
51
+ @files = files
52
+ @languages = languages
53
+ @last_scan_time = Time.now - start_time
54
+ end
55
+ @files
56
+ end
57
+
58
+ def incremental_scan
59
+ start_time = Time.now
60
+ snapshot = @mutex.synchronize { @files }
61
+ new_scan = @scanner.scan(snapshot)
62
+ old_files = snapshot
63
+
64
+ added = []
65
+ modified = []
66
+ deleted = []
67
+ unchanged = []
68
+
69
+ new_scan.each do |rel_path, info|
70
+ if old_files.key?(rel_path)
71
+ old_info = old_files[rel_path]
72
+ if old_info[:mtime] != info[:mtime] || old_info[:size] != info[:size] || old_info[:hash] != info[:hash]
73
+ modified << rel_path
74
+ else
75
+ unchanged << rel_path
76
+ end
77
+ else
78
+ added << rel_path
79
+ end
80
+ end
81
+
82
+ old_files.each_key do |rel_path|
83
+ deleted << rel_path unless new_scan.key?(rel_path)
84
+ end
85
+
86
+ languages = @scanner.detect_languages(new_scan)
87
+ @mutex.synchronize do
88
+ @files = new_scan
89
+ @languages = languages
90
+ @last_scan_time = Time.now - start_time
91
+ end
92
+
93
+ {
94
+ added: added,
95
+ modified: modified,
96
+ deleted: deleted,
97
+ unchanged: unchanged
98
+ }
99
+ end
100
+
101
+ def update_file(rel_path)
102
+ full_path = File.expand_path(rel_path, @root_dir)
103
+ unless File.file?(full_path)
104
+ return @mutex.synchronize do
105
+ @files.delete(rel_path)
106
+ false
107
+ end
108
+ end
109
+
110
+ info = @scanner.inspect_file(rel_path, full_path)
111
+ @mutex.synchronize do
112
+ old_info = @files[rel_path]
113
+ if old_info.nil? || old_info[:hash] != info[:hash]
114
+ @files[rel_path] = info
115
+ @languages = @scanner.detect_languages(@files)
116
+ true
117
+ else
118
+ false
119
+ end
120
+ end
121
+ end
122
+
123
+ def status_summary
124
+ @mutex.synchronize do
125
+ {
126
+ file_count: @files.size,
127
+ languages: @languages,
128
+ last_scan_time_sec: @last_scan_time&.round(4)
129
+ }
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end