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,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Concurrency
5
+ # Monotonic min-heap of fiber wakeups. Entries are
6
+ # [deadline, fiber, resume_value, generation]; only the EventLoop thread
7
+ # touches this structure (timers are registered from VM hooks, which run
8
+ # on the loop thread), so no locking is needed.
9
+ #
10
+ # Staleness: a timer for a fiber that has since re-parked (new park
11
+ # generation) or been resumed dead is inert. The EventLoop passes a
12
+ # stale? check at construction; entries failing it are pruned lazily by
13
+ # every query, so a dead timer never gates idle-draining — without this
14
+ # an interrupted 30 s sleep would hold the whole loop hostage until its
15
+ # deadline despite the fiber being long gone.
16
+ class Timers
17
+ def initialize(&staleness)
18
+ @staleness = staleness
19
+ @heap = []
20
+ end
21
+
22
+ # Returns the stored entry (opaque; useful for diagnostics only).
23
+ def add(deadline, fiber, resume_value, generation)
24
+ entry = [deadline, fiber, resume_value, generation]
25
+ @heap << entry
26
+ sift_up(@heap.size - 1)
27
+ entry
28
+ end
29
+
30
+ def empty?
31
+ prune!
32
+ @heap.empty?
33
+ end
34
+
35
+ def size
36
+ prune!
37
+ @heap.size
38
+ end
39
+
40
+ # Seconds until the earliest deadline (0.0 when already due), or nil
41
+ # when no timers are pending.
42
+ def next_gap(now)
43
+ return nil if empty?
44
+
45
+ gap = @heap[0][0] - now
46
+ gap.positive? ? gap : 0.0
47
+ end
48
+
49
+ # Calls sink.call(fiber, resume_value, generation) for every due entry,
50
+ # removing it. Returns the number of entries fired. The sink decides
51
+ # whether a stale entry actually resumes its fiber.
52
+ def fire_due!(now, &sink)
53
+ return 0 if empty? || sink.nil?
54
+
55
+ fired = 0
56
+ while !empty? && @heap[0][0] <= now
57
+ entry = @heap[0]
58
+ last = @heap.pop
59
+ if @heap.empty?
60
+ @heap << last unless last.equal?(entry)
61
+ else
62
+ @heap[0] = last
63
+ sift_down(0)
64
+ end
65
+ sink.call(entry[1], entry[2], entry[3])
66
+ fired += 1
67
+ end
68
+ fired
69
+ end
70
+
71
+ private
72
+
73
+ def prune!
74
+ return unless @staleness
75
+
76
+ @heap.reject! { |entry| @staleness.call(entry[1], entry[3]) }
77
+ end
78
+
79
+ def sift_up(index)
80
+ entry = @heap[index]
81
+ while index.positive?
82
+ parent = (index - 1) / 2
83
+ break if @heap[parent][0] <= entry[0]
84
+
85
+ @heap[index] = @heap[parent]
86
+ index = parent
87
+ end
88
+ @heap[index] = entry
89
+ end
90
+
91
+ def sift_down(index)
92
+ size = @heap.size
93
+ entry = @heap[index]
94
+ loop do
95
+ child = (2 * index) + 1
96
+ break if child >= size
97
+
98
+ right = child + 1
99
+ child = right if right < size && @heap[right][0] < @heap[child][0]
100
+ break if @heap[child][0] >= entry[0]
101
+
102
+ @heap[index] = @heap[child]
103
+ index = child
104
+ end
105
+ @heap[index] = entry
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "log"
4
+ require_relative "concurrency/errors"
5
+ require_relative "concurrency/event_loop"
6
+ require_relative "concurrency/scheduler"
7
+ require_relative "concurrency/boundary"
8
+ require_relative "concurrency/join"
9
+
10
+ module Rubik
11
+ # Silent concurrency over Ruby's Fiber Scheduler: plain blocking I/O —
12
+ # Net::HTTP, sleep backoffs, Open3 pipe reads, Mutex/ConditionVariable
13
+ # waits, Thread#join — interleaves cooperatively on a background event
14
+ # loop while a boundary is open, with ZERO changes at the I/O call sites.
15
+ # "Silently" = no async/await-style syntax noise ever reaches callers.
16
+ #
17
+ # Envelope (documented limits, plan §9): this is I/O concurrency, not CPU
18
+ # parallelism (the GVL still serializes computation); regular-file reads
19
+ # never park (read_nonblock on files returns immediately — they simply run
20
+ # serially, which is harmless); Timeout.timeout-backed code paths are not
21
+ # adopted inside boundaries in v1 (Thread.raise cannot be aimed at a
22
+ # specific fiber). Escape hatch: RUBIK_FIBER_SCHEDULER=off degrades every
23
+ # boundary to plain sequential execution.
24
+ module Concurrency
25
+ module_function
26
+
27
+ # Runs the block on the current OS thread with a fiber scheduler
28
+ # installed. Fibers spawned inside (via .map or Fiber.schedule) drain
29
+ # before the block returns. Returns the block's value; errors surface
30
+ # per Boundary.open's drain-then-raise policy. Nested calls reuse the
31
+ # outer boundary. The scheduler is always uninstalled on exit.
32
+ def silently(&block)
33
+ disabled? ? block.call : Boundary.open(&block)
34
+ end
35
+
36
+ # Ordered fan-out: runs the block for every item concurrently and
37
+ # returns results in INPUT order. concurrency: bounds simultaneously
38
+ # live fibers (fiber stacks cost real memory — pass a bound for very
39
+ # large enumerables). Drain-then-raise: after every item completes, the
40
+ # lowest-INPUT-index error is raised and its siblings are logged.
41
+ def map(enum, concurrency: nil, &block)
42
+ return enum.map(&block) if disabled?
43
+
44
+ items = enum.to_a
45
+ return [] if items.empty?
46
+
47
+ limit = concurrency&.positive? ? concurrency.to_i : nil
48
+ slots = Boundary.open do
49
+ loop = ::Thread.current.thread_variable_get(Boundary::ACTIVE_KEY).loop
50
+ results = ::Array.new(items.size)
51
+ latch = ::Object.new
52
+ waiter = ::Fiber.current
53
+ remaining = items.size
54
+ slot_lock = ::Mutex.new
55
+ slot_free = ::ConditionVariable.new
56
+ used = 0
57
+
58
+ items.each_with_index do |item, index|
59
+ ::Fiber.schedule do
60
+ acquired = false
61
+ begin
62
+ slot_lock.synchronize do
63
+ slot_free.wait(slot_lock) while limit && used >= limit
64
+ used += 1
65
+ end
66
+ acquired = true
67
+ results[index] = { ok: true, value: block.call(item) }
68
+ rescue StandardError => e
69
+ results[index] = { ok: false, error: e }
70
+ ensure
71
+ # The ensure spans the ACQUISITION too: an Exception-level
72
+ # death while parked on the semaphore (interrupt delivery)
73
+ # must still count this item down or the latch never fires and
74
+ # the boundary hangs. Mutex#synchronize releases on raise, so
75
+ # re-locking here is safe.
76
+ if acquired
77
+ slot_lock.synchronize do
78
+ used -= 1
79
+ slot_free.signal
80
+ end
81
+ end
82
+ remaining -= 1 # fibers serialize on the loop thread — no race
83
+ loop.unpark(latch, waiter) if remaining.zero?
84
+ end
85
+ end
86
+ end
87
+ loop.park(latch)
88
+ results
89
+ end
90
+
91
+ failures = slots.filter_map.with_index { |slot, i| [i, slot[:error]] if slot && !slot[:ok] }
92
+ failures.each do |(index, error)|
93
+ Rubik::Log.warn(:concurrency, "fan-out item #{index} failed: #{error.class}: #{error.message}")
94
+ end
95
+ raise failures.first[1] if failures.any?
96
+
97
+ slots.map { |slot| slot[:value] }
98
+ end
99
+
100
+ # True while a boundary is installed on the current OS thread.
101
+ def active?
102
+ !::Thread.current.thread_variable_get(Boundary::ACTIVE_KEY).nil?
103
+ end
104
+
105
+ # RUBIK_FIBER_SCHEDULER=off degrades boundaries to sequential execution
106
+ # (memoized at first use — the process-wide kill switch, plan K14).
107
+ def disabled?
108
+ return @disabled unless @disabled.nil?
109
+
110
+ @disabled = ::ENV["RUBIK_FIBER_SCHEDULER"].to_s.casecmp("off").zero?
111
+ end
112
+
113
+ class << self
114
+ # Test seam: reset the memoized kill-switch state after stubbing ENV.
115
+ def reset_disabled_cache!
116
+ @disabled = nil
117
+ nil
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ # Ruby prints "IO::Buffer is experimental" on the first buffer use; our
124
+ # io_read/io_write hooks exercise it on every boundary. Pay that one-shot
125
+ # warning now, silently, so it never surfaces mid-session.
126
+ original_verbose = $VERBOSE
127
+ begin
128
+ $VERBOSE = nil
129
+ IO::Buffer.new(1)
130
+ ensure
131
+ $VERBOSE = original_verbose
132
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ class Config
5
+ module DeepOps
6
+ private
7
+
8
+ def deep_dup(obj)
9
+ case obj
10
+ when Hash
11
+ obj.transform_values { |v| deep_dup(v) }
12
+ when Array
13
+ obj.map { |v| deep_dup(v) }
14
+ else
15
+ begin
16
+ obj.dup
17
+ rescue TypeError
18
+ # nil/true/false are undupable immediates; YAML-derived config
19
+ # values raise nothing else here. A custom #dup raising anything
20
+ # else must fail loud, not silently alias the value.
21
+ obj
22
+ end
23
+ end
24
+ end
25
+
26
+ # Merges source over target.
27
+ # Invariant (W6): Arrays are replaced as whole units by default so config overrides
28
+ # can completely reset lists. If array_union: true, arrays are merged via union.
29
+ def deep_merge(target, source, array_union: false)
30
+ return deep_dup(target) unless source.is_a?(Hash)
31
+
32
+ result = deep_dup(target)
33
+ source.each do |key, val|
34
+ result[key] = if result[key].is_a?(Hash) && val.is_a?(Hash)
35
+ deep_merge(result[key], val, array_union: array_union)
36
+ elsif array_union && result[key].is_a?(Array) && val.is_a?(Array)
37
+ (result[key] + val).uniq
38
+ else
39
+ deep_dup(val)
40
+ end
41
+ end
42
+ result
43
+ end
44
+
45
+ def deep_stringify_keys(obj)
46
+ case obj
47
+ when Hash
48
+ obj.each_with_object({}) do |(k, v), result|
49
+ result[k.to_s] = deep_stringify_keys(v)
50
+ end
51
+ when Array
52
+ obj.map { |v| deep_stringify_keys(v) }
53
+ else
54
+ obj
55
+ end
56
+ end
57
+
58
+ # Deletes the dot-path leaf from nested string-keyed hashes. Returns
59
+ # :deleted when the leaf existed and was removed, :absent when any
60
+ # segment of the path is missing (nothing to delete), :blocked when
61
+ # the path traverses an existing non-Hash (nothing was changed).
62
+ def deep_delete(obj, path)
63
+ keys = path.to_s.split(".")
64
+ return :blocked unless obj.is_a?(Hash)
65
+
66
+ node = keys[0...-1].reduce(obj) do |acc, k|
67
+ return :absent unless acc.is_a?(Hash) && acc.key?(k)
68
+
69
+ child = acc[k]
70
+ return :blocked unless child.is_a?(Hash)
71
+
72
+ child
73
+ end
74
+ return :blocked unless node.is_a?(Hash)
75
+
76
+ node.delete(keys.last) ? :deleted : :absent
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../platform"
4
+
5
+ module Rubik
6
+ class Config
7
+ module Defaults
8
+ DEFAULT_CONFIG = {
9
+ "llm" => {
10
+ "stream" => true,
11
+ "stream_timeout" => 120,
12
+ "open_timeout" => 15,
13
+ "default_max_output_tokens" => 4096,
14
+ "capture_reasoning" => true,
15
+ "auto_continue_on_length" => true,
16
+ "max_auto_continues" => 3,
17
+ # Plan 90 bridge (loop.rb): nil = not sent; non-nil reasoning_effort also flips reasoning_model?.
18
+ "thinking_budget_ratio" => nil, # Anthropic thinking budget share (0.0-0.8) -> opts[:thinking_budget_ratio]
19
+ "reasoning_effort" => nil, # OpenAI reasoning effort "low"|"medium"|"high" -> opts[:reasoning_effort]
20
+ "headers" => {}
21
+ },
22
+ "model" => { "provider" => "ollama", "name" => "qwen2.5-coder:7b", "endpoint" => "http://localhost:11434" },
23
+ "models" => {
24
+ "default" => { "provider" => "ollama", "name" => "qwen2.5-coder:7b", "endpoint" => "http://localhost:11434" },
25
+ "fast" => { "provider" => "ollama", "name" => "qwen2.5-coder:1.5b", "endpoint" => "http://localhost:11434" },
26
+ "advanced" => { "provider" => "ollama", "name" => "qwen2.5-coder:14b", "endpoint" => "http://localhost:11434" }
27
+ },
28
+ "providers" => {},
29
+ "agent" => {
30
+ "max_steps" => 40,
31
+ "goal_max_steps" => 100,
32
+ "plan_max_steps" => 15,
33
+ "warn_at_ratio" => 0.75,
34
+ "ask_continue_on_limit" => true, "goal_wait_timeout" => 300
35
+ },
36
+ # rubik://storage sanal FS-DB (Rubik::Storage): hedef/plan/bellek/wiki/transcript varlıkları
37
+ # + worktree'yi temiz tutan artifact absorb'u. root "~" içerebilir; project nil ise slug türetilir.
38
+ "storage" => {
39
+ "enabled" => true,
40
+ "root" => "~/.rubik/rubikstorage",
41
+ "project" => nil,
42
+ "absorb_artifacts" => true
43
+ },
44
+ "tools" => {
45
+ "circuit_breaker" => { "enabled" => false, "threshold" => 5, "cooldown_seconds" => 60 },
46
+ "max_output_chars" => 12_000,
47
+ "max_output_lines" => 300,
48
+ "truncation_strategy" => "head_tail",
49
+ "timeout_seconds" => 30,
50
+ "native" => "auto"
51
+ },
52
+ "router" => { "mode" => "auto", "sticky" => true },
53
+ "repair" => { "max_attempts" => 3 },
54
+ "sandbox" => {
55
+ "enabled" => false,
56
+ "driver" => "bwrap",
57
+ "image" => "rubik-sandbox:latest",
58
+ "timeout_seconds" => 15,
59
+ "memory_limit_mb" => 512,
60
+ "cpus" => "1.0",
61
+ "allow_network" => false,
62
+ "shell" => "auto", # sandbox shell resolution: auto | bash | powershell (sandbox/runner.rb)
63
+ "prompt_on_startup" => true, "isolation_note" => true # append the 🔍 isolation line to run_command output (sandbox/runner.rb)
64
+ },
65
+ "security" => {
66
+ # NOTE: no audit_log_path default — Guard.default_audit_log_path (security/guard.rb) is the
67
+ # single source so the value resolves at call time; a load-time snapshot would pin require-time HOME.
68
+ "command_allowlist" => [], # operator-managed token-prefix entries (e.g. "node .gitnexus/run.cjs")
69
+ "allow_safe_probes" => true # `bash --version`-style version/help probes bypass the blocklist
70
+ },
71
+ "network" => { "allow" => false },
72
+ "buffer" => {
73
+ "auto_apply" => "manual",
74
+ "model_can_apply" => true,
75
+ "prompt_indicator" => true,
76
+ "exit_guard" => true,
77
+ "reminder_interval" => 3,
78
+ "on_external_modification" => "skip"
79
+ },
80
+ "lsp" => {
81
+ "enabled" => true,
82
+ "auto_start" => true,
83
+ "auto_diagnostics" => true,
84
+ "servers" => {
85
+ "go" => "gopls", "rust" => "rust-analyzer", "zig" => "zls", "c" => "clangd", "cpp" => "clangd",
86
+ "vue" => "vue-language-server --stdio", "python" => "ruff server", "ruby" => "typeprof --lsp --stdio",
87
+ "typescript" => "typescript-language-server --stdio", "javascript" => "typescript-language-server --stdio",
88
+ "bash" => "bash-language-server start", "crystal" => "crystalline", "gleam" => "gleam lsp"
89
+ }
90
+ },
91
+ "mcp" => { "enabled" => true, "servers" => {}, "timeout" => 10, "call_timeout" => 30, "async_load" => nil }, # async_load nil = env-aware; per-server "timeout" overrides
92
+ "ast" => { "async_warm" => nil }, # nil = env-aware default (false under RUBIK_TEST); repl/lifecycle.rb warm_up!
93
+ "context" => {
94
+ "include" => [],
95
+ "exclude" => [],
96
+ "max_tokens" => 32_768, # subagent context-window fallback when the routed model spec carries no context_limit (subagent/instance.rb, subagent/supervisor.rb)
97
+ "compaction_strategy" => "hybrid", # "hybrid", "prune_tool_results", "sliding_window", "off"
98
+ "max_tokens_ratio" => 0.85,
99
+ "tool_result_prune_chars" => 1500,
100
+ "preserve_first_n" => 2,
101
+ "preserve_last_n" => 6,
102
+ "safety_margin_tokens" => 1024,
103
+ "auto_recover_on_overflow" => true,
104
+ "log_failed_turns" => true,
105
+ "tool_chronicle_in_history" => true, # Plan 85 D-2/D-3
106
+ "tool_chronicle_max_entries" => 15, # Plan 85 D-2/D-3
107
+ "merge_turn_tool_history" => true, # Plan 85 D-7
108
+ "merge_turn_tool_max_entries" => 20, # Plan 85 D-7
109
+ "emergency_truncate_strategy" => "head_tail" # EmergencyCompactor hard-truncate layer: "head_tail" | "head_only" | "tail_only"
110
+ },
111
+ "ui" => {
112
+ "verbosity" => "normal",
113
+ "color" => "auto",
114
+ "glyphs" => "unicode",
115
+ "banner" => true,
116
+ "spinner" => true,
117
+ "spinner_verbs" => [], # empty = built-in verb set (ui/theme.rb from_config)
118
+ "markdown" => "live",
119
+ "statusline" => true,
120
+ "statusline_template" => "{model} · ctx {ctx} · {dirty} · sandbox {sandbox}",
121
+ "input_box" => false,
122
+ "paste_join_window_ms" => 120, # Plan 91: lines arriving within this gap join into one paste turn
123
+ "quiet_notices" => false # Plan 105: buffer background notices to inbox until prompt/enter
124
+ },
125
+ "workspace" => {
126
+ "additional_roots" => [],
127
+ "allowed_external_dirs" => [],
128
+ "allow_external_writes" => false,
129
+ "auto_index_additional" => true
130
+ },
131
+ "plugins" => {
132
+ "load" => [],
133
+ "aliases" => {},
134
+ "autoload_dropin" => true
135
+ },
136
+ "skills" => {
137
+ "enabled" => true,
138
+ "paths" => [], # extra skill directories (absolute or root-relative)
139
+ "disabled" => [], # skill names excluded from discovery
140
+ "max_body_chars" => 8000 # keep below tools.max_output_chars (head_tail would cut the middle)
141
+ },
142
+ "subagents" => {
143
+ "max_concurrency" => 10,
144
+ "max_depth" => 2,
145
+ "default_max_steps" => 88,
146
+ "provider_concurrency" => 2,
147
+ "budget_wait_timeout" => 180,
148
+ "auto_apply_buffer" => true,
149
+ "network_retry_count" => 3,
150
+ "network_backoff_base" => 0.5,
151
+ "error_wait_timeout" => 300,
152
+ "question_timeout" => 300,
153
+ # Bitmiş subagent bu grace (sn) sonrasında reap edilir (0 = kapalı);
154
+ # tombstone_limit sorgulanabilir mezar taşı sayısını sınırlar:
155
+ "reap_finished_after" => 300,
156
+ "tombstone_limit" => 25,
157
+ # Maestro kapısı: maestro write_file/replace_file_content/run_command/
158
+ # run_tests araçlarını doğrudan ÇALIŞTIRAMAZ — subagent'a devir zorunlu
159
+ # (false = doktrin yalnız prompt düzeyinde, kapı kapalı):
160
+ "maestro_gate" => true,
161
+ "auto_pause_on_network_error" => true,
162
+ "reactive_wakeup" => true,
163
+ # Vakanüvis arka plan kronikçisi (storage/vakanuvis; plan
164
+ # 2026-10-04 K1-K9): deterministik annal + wiki derleyicisi.
165
+ # enabled: Düzlem 1 harness'i kur/kurulama; distiller: Düzlem 2
166
+ # LLM doğumu (token üst sınırı flush_events x max_steps);
167
+ # flush_events: damıtıcı doğumu için olay eşiği; model: nil =
168
+ # root provider; retention: supersede (iz bırak) | purge (sil);
169
+ # root_turns: REPL kök-turn'leri de annal'a yazılsın mı
170
+ # (root-turn planı 2026-10-04; kapalıyken abonelik kurulmaz).
171
+ "vakanuvis" => {
172
+ "enabled" => true,
173
+ "distiller" => true,
174
+ "max_steps" => 8,
175
+ "flush_events" => 10,
176
+ "model" => nil,
177
+ "retention" => "supersede",
178
+ "root_turns" => true
179
+ },
180
+ # Pruner (repl/pruner): yarı deterministik KÖK pencere temizleyicisi.
181
+ # DEFAULT ON (kullanıcı kararı 2026-10-10) — deterministik yığın
182
+ # (trim_conversation_window! / ContextCompactor / EmergencyCompactor)
183
+ # her zaman güvenlik ağıdır; subagent kanalları bu özellikten asla
184
+ # etkilenmez; opt-out: "enabled" => false (kill-switch). enabled:
185
+ # harness'i kur (prune_context aracı + !prune komutu açılır);
186
+ # trigger_ratio: turn sonu pencere tahmini girdi tavanının bu
187
+ # oranına ulaşınca LLM budayıcı doğar (0.85'lik kör trim'den ÖNCE
188
+ # anlamsal sıkıştırma); target_ratio: plan yalnızca sonuç bu
189
+ # orana sığıyorsa uygulanır; max_steps/model: budayıcı bütçesi
190
+ # (nil = root provider); min_turns_between: otomatik doğum oranı
191
+ # sınırlayıcısı (turn sayısı).
192
+ "pruner" => {
193
+ "enabled" => true,
194
+ "trigger_ratio" => 0.6,
195
+ "target_ratio" => 0.35,
196
+ "max_steps" => 16,
197
+ "model" => nil,
198
+ "min_turns_between" => 5
199
+ }
200
+ },
201
+ # Goal ledger (SKILL.state ailesi, plan A): !goal'nun yapılandırılmış
202
+ # ΔΣ defteri — DEFAULT ON (kullanıcı kararı 2026-10-10; opt-out:
203
+ # enabled=false, o zaman araç/prompt/storage yüzü tamamen sessiz).
204
+ # max_patch_retries: ardışık reddedilen update_goal_ledger bütçesi.
205
+ # B/D anahtarları (verify / observe_projection) aynı hash'e eklenir.
206
+ "goal" => {
207
+ "ledger" => { "enabled" => true, "verify" => true, "observe_projection" => true,
208
+ "observe_window" => 5, "max_patch_retries" => 2 }
209
+ },
210
+ "project" => { "test_command" => nil },
211
+ "test" => { "timeout" => 30, "runners" => {} }, # run_tests timeout (adapter/base.rb); per-language runner overrides (exec.rb)
212
+ "adapter" => {}
213
+ }.freeze
214
+
215
+ private
216
+
217
+ def validate_schema(config, schema = DEFAULT_CONFIG, prefix = "", strict: false)
218
+ raise ArgumentError, "Configuration must be a Hash, got #{config.class}" unless config.is_a?(Hash)
219
+
220
+ config.each do |key, val|
221
+ current_key = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
222
+ if schema.is_a?(Hash) && !schema.key?(key.to_s)
223
+ candidate_keys = schema.keys
224
+ candidates = if defined?(Rubik::Tools::FuzzyMatcher)
225
+ Rubik::Tools::FuzzyMatcher.best_matches(key.to_s, candidate_keys)
226
+ else
227
+ []
228
+ end
229
+
230
+ suggestion_msg = if candidates && !candidates.empty?
231
+ suggested_full = prefix.empty? ? candidates.first : "#{prefix}.#{candidates.first}"
232
+ "Unknown config key '#{current_key}'. Did you mean '#{suggested_full}'?"
233
+ else
234
+ "Unknown config key '#{current_key}'."
235
+ end
236
+
237
+ raise ArgumentError, suggestion_msg if strict
238
+
239
+ @warnings << suggestion_msg
240
+
241
+ elsif val.is_a?(Hash) && schema.is_a?(Hash) && schema[key.to_s].is_a?(Hash) && !schema[key.to_s].empty?
242
+ validate_schema(val, schema[key.to_s], current_key, strict: strict)
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Provider/model catalog CRUD (G21 extraction from config.rb): the mutating
4
+ # verbs over the `models.<role>` and `providers.<name>` storage shapes. The
5
+ # read-side `model_spec` stays typed access in config.rb (Plan 66 ruling).
6
+ # Public API is unchanged — these are Config instance methods via include;
7
+ # config never prints, outcome hashes (ok/error/reason) let callers report.
8
+ module Rubik
9
+ class Config
10
+ module ModelCatalog
11
+ def set_model_spec(role, provider:, name:, endpoint: nil, host: nil, persist: nil)
12
+ role_str = role.to_s.downcase
13
+ old_spec = fetch("models.#{role_str}")
14
+ @data["models"] ||= {}
15
+ spec_hash = {
16
+ "provider" => provider.to_s,
17
+ "name" => name.to_s
18
+ }
19
+ target_endpoint = endpoint || host
20
+ spec_hash["endpoint"] = target_endpoint if target_endpoint
21
+ @data["models"][role_str] = spec_hash
22
+
23
+ @data["model"] = spec_hash.dup if role_str == "default"
24
+
25
+ res = persisting?(persist) ? persist_result(save!(persist)) : { ok: true }
26
+ res[:reason] = :persist_failed unless res[:ok]
27
+ notify_change("models.#{role_str}", spec_hash, old_spec)
28
+ notify_change("model", spec_hash, old_spec) if role_str == "default"
29
+ res
30
+ end
31
+
32
+ # ── Provider-model catalog (providers.<name> storage shape) ──────────
33
+ # The spec hash and its "models" array (Hash entries with "name" /
34
+ # "context_limit"; legacy bare String names tolerated) are storage
35
+ # shape: commands ask intent here instead of re-encoding it per
36
+ # !model/!provider branch.
37
+
38
+ # Live spec hash for `provider`, or nil when unknown (absent or empty —
39
+ # the historical !provider "not found" semantics).
40
+ def provider_spec(provider)
41
+ spec = fetch("providers.#{provider}")
42
+ spec.is_a?(Hash) && !spec.empty? ? spec : nil
43
+ end
44
+
45
+ # Replace (or append) provider's catalog entry for `model` with the
46
+ # given context limit. {ok: false, reason: :unknown_provider} when the
47
+ # provider is unknown.
48
+ def upsert_provider_model(provider, model, context_limit:, persist: nil)
49
+ mutate_provider_models(provider, persist: persist) do |models|
50
+ models.reject! { |m| model_entry_name(m) == model }
51
+ models << { "name" => model, "context_limit" => context_limit }
52
+ end
53
+ end
54
+
55
+ # Drop provider's catalog entry for `model`. {ok: false,
56
+ # reason: :unknown_provider} when the provider is unknown; removing an
57
+ # absent model is a successful no-op (matches the historical
58
+ # !provider remove-model report).
59
+ def remove_provider_model(provider, model, persist: nil)
60
+ mutate_provider_models(provider, persist: persist) do |models|
61
+ models.reject! { |m| model_entry_name(m) == model }
62
+ end
63
+ end
64
+
65
+ # Drop a provider and its catalog entirely. Like the model verbs this
66
+ # notifies no listener — callers refresh the runtime explicitly. Routed
67
+ # through delete so the removal survives the disk merge.
68
+ def remove_provider(provider, persist: nil)
69
+ return { ok: false, error: "Provider '#{provider}' not found", reason: :unknown_provider } unless provider_spec(provider)
70
+
71
+ delete("providers.#{provider}", persist: persist)
72
+ end
73
+
74
+ private
75
+
76
+ def mutate_provider_models(provider, persist:)
77
+ p_spec = provider_spec(provider)
78
+ return { ok: false, error: "Provider '#{provider}' not found", reason: :unknown_provider } unless p_spec
79
+
80
+ models = Array(p_spec["models"]).dup
81
+ yield models
82
+ p_spec["models"] = models
83
+ set("providers.#{provider}", p_spec, persist: persist)
84
+ end
85
+
86
+ # Catalog entries are Hash-shaped today; older configs stored bare model
87
+ # name Strings. One predicate keeps the tolerance from drifting per verb.
88
+ def model_entry_name(entry)
89
+ entry.is_a?(Hash) ? entry["name"] : entry.to_s
90
+ end
91
+ end
92
+ end
93
+ end