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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "benchmark/runner"
4
+ require_relative "benchmark/synth_repo"
5
+
6
+ module Rubik
7
+ module Benchmark
8
+ end
9
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "diff/lcs"
4
+ require "diff/lcs/hunk"
5
+
6
+ module Rubik
7
+ module Buffer
8
+ class Diff
9
+ def self.unified_diff(file_path, original_content, new_content, context: 3)
10
+ orig_lines = (original_content || "").lines
11
+ new_lines = (new_content || "").lines
12
+
13
+ return "" if orig_lines == new_lines
14
+
15
+ diffs = ::Diff::LCS.diff(orig_lines, new_lines)
16
+ return "" if diffs.empty?
17
+
18
+ output = "--- a/#{file_path}\n+++ b/#{file_path}\n"
19
+ file_length_difference = 0
20
+ oldhunk = hunk = nil
21
+
22
+ diffs.each do |piece|
23
+ hunk = ::Diff::LCS::Hunk.new(orig_lines, new_lines, piece, context, file_length_difference)
24
+ file_length_difference = hunk.file_length_difference
25
+ next unless oldhunk
26
+ next if context.positive? && hunk.overlaps?(oldhunk) && hunk.merge(oldhunk)
27
+
28
+ output << oldhunk.diff(:unified) << "\n"
29
+ ensure
30
+ oldhunk = hunk
31
+ end
32
+
33
+ if oldhunk
34
+ last = oldhunk.diff(:unified, true)
35
+ last << "\n" unless last.empty? || last.end_with?("\n")
36
+ output << last
37
+ end
38
+
39
+ output
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,427 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "digest"
5
+ require "pathname"
6
+ require "monitor"
7
+ require_relative "diff"
8
+ require_relative "path_validator"
9
+ require_relative "../platform"
10
+ require_relative "../security/guard"
11
+
12
+ module Rubik
13
+ module Buffer
14
+ class InvalidPathError < StandardError; end
15
+
16
+ class Manager
17
+ attr_reader :root_dir, :dirty_files, :additional_roots, :rw_roots, :allowed_external_dirs, :rw_allowed_dirs, :guard, :last_conflicts
18
+ attr_accessor :allow_external_writes, :config
19
+
20
+ def initialize(root_dir: Dir.pwd, additional_roots: [], allowed_external_dirs: [], allow_external_writes: false,
21
+ event_bus: nil, guard: nil, config: nil)
22
+ @monitor = Monitor.new
23
+ @root_dir = File.expand_path(root_dir)
24
+ @additional_roots = (additional_roots || []).map { |r| File.expand_path(r.to_s, @root_dir) }.uniq
25
+ @allowed_external_dirs = (allowed_external_dirs || []).map { |d| File.expand_path(d.to_s, @root_dir) }.uniq
26
+ @allow_external_writes = allow_external_writes || false
27
+ @rw_roots = Set.new
28
+ @rw_allowed_dirs = Set.new(@allowed_external_dirs)
29
+ @event_bus = event_bus
30
+ @guard = guard || Rubik::Security::Guard.new
31
+ @config = config
32
+ @buffer = {}
33
+ @dirty_files = {}
34
+ @original_hashes = {}
35
+ @last_conflicts = []
36
+ @last_applied = nil
37
+ end
38
+
39
+ def add_root(path, rw: false) # rubocop:disable Naming/MethodParameterName -- rw: bilinçli kisa kwarg
40
+ @monitor.synchronize do
41
+ expanded = File.expand_path(path.to_s, @root_dir)
42
+ @additional_roots << expanded unless @additional_roots.include?(expanded)
43
+ if rw
44
+ @rw_roots << expanded
45
+ else
46
+ @rw_roots.delete(expanded)
47
+ end
48
+ expanded
49
+ end
50
+ end
51
+
52
+ def remove_root(path)
53
+ @monitor.synchronize do
54
+ expanded = File.expand_path(path.to_s, @root_dir)
55
+ @additional_roots.delete(expanded)
56
+ @rw_roots.delete(expanded)
57
+ expanded
58
+ end
59
+ end
60
+
61
+ def add_allowed_dir(path, rw: true) # rubocop:disable Naming/MethodParameterName -- rw: bilinçli kisa kwarg
62
+ @monitor.synchronize do
63
+ expanded = File.expand_path(path.to_s, @root_dir)
64
+ @allowed_external_dirs << expanded unless @allowed_external_dirs.include?(expanded)
65
+ if rw
66
+ @rw_allowed_dirs << expanded
67
+ else
68
+ @rw_allowed_dirs.delete(expanded)
69
+ end
70
+ expanded
71
+ end
72
+ end
73
+
74
+ def remove_allowed_dir(path)
75
+ @monitor.synchronize do
76
+ expanded = File.expand_path(path.to_s, @root_dir)
77
+ @allowed_external_dirs.delete(expanded)
78
+ @rw_allowed_dirs.delete(expanded)
79
+ expanded
80
+ end
81
+ end
82
+
83
+ def sanitize_path(path, for_write: false)
84
+ expanded = File.expand_path(path.to_s, @root_dir)
85
+
86
+ # Boundary containment (symlink escape): lexical prefix matching is
87
+ # the floor; containment_root additionally resolves symlinks on BOTH
88
+ # sides, so a link planted inside the root cannot redirect
89
+ # reads/writes outside it. Stored buffer keys stay lexical.
90
+ matched_kind, matched_dir = containment_root(expanded)
91
+
92
+ unless matched_dir
93
+ all_allowed = ([@root_dir] + @additional_roots + @allowed_external_dirs).uniq
94
+ raise InvalidPathError,
95
+ "Path traversal attempt blocked: '#{path}'. Not within primary root, allowed workspace roots, " \
96
+ "or external allowed dirs: #{all_allowed.join(', ')}"
97
+ end
98
+
99
+ matched_root = matched_kind == :root ? matched_dir : nil
100
+ matched_ext = matched_kind == :external ? matched_dir : nil
101
+
102
+ rel_or_exp = if matched_root && Rubik::Platform.path_equal?(matched_root, @root_dir)
103
+ begin
104
+ rel = Pathname.new(expanded).relative_path_from(Pathname.new(@root_dir)).to_s.gsub("\\", "/")
105
+ rel == "." ? "." : rel
106
+ rescue ArgumentError
107
+ expanded
108
+ end
109
+ else
110
+ expanded
111
+ end
112
+
113
+ if for_write
114
+ # Critical path security check (@guard is always assigned in
115
+ # initialize — the historical re-fallback here was unreachable).
116
+ # Pass the ROOT-ANCHORED expanded path: the guard's lexical
117
+ # candidates and its symlink resolution both anchor to the process
118
+ # cwd, which diverges from the workspace root when the REPL is
119
+ # launched from elsewhere — resolving relative keys there would
120
+ # inspect the wrong tree.
121
+ begin
122
+ @guard.verify_path_write!(expanded)
123
+ rescue Rubik::Security::BlockedWriteError => e
124
+ raise InvalidPathError, e.message
125
+ end
126
+
127
+ if matched_root && !Rubik::Platform.path_equal?(matched_root, @root_dir)
128
+ if !@allow_external_writes && @rw_roots.none? { |r| Rubik::Platform.path_equal?(r, matched_root) }
129
+ raise InvalidPathError,
130
+ "Write blocked on external workspace root: '#{path}'. Configure 'workspace.allow_external_writes: true' or add with --rw to enable writes."
131
+ end
132
+ elsif matched_ext
133
+ if !@allow_external_writes && @rw_allowed_dirs.none? { |d| Rubik::Platform.path_equal?(d, matched_ext) }
134
+ raise InvalidPathError,
135
+ "Write blocked on external allowed directory: '#{path}'. " \
136
+ "Configure 'workspace.allow_external_writes: true' or add with --rw to enable writes."
137
+ end
138
+ end
139
+ end
140
+
141
+ rel_or_exp
142
+ end
143
+
144
+ # Reads a file from the buffer (if dirty) or from disk, normalizing newlines.
145
+ #
146
+ # @param path [String] file path relative to the root directory
147
+ # @param reload [Boolean] force reload from disk, discarding in-memory buffer if present
148
+ # @param from_disk [Boolean] read directly from disk without modifying buffer or original hashes
149
+ # @return [String, nil] the file content, or nil if the file does not exist
150
+ def read(path, reload: false, from_disk: false)
151
+ @monitor.synchronize do
152
+ rel_path = sanitize_path(path, for_write: false)
153
+ if reload
154
+ @buffer.delete(rel_path)
155
+ @dirty_files.delete(rel_path)
156
+ @original_hashes.delete(rel_path)
157
+ @last_conflicts.delete(rel_path)
158
+ end
159
+
160
+ if from_disk
161
+ full_path = File.expand_path(rel_path, @root_dir)
162
+ return nil unless File.exist?(full_path)
163
+
164
+ return Rubik::Platform.normalize_newlines(Rubik::Platform.read_text(full_path))
165
+ elsif @buffer.key?(rel_path)
166
+ return @buffer[rel_path]
167
+ end
168
+
169
+ full_path = File.expand_path(rel_path, @root_dir)
170
+ return nil unless File.exist?(full_path)
171
+
172
+ content = Rubik::Platform.normalize_newlines(Rubik::Platform.read_text(full_path))
173
+ @original_hashes[rel_path] = Digest::SHA256.hexdigest(content)
174
+ content
175
+ end
176
+ end
177
+
178
+ def discard(path)
179
+ @monitor.synchronize do
180
+ rel_path = sanitize_path(path, for_write: false)
181
+ @buffer.delete(rel_path)
182
+ @dirty_files.delete(rel_path)
183
+ @original_hashes.delete(rel_path)
184
+ @last_conflicts.delete(rel_path)
185
+ @event_bus&.emit(:buffer_discarded, path: rel_path)
186
+ rel_path
187
+ end
188
+ end
189
+
190
+ def write(path, content, reset_baseline: false)
191
+ @monitor.synchronize do
192
+ rel_path = sanitize_path(path, for_write: true)
193
+ normalized_content = Rubik::Platform.normalize_newlines(content)
194
+
195
+ if reset_baseline || !@original_hashes.key?(rel_path)
196
+ full_path = File.expand_path(rel_path, @root_dir)
197
+ @original_hashes[rel_path] = (Digest::SHA256.hexdigest(Rubik::Platform.normalize_newlines(Rubik::Platform.read_text(full_path))) if File.exist?(full_path))
198
+ end
199
+
200
+ @buffer[rel_path] = normalized_content
201
+ @dirty_files[rel_path] = true
202
+ @event_bus&.emit(:buffer_changed, path: rel_path)
203
+ normalized_content
204
+ end
205
+ end
206
+
207
+ # Returns whether the buffer has any unapplied file modifications,
208
+ # or whether a specific file path is dirty or unwritten to disk.
209
+ #
210
+ # @param path [String, nil] optional path to check
211
+ # @return [Boolean] true if there are dirty files (or specified path is dirty)
212
+ def dirty?(path = nil)
213
+ @monitor.synchronize do
214
+ if path
215
+ rel_path = begin
216
+ sanitize_path(path, for_write: false)
217
+ rescue InvalidPathError
218
+ path.to_s
219
+ end
220
+
221
+ return true if @dirty_files.key?(rel_path) || @dirty_files.key?(path.to_s)
222
+
223
+ target = if @buffer.key?(rel_path)
224
+ rel_path
225
+ elsif @buffer.key?(path.to_s)
226
+ path.to_s
227
+ end
228
+
229
+ if target
230
+ full_path = File.expand_path(target, @root_dir)
231
+ return true unless File.exist?(full_path)
232
+ end
233
+
234
+ false
235
+ else
236
+ !@dirty_files.empty?
237
+ end
238
+ end
239
+ end
240
+
241
+ # Returns list of all known files in the buffer and on disk within root_dir.
242
+ # The disk walk happens OUTSIDE the monitor (review 2026-09-17, Finding 4):
243
+ # a full-tree Dir.glob under the lock used to block every concurrent
244
+ # buffer operation on large repos. We snapshot the in-buffer keys under
245
+ # the lock, glob without it, then merge — results may be a few ms stale
246
+ # for callers that only need a fuzzy-match candidate list.
247
+ def all_files
248
+ buffer_keys = @monitor.synchronize { @buffer.keys }
249
+ disk_files = begin
250
+ Dir.glob("**/*", File::FNM_DOTMATCH, base: @root_dir).reject do |f|
251
+ f.start_with?(".git") || File.directory?(File.expand_path(f, @root_dir))
252
+ end
253
+ rescue SystemCallError
254
+ []
255
+ end
256
+ (buffer_keys + disk_files).uniq
257
+ end
258
+
259
+ def diff_all
260
+ snapshot = @monitor.synchronize do
261
+ @dirty_files.keys.map { |rel_path| [rel_path, @buffer[rel_path] || ""] }
262
+ end
263
+
264
+ diffs = snapshot.map do |rel_path, new_content|
265
+ full_path = File.expand_path(rel_path, @root_dir)
266
+ orig_content = File.exist?(full_path) ? Rubik::Platform.normalize_newlines(File.read(full_path)) : ""
267
+ Diff.unified_diff(rel_path, orig_content, new_content)
268
+ end
269
+
270
+ diffs.join("\n")
271
+ end
272
+
273
+ def apply!(target_paths = nil, policy: nil)
274
+ @monitor.synchronize do
275
+ applied_paths = []
276
+ conflicts = []
277
+ keys_to_apply = if target_paths
278
+ Array(target_paths).map { |p| sanitize_path(p, for_write: true) }
279
+ else
280
+ @dirty_files.keys.dup
281
+ end
282
+
283
+ effective_policy = (policy || external_modification_policy).to_s.downcase
284
+
285
+ keys_to_apply.each do |rel_path|
286
+ next unless @dirty_files.key?(rel_path)
287
+
288
+ unless PathValidator.valid?(rel_path)
289
+ @dirty_files.delete(rel_path)
290
+ @buffer.delete(rel_path)
291
+ @original_hashes.delete(rel_path)
292
+ @event_bus&.emit(:buffer_garbage_skipped, path: rel_path)
293
+ next
294
+ end
295
+
296
+ baseline = @original_hashes[rel_path]
297
+ current_disk = disk_hash(rel_path)
298
+
299
+ conflict = if baseline
300
+ current_disk != baseline
301
+ else
302
+ !current_disk.nil?
303
+ end
304
+
305
+ if conflict && effective_policy != "overwrite"
306
+ conflicts << rel_path
307
+ @event_bus&.emit(:buffer_apply_conflict, paths: conflicts, path: rel_path)
308
+ next
309
+ end
310
+
311
+ full_path = File.expand_path(rel_path, @root_dir)
312
+
313
+ # TOCTOU close: re-verify containment immediately before the
314
+ # atomic write — a symlink planted between write-time sanitize
315
+ # and apply! must not redirect the write outside the workspace.
316
+ # Fail loud: an untrustworthy path is an attack signal or a
317
+ # mid-flight root misconfiguration, not a content problem; the
318
+ # entry stays dirty for a retry after remediation.
319
+ containment_kind, = containment_root(full_path)
320
+ unless containment_kind
321
+ @event_bus&.emit(:buffer_containment_blocked, path: rel_path)
322
+ # Commit what this run has seen so "apply! rewrites
323
+ # last_conflicts on every run" holds for readers (AutoApply's
324
+ # report_conflicts) even on the failure path.
325
+ @last_conflicts = conflicts
326
+ raise InvalidPathError,
327
+ "Write blocked: '#{rel_path}' resolves outside all allowed roots at apply time " \
328
+ "(possible symlink swap since buffering). Re-verify the workspace and retry."
329
+ end
330
+
331
+ content = @buffer[rel_path]
332
+
333
+ Rubik::Platform.atomic_write(full_path, content)
334
+
335
+ @original_hashes[rel_path] = Digest::SHA256.hexdigest(content)
336
+ @dirty_files.delete(rel_path)
337
+ @buffer.delete(rel_path)
338
+ applied_paths << rel_path
339
+ end
340
+
341
+ @last_conflicts = conflicts
342
+ @last_applied = { at: Time.now.utc, paths: applied_paths.dup.freeze } unless applied_paths.empty?
343
+ @event_bus&.emit(:buffer_applied, paths: applied_paths) unless applied_paths.empty?
344
+ applied_paths
345
+ end
346
+ end
347
+
348
+ # When and which files the most recent apply! actually wrote to disk —
349
+ # lets apply_buffer distinguish "buffer was clean because the auto-apply
350
+ # policy already flushed these edits" from "nothing ever happened"
351
+ # (issue #97 finding 6). nil until the first successful apply.
352
+ def last_applied
353
+ @last_applied && { at: @last_applied[:at], paths: @last_applied[:paths] }
354
+ end
355
+
356
+ def reset!
357
+ @monitor.synchronize do
358
+ @buffer.clear
359
+ @dirty_files.clear
360
+ @original_hashes.clear
361
+ @last_conflicts.clear
362
+ @last_applied = nil
363
+ end
364
+ end
365
+
366
+ def snapshot_state
367
+ @monitor.synchronize do
368
+ state = {}
369
+ @dirty_files.each_key do |rel_path|
370
+ state[rel_path] = @buffer[rel_path]
371
+ end
372
+ state
373
+ end
374
+ end
375
+
376
+ def restore_snapshot_state(state)
377
+ @monitor.synchronize do
378
+ @buffer.clear
379
+ @dirty_files.clear
380
+ state.each do |rel_path, content|
381
+ @buffer[rel_path] = content
382
+ @dirty_files[rel_path] = true
383
+ end
384
+ end
385
+ end
386
+
387
+ private
388
+
389
+ # Single source of boundary containment for both the sanitize_path
390
+ # entry check and the apply! re-verification. Returns [kind, dir] with
391
+ # kind :root / :external (dir is the LEXICAL matched root/external
392
+ # dir — downstream keying stays lexical), or nil when the path,
393
+ # symlink-resolved on both sides, escapes every allowed root.
394
+ def containment_root(path)
395
+ allowed_roots = [@root_dir] + @additional_roots.map { |r| File.expand_path(r.to_s, @root_dir) }
396
+ root = allowed_roots.find { |r| Rubik::Platform.resolved_path_under?(path, r) }
397
+ return [:root, root] if root
398
+
399
+ ext = @allowed_external_dirs.map { |d| File.expand_path(d.to_s, @root_dir) }
400
+ .find { |d| Rubik::Platform.resolved_path_under?(path, d) }
401
+ ext && [:external, ext]
402
+ end
403
+
404
+ def external_modification_policy
405
+ raw = if @config.is_a?(Rubik::Config)
406
+ @config.fetch("buffer.on_external_modification", "skip")
407
+ elsif @config.respond_to?(:dig)
408
+ @config.dig("buffer", "on_external_modification") ||
409
+ (@config["buffer.on_external_modification"] if @config.respond_to?(:[])) ||
410
+ "skip"
411
+ else
412
+ "skip"
413
+ end
414
+ raw.to_s.downcase
415
+ end
416
+
417
+ def disk_hash(rel_path)
418
+ full_path = File.expand_path(rel_path, @root_dir)
419
+ return nil unless File.exist?(full_path)
420
+
421
+ Digest::SHA256.hexdigest(Rubik::Platform.normalize_newlines(Rubik::Platform.read_text(full_path)))
422
+ rescue Errno::ENOENT, Errno::EACCES
423
+ nil
424
+ end
425
+ end
426
+ end
427
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Buffer
5
+ module PathValidator
6
+ # Regex for "simple filenames" — alphanumeric, hyphens, underscores only.
7
+ # Covers Gemfile, Dockerfile, LICENSE, README, Makefile, CHANGELOG, TODO,
8
+ # Vagrantfile, Procfile, and similar convention filenames without extensions.
9
+ SIMPLE_FILENAME_RE = /\A[a-zA-Z][a-zA-Z0-9_-]*\z/
10
+
11
+ # Characters that are never valid in a file path — typically indicate LLM
12
+ # code fragment leakage (trailing comma from argument lists, leading ?
13
+ # from Ruby symbol syntax, shell wildcards, etc.).
14
+ INVALID_PATH_CHARS = [",", "?", "*", '"', "<", ">", "|", "\0"].freeze
15
+
16
+ class << self
17
+ # Checks whether a file path is valid for writing.
18
+ #
19
+ # A path is valid if it:
20
+ # 1. Is non-nil and non-blank
21
+ # 2. Contains no invalid characters (see INVALID_PATH_CHARS)
22
+ # 3. Matches at least one positive pattern:
23
+ # a. A "simple filename" (alphanumeric + hyphens/underscores only)
24
+ # b. Contains a dot followed by at least one character (extension)
25
+ # c. Contains a path separator (/ or \)
26
+ #
27
+ # @param path [String, nil] the path to validate
28
+ # @return [Boolean] true if the path is valid
29
+ def valid?(path)
30
+ return false if path.nil? || path.to_s.strip.empty?
31
+
32
+ cleaned = path.to_s.strip
33
+
34
+ # 1. NEGATIVE CHECK: Reject paths with invalid characters.
35
+ # Runs BEFORE positive checks — catches LLM code fragment leakage
36
+ # (trailing comma, leading question mark, etc.).
37
+ return false if contains_invalid_chars?(cleaned)
38
+
39
+ # 2. Simple filename (alphanumeric + hyphens/underscores, no extension)
40
+ return true if SIMPLE_FILENAME_RE.match?(cleaned)
41
+
42
+ # 3. Valid file extension — contains a dot with at least 1 char after
43
+ return true if cleaned.include?(".") && !cleaned.end_with?(".")
44
+
45
+ # 4. Contains a path separator
46
+ return true if cleaned.include?("/") || cleaned.include?("\\")
47
+
48
+ # 5. No condition matched — garbage
49
+ false
50
+ end
51
+
52
+ private
53
+
54
+ def contains_invalid_chars?(cleaned)
55
+ cleaned.chars.any? { |c| INVALID_PATH_CHARS.include?(c) }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "json"
5
+ require "fileutils"
6
+ require "securerandom"
7
+
8
+ module Rubik
9
+ module Buffer
10
+ class Snapshot
11
+ attr_reader :id, :timestamp, :files
12
+
13
+ def initialize(id: nil, timestamp: nil, files: {})
14
+ @id = id || SecureRandom.hex(6)
15
+ @timestamp = timestamp || Time.now.to_i
16
+ @files = files
17
+ end
18
+
19
+ def to_h
20
+ {
21
+ "id" => @id,
22
+ "timestamp" => @timestamp,
23
+ "files" => @files
24
+ }
25
+ end
26
+
27
+ def self.from_h(hash)
28
+ new(
29
+ id: hash["id"],
30
+ timestamp: hash["timestamp"],
31
+ files: hash["files"] || {}
32
+ )
33
+ end
34
+
35
+ def compress
36
+ Zlib::Deflate.deflate(JSON.dump(to_h))
37
+ end
38
+
39
+ def self.decompress(binary_data)
40
+ decompressed = Zlib::Inflate.inflate(binary_data)
41
+ from_h(JSON.parse(decompressed))
42
+ end
43
+ end
44
+
45
+ class SnapshotStore
46
+ attr_reader :storage_dir, :snapshots
47
+
48
+ def initialize(storage_dir: File.join(ENV.fetch("HOME", nil), ".rubik", "snapshots", "default"))
49
+ @storage_dir = storage_dir
50
+ @snapshots = []
51
+ @current_index = -1
52
+ FileUtils.mkdir_p(@storage_dir)
53
+ end
54
+
55
+ def create_snapshot(buffer_manager)
56
+ return nil unless buffer_manager.dirty?
57
+
58
+ snapshot = Snapshot.new(files: buffer_manager.snapshot_state)
59
+ # Naming reflects the actual codec: this is Zlib::Deflate (RFC 1951),
60
+ # NOT Zstandard despite the "z" prefix. "zst" was misleading and could
61
+ # fool a tool into trying a zstd decompressor. Kept short to preserve
62
+ # the existing file layout.
63
+ file_path = File.join(@storage_dir, "#{snapshot.id}.zdeflate")
64
+ File.binwrite(file_path, snapshot.compress)
65
+
66
+ @snapshots = @snapshots[0..@current_index] if @current_index < @snapshots.size - 1
67
+
68
+ @snapshots << snapshot
69
+ @current_index = @snapshots.size - 1
70
+ snapshot
71
+ end
72
+
73
+ def undo(buffer_manager)
74
+ return false if @current_index.negative?
75
+
76
+ if @current_index.zero?
77
+ buffer_manager.restore_snapshot_state({})
78
+ @current_index = -1
79
+ return true
80
+ end
81
+
82
+ @current_index -= 1
83
+ prev_snapshot = @snapshots[@current_index]
84
+ buffer_manager.restore_snapshot_state(prev_snapshot.files)
85
+ true
86
+ end
87
+
88
+ def rollback(snapshot_id, buffer_manager)
89
+ target_idx = @snapshots.index { |s| s.id == snapshot_id }
90
+ return false unless target_idx
91
+
92
+ target_snapshot = @snapshots[target_idx]
93
+ buffer_manager.restore_snapshot_state(target_snapshot.files)
94
+ @current_index = target_idx
95
+ true
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "buffer/diff"
4
+ require_relative "buffer/manager"
5
+ require_relative "buffer/snapshot"
6
+
7
+ module Rubik
8
+ module Buffer
9
+ end
10
+ end