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,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Security
5
+ module Redactor
6
+ MASK_STRING = "***MASKED***"
7
+
8
+ PATTERNS = [
9
+ /sk-ant-[a-zA-Z0-9_-]{20,}/,
10
+ /sk-[a-zA-Z0-9_-]{20,}/,
11
+ /AKIA[0-9A-Z]{16}/,
12
+ /ghp_[a-zA-Z0-9]{36}/,
13
+ /github_pat_[a-zA-Z0-9_]{22,}/,
14
+ /AIzaSy[a-zA-Z0-9_-]{33}/,
15
+ /\bbearer\s+[a-zA-Z0-9_\-.]{20,}/i,
16
+ /-----BEGIN [A-Z0-9_\s-]+PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9_\s-]+PRIVATE KEY-----/,
17
+ %r{(https?://[^:]+:)([^@]+)@}
18
+ ].freeze
19
+
20
+ ASSIGNMENT_KEY_REGEX = /\b(password|passwd|api_key|secret)(\s*[:=]\s*)(['"][^'"]*['"]|[^\s,;'"]+)/i
21
+ ASSIGNMENT_TOKEN_REGEX = /\b(token)(\s*[:=]\s*)(['"][^'"]*['"]|[^\s,;'"]+)/i
22
+
23
+ # Anahtar-maskeleme desenleri (Session::Recorder#mask_payload ile aynı
24
+ # aile): sır görünümlü anahtarların değerleri desen eşleşmese bile
25
+ # maskelenir; sayaç anahtarları (tokens, max_tokens) istisnadır.
26
+ SECRET_KEY_REGEX = /(api_key|auth|password|secret|_token\b|^token\b)/i
27
+ TOKEN_COUNT_REGEX = /(tokens|token_count|max_tokens)/i
28
+
29
+ def self.redact(text)
30
+ return text unless text.is_a?(String)
31
+ return text if text.empty?
32
+
33
+ # scrub: redact ham geçmişe (tool çıktısı, prompt) dokunur — geçersiz
34
+ # UTF-8 gsub!'ı ArgumentError ile düşürür; total olmak maskenin
35
+ # kendisi bir arıza kaynağı olmasın demektir.
36
+ result = text.dup.scrub
37
+ PATTERNS.each do |pattern|
38
+ if pattern == %r{(https?://[^:]+:)([^@]+)@}
39
+ result.gsub!(pattern, "\\1#{MASK_STRING}@")
40
+ else
41
+ result.gsub!(pattern, MASK_STRING)
42
+ end
43
+ end
44
+
45
+ result.gsub!(ASSIGNMENT_KEY_REGEX) do
46
+ "#{Regexp.last_match(1)}#{Regexp.last_match(2)}#{MASK_STRING}"
47
+ end
48
+
49
+ result.gsub!(ASSIGNMENT_TOKEN_REGEX) do |match|
50
+ prefix = Regexp.last_match(1)
51
+ sep = Regexp.last_match(2)
52
+ val = Regexp.last_match(3)
53
+ if val =~ /\A['"]?\d+['"]?\z/
54
+ match
55
+ else
56
+ "#{prefix}#{sep}#{MASK_STRING}"
57
+ end
58
+ end
59
+
60
+ result
61
+ end
62
+
63
+ # Bir payload ağacında (Hash/Array/String) her metne redact uygular ve
64
+ # sır görünümlü anahtarların değerlerini komple maskeler — kalıcı
65
+ # serileştirmeden önce satır başına çağrılır (transcript sağlığı
66
+ # paketi 2026-10-10; Session::Recorder#mask_payload ile eş davranış).
67
+ # Girdiyi asla mutemez: her düzey yeni yapı döner. Sözleşme: satırlar
68
+ # düz haritalardır (derinlik provider JSON parse'ında max_nesting ile
69
+ # sınırlı) — döngü koruması JSON üretim katının (clean_utf8_deep)
70
+ # işidir; buraya döngülü yapı verilirse StackError çağıranındır.
71
+ def self.mask_value(payload)
72
+ case payload
73
+ when Hash
74
+ payload.each_with_object({}) do |(k, v), h|
75
+ key_str = k.to_s
76
+ is_secret_key = key_str.match?(SECRET_KEY_REGEX) && !key_str.match?(TOKEN_COUNT_REGEX)
77
+ h[k] = is_secret_key ? MASK_STRING : mask_value(v)
78
+ end
79
+ when Array
80
+ payload.map { |v| mask_value(v) }
81
+ when String
82
+ redact(payload)
83
+ else
84
+ payload
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "security/errors"
4
+ require_relative "security/redactor"
5
+ require_relative "security/guard"
6
+ require_relative "security/classifier"
7
+
8
+ module Rubik
9
+ module Security
10
+ end
11
+ end
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require_relative "replayer"
6
+ require_relative "../llm/message_sanitizer"
7
+ require_relative "../prompt/context_compactor"
8
+
9
+ module Rubik
10
+ module Session
11
+ class Manager
12
+ PROMPT_EVENT_TYPES = ["prompt", "input", "user_turn", "repl_command"].freeze
13
+
14
+ def self.project_sessions_dir(root_dir = Dir.pwd)
15
+ File.join(File.expand_path(root_dir), ".rubik", "sessions")
16
+ end
17
+
18
+ # Absorb (storage.absorb_artifacts): storage köküne absorbe edilmiş
19
+ # oturum dizini. Recorder oraya yazdığında eski .rubik/sessions yanında
20
+ # görünür kalır; dizin yoksa Dir.directory? zaten atlar. Config threading
21
+ # (review bulgusu 2): recorder storage.root/project OVERRIDE'una saygı
22
+ # duyarak yazar — okuma tarafı da aynı config'i almalı; bare çağrılar
23
+ # (config nil) varsayılan kökte kalır.
24
+ def self.absorbed_sessions_dir(root_dir = Dir.pwd, config = nil)
25
+ Rubik::Storage::Layout.absorbed_sessions_dir(Rubik::Storage::Layout.storage_root(config),
26
+ Rubik::Storage::Layout.project_slug(root_dir, config))
27
+ end
28
+
29
+ def self.global_sessions_dir(home_dir = ENV.fetch("HOME", nil))
30
+ File.join(File.expand_path(home_dir || "."), ".rubik", "sessions")
31
+ end
32
+
33
+ def self.list_sessions(root_dir = Dir.pwd, home_dir = ENV.fetch("HOME", nil), config: nil)
34
+ dirs = [absorbed_sessions_dir(root_dir, config), project_sessions_dir(root_dir), global_sessions_dir(home_dir)]
35
+ sessions = []
36
+
37
+ dirs.each_with_index do |dir, idx|
38
+ scope = idx < 2 ? :project : :global
39
+ next unless File.directory?(dir)
40
+
41
+ Dir.glob(File.join(dir, "*.jsonl")).each do |file|
42
+ meta = parse_session_file(file, scope)
43
+ sessions << meta if meta
44
+ end
45
+ end
46
+
47
+ # Sort by mtime descending (most recent first)
48
+ sessions.sort_by { |s| s[:mtime] }.reverse
49
+ end
50
+
51
+ def self.find_latest_session(root_dir = Dir.pwd, home_dir = ENV.fetch("HOME", nil), exclude_file: nil, config: nil)
52
+ # Absorbed (project-scope) first, then legacy project, then global.
53
+ [absorbed_sessions_dir(root_dir, config), project_sessions_dir(root_dir), global_sessions_dir(home_dir)].each do |dir|
54
+ latest = latest_jsonl_in(dir, exclude_file)
55
+ return latest if latest
56
+ end
57
+ nil
58
+ end
59
+
60
+ def self.latest_jsonl_in(dir, exclude_file)
61
+ return nil unless File.directory?(dir)
62
+
63
+ files = Dir.glob(File.join(dir, "*.jsonl"))
64
+ files.reject! { |f| File.expand_path(f) == File.expand_path(exclude_file) } if exclude_file
65
+ files.sort_by! { |f| File.mtime(f) }
66
+ files.last
67
+ end
68
+
69
+ def self.find_session(identifier, root_dir = Dir.pwd, home_dir = ENV.fetch("HOME", nil), config: nil)
70
+ return identifier if File.exist?(identifier.to_s)
71
+
72
+ id_str = identifier.to_s.strip
73
+ all = list_sessions(root_dir, home_dir, config: config)
74
+
75
+ # Match exact filename, basename without ext, or prefix
76
+ matched = all.find do |s|
77
+ s[:name] == id_str ||
78
+ s[:name].sub(/\.jsonl$/, "") == id_str ||
79
+ s[:id].start_with?(id_str) ||
80
+ File.basename(s[:path]).include?(id_str)
81
+ end
82
+
83
+ matched ? matched[:path] : nil
84
+ end
85
+
86
+ # Delete recorded session files older than `days`, protecting the
87
+ # currently active session file (exclude_file). Project sessions are
88
+ # pruned by default; pass include_global: true to fold in the
89
+ # ~/.rubik/sessions scope. Dry-run by default (apply: false) so
90
+ # callers can preview the blast radius; pass apply: true to actually
91
+ # unlink. Returns a report hash so callers — and tests — can assert
92
+ # on the outcome without touching the disk.
93
+ def self.prune_sessions(days:, root_dir: Dir.pwd, home_dir: ENV.fetch("HOME", nil),
94
+ exclude_file: nil, apply: false, include_global: false, config: nil)
95
+ days = days.to_f
96
+ days = 7.0 unless days.positive?
97
+ cutoff = Time.now - (days * 86_400)
98
+ protected = exclude_file && File.expand_path(exclude_file.to_s)
99
+
100
+ candidates = []
101
+ protected_skipped = 0
102
+ list_sessions(root_dir, home_dir, config: config).each do |s|
103
+ next if s[:scope] == :global && !include_global
104
+
105
+ if protected && same_path?(File.expand_path(s[:path]), protected)
106
+ protected_skipped += 1
107
+ next
108
+ end
109
+ candidates << s if s[:mtime] < cutoff
110
+ end
111
+
112
+ bytes = candidates.sum { |s| session_file_size(s[:path]) }
113
+
114
+ deleted = []
115
+ if apply
116
+ candidates.each do |s|
117
+ File.delete(s[:path])
118
+ deleted << s[:path]
119
+ rescue StandardError
120
+ # a file raced away between listing and unlink — ignore
121
+ end
122
+ end
123
+
124
+ {
125
+ cutoff: cutoff,
126
+ older_than_days: days,
127
+ candidates: candidates.size,
128
+ deleted: deleted.size,
129
+ bytes: bytes,
130
+ applied: apply,
131
+ protected_skipped: protected_skipped,
132
+ sample: candidates.first(5).map { |s| s[:id] }
133
+ }
134
+ end
135
+
136
+ def self.session_file_size(path)
137
+ File.size(path)
138
+ rescue SystemCallError
139
+ 0
140
+ end
141
+
142
+ # Case-insensitive on Windows (its filesystem folds case), exact
143
+ # elsewhere — so the protected active-session comparison can never
144
+ # fail to match on casing and let prune delete the live file.
145
+ def self.same_path?(left, right)
146
+ return false if left.nil? || right.nil?
147
+
148
+ Rubik::Platform.windows? ? left.casecmp?(right) : left == right
149
+ end
150
+
151
+ def self.restore_session(shell, session_file)
152
+ return { ok: false, error: "Session file not found: #{session_file}" } unless File.exist?(session_file)
153
+
154
+ events = []
155
+ turns = []
156
+ last_prompt = nil
157
+
158
+ File.foreach(session_file) do |line|
159
+ next if line.strip.empty?
160
+
161
+ begin
162
+ event = JSON.parse(line)
163
+ rescue JSON::ParserError
164
+ next
165
+ end
166
+
167
+ events << event
168
+ type = event["type"].to_s
169
+ payload = event["payload"]
170
+
171
+ case type
172
+ when "user_turn"
173
+ content = payload.is_a?(Hash) ? (payload["content"] || payload.to_s) : payload.to_s
174
+ content = content.sub(/\A!goal\s+/i, "Goal: ")
175
+ turns << { role: "user", content: content }
176
+ last_prompt = content
177
+ when "assistant_response"
178
+ content = payload.is_a?(Hash) ? (payload["content"] || payload.to_s) : payload.to_s
179
+ turns << { role: "assistant", content: content }
180
+ when "repl_command"
181
+ if payload.is_a?(Hash)
182
+ cmd = payload["command"]
183
+ out = payload["output"]
184
+ if cmd && !cmd.to_s.strip.empty?
185
+ # Bang syntax stays out of the restored LLM context (Plan 53 D-8);
186
+ # the session file itself keeps the literal raw command.
187
+ clean = cmd.to_s.delete_prefix("!")
188
+ turns << { role: "user", content: clean }
189
+ last_prompt = clean
190
+ turns << { role: "assistant", content: out.to_s } if out && !out.to_s.strip.empty?
191
+ end
192
+ end
193
+ when "input", "prompt"
194
+ content = if payload.is_a?(Hash)
195
+ payload["raw"] || payload["content"] || payload["input"] || payload.to_s
196
+ else
197
+ payload.to_s
198
+ end
199
+ turns << { role: "user", content: content }
200
+ last_prompt = content
201
+ when "output", "assistant"
202
+ content = if payload.is_a?(Hash)
203
+ payload["content"] || payload["output"] || payload.to_s
204
+ else
205
+ payload.to_s
206
+ end
207
+ turns << { role: "assistant", content: content }
208
+ end
209
+ end
210
+
211
+ if shell.respond_to?(:conversation_history) && shell.conversation_history
212
+ shell.conversation_history.clear
213
+ # Plan 61 D-6/D-12: merge consecutive same-role turns left by
214
+ # failed/legacy sessions, then fit the window to the model's
215
+ # token budget under the shared conversation-window policy.
216
+ turns = Rubik::LLM::MessageSanitizer.normalize(turns)
217
+ ceiling = begin
218
+ spec = shell.config.model_spec(:default)
219
+ shell.llm_registry.effective_input_limit_for(spec["provider"], spec["name"])
220
+ rescue StandardError => e
221
+ Rubik::Log.warn(:session, "input ceiling lookup failed during restore (#{e.class}: #{e.message}); using nil budget")
222
+ nil
223
+ end
224
+ window = Rubik::Prompt::ContextCompactor.trim_history_to_budget(
225
+ turns, ceiling: ceiling,
226
+ max_entries: Rubik::Prompt::ContextCompactor::DEFAULT_MAX_CONVERSATION_ENTRIES
227
+ )
228
+ window.each { |t| shell.conversation_history << t }
229
+ end
230
+
231
+ shell.recorder.switch_session_file(session_file) if shell.respond_to?(:recorder) && shell.recorder
232
+
233
+ {
234
+ ok: true,
235
+ path: session_file,
236
+ turns_hydrated: turns.size,
237
+ event_count: events.size,
238
+ last_prompt: last_prompt || "No prompt",
239
+ replayed_count: turns.size
240
+ }
241
+ rescue StandardError => e
242
+ { ok: false, error: "Failed to restore session: #{e.message}" }
243
+ end
244
+
245
+ def self.parse_session_file(file, scope)
246
+ return nil unless File.exist?(file)
247
+ return nil if File.empty?(file)
248
+
249
+ event_count = 0
250
+ first_line = nil
251
+
252
+ File.open(file, "rb") do |f|
253
+ first_line = f.gets
254
+ return nil unless first_line
255
+
256
+ last_char = first_line[-1]
257
+ event_count = first_line.count("\n")
258
+ buf = +""
259
+ while f.read(65_536, buf)
260
+ event_count += buf.count("\n")
261
+ last_char = buf[-1]
262
+ end
263
+ # Count a final line left unterminated by a truncated write.
264
+ event_count += 1 if last_char && last_char != "\n"
265
+ end
266
+
267
+ first_event = begin
268
+ JSON.parse(first_line)
269
+ rescue JSON::ParserError
270
+ {}
271
+ end
272
+
273
+ last_prompt = nil
274
+ chunk_size = 4096
275
+
276
+ File.open(file, "rb") do |f|
277
+ size = f.size
278
+ pos = size
279
+ buffer = +""
280
+
281
+ while pos.positive? && last_prompt.nil?
282
+ read_len = [pos, chunk_size].min
283
+ pos -= read_len
284
+ f.seek(pos)
285
+ chunk = f.read(read_len)
286
+ buffer.prepend(chunk)
287
+
288
+ lines = buffer.split("\n")
289
+ buffer = pos.positive? ? (lines.shift || +"") : +""
290
+
291
+ lines.reverse_each do |line|
292
+ next if line.strip.empty?
293
+
294
+ ev = begin
295
+ JSON.parse(line)
296
+ rescue JSON::ParserError
297
+ next
298
+ end
299
+
300
+ type = ev["type"].to_s
301
+ next unless PROMPT_EVENT_TYPES.include?(type)
302
+
303
+ payload = ev["payload"]
304
+ last_prompt = if payload.is_a?(Hash)
305
+ payload["content"] || payload["command"] || payload["raw"] || payload.to_s
306
+ else
307
+ payload.to_s
308
+ end
309
+ break
310
+ end
311
+ end
312
+ end
313
+
314
+ start_time = first_event["ts"] || File.mtime(file).iso8601
315
+
316
+ {
317
+ id: File.basename(file, ".jsonl"),
318
+ name: File.basename(file),
319
+ path: file,
320
+ scope: scope,
321
+ mtime: File.mtime(file),
322
+ start_time: start_time,
323
+ event_count: event_count,
324
+ last_prompt: (last_prompt || "No prompt")[0..50]
325
+ }
326
+ rescue StandardError
327
+ nil
328
+ end
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "time"
6
+ require_relative "../platform"
7
+ require_relative "../security/redactor"
8
+
9
+ module Rubik
10
+ module Session
11
+ class Recorder
12
+ MASK_PATTERNS = Rubik::Security::Redactor::PATTERNS
13
+
14
+ MAX_RECENT_EVENTS = 500
15
+
16
+ attr_reader :session_file, :events
17
+
18
+ def initialize(sessions_dir: nil, session_name: nil, root_dir: Dir.pwd, home_dir: ENV.fetch("HOME", nil)) # rubocop:disable Lint/UnusedMethodArgument -- arayüz kwarg'i korunuyor
19
+ @sessions_dir = sessions_dir || File.join(root_dir, ".rubik", "sessions")
20
+ begin
21
+ FileUtils.mkdir_p(@sessions_dir)
22
+ rescue SystemCallError => e
23
+ warn "[WARN] Failed to create sessions directory #{@sessions_dir}: #{e.message}" if ENV["RUBIK_DEBUG"]
24
+ end
25
+
26
+ filename = session_name || "session_#{Time.now.strftime('%Y%m%d_%H%M%S')}_#{Process.pid}_#{(Time.now.to_f * 1000).to_i % 1000}.jsonl"
27
+ @session_file = File.join(@sessions_dir, filename)
28
+ @events = []
29
+ end
30
+
31
+ def record(type, payload)
32
+ masked_payload = mask_payload(payload)
33
+ event = {
34
+ "ts" => Time.now.iso8601,
35
+ "type" => type.to_s,
36
+ "payload" => masked_payload
37
+ }
38
+
39
+ push_event(event)
40
+ File.open(@session_file, "a") do |f|
41
+ f.puts(Rubik::Platform.safe_json_dump(event))
42
+ end
43
+ event
44
+ end
45
+
46
+ def record_user_turn(text)
47
+ record("user_turn", { "role" => "user", "content" => text })
48
+ end
49
+
50
+ # Plan 61: streamed records whether the text was displayed live as
51
+ # chunks — session files become diagnosable when someone later asks
52
+ # "did the answer stream or not?".
53
+ def record_assistant_response(text, tokens: nil, streamed: nil)
54
+ payload = { "role" => "assistant", "content" => text, "tokens" => tokens, "streamed" => streamed }.compact
55
+ record("assistant_response", payload)
56
+ end
57
+
58
+ def record_tool_call(tool_name, args, call_id: nil)
59
+ payload = { "tool" => tool_name, "args" => args, "call_id" => call_id }.compact
60
+ record("tool_call", payload)
61
+ end
62
+
63
+ def record_tool_result(tool_name, output, status: :ok, call_id: nil)
64
+ payload = { "tool" => tool_name, "output" => output, "status" => status, "call_id" => call_id }.compact
65
+ record("tool_result", payload)
66
+ end
67
+
68
+ def record_repl_command(cmd, output)
69
+ record("repl_command", { "command" => cmd, "output" => output })
70
+ end
71
+
72
+ def switch_session_file(target_file, cleanup_empty_current: true)
73
+ if cleanup_empty_current && @session_file && File.exist?(@session_file) && File.expand_path(@session_file) != File.expand_path(target_file)
74
+ begin
75
+ lines = File.readlines(@session_file).reject { |l| l.strip.empty? }
76
+ is_empty_or_only_start = lines.empty? || (lines.size == 1 && (
77
+ begin
78
+ ev = JSON.parse(lines.first)
79
+ ev["type"] == "session_start"
80
+ rescue JSON::ParserError
81
+ false
82
+ end
83
+ ))
84
+ File.delete(@session_file) if is_empty_or_only_start
85
+ rescue StandardError
86
+ # ignore file cleanup error
87
+ end
88
+ end
89
+
90
+ @session_file = target_file
91
+ @events = []
92
+ if File.exist?(@session_file)
93
+ File.foreach(@session_file) do |line|
94
+ next if line.strip.empty?
95
+
96
+ begin
97
+ push_event(JSON.parse(line))
98
+ rescue JSON::ParserError
99
+ # ignore malformed line
100
+ end
101
+ end
102
+ end
103
+
104
+ record("session_resumed", {
105
+ "parent_session_id" => File.basename(target_file, ".jsonl"),
106
+ "resumed_at" => Time.now.iso8601
107
+ })
108
+ end
109
+
110
+ def mask_text(text)
111
+ Rubik::Security::Redactor.redact(text)
112
+ end
113
+
114
+ def read_history(limit = 20)
115
+ inputs = @events.select { |e| ["input", "command", "prompt", "user_turn", "repl_command"].include?(e["type"]) }
116
+ inputs.last(limit)
117
+ end
118
+
119
+ private
120
+
121
+ def push_event(event)
122
+ @events << event
123
+ @events.shift while @events.size > MAX_RECENT_EVENTS
124
+ event
125
+ end
126
+
127
+ def mask_payload(payload)
128
+ case payload
129
+ when Hash
130
+ payload.each_with_object({}) do |(k, v), h|
131
+ key_str = k.to_s
132
+ is_secret_key = key_str.match?(/(api_key|auth|password|secret|_token\b|^token\b)/i) && !key_str.match?(/(tokens|token_count|max_tokens)/i)
133
+ h[k] = if is_secret_key
134
+ "***MASKED***"
135
+ else
136
+ mask_payload(v)
137
+ end
138
+ end
139
+ when Array
140
+ payload.map { |v| mask_payload(v) }
141
+ when String
142
+ mask_text(payload)
143
+ else
144
+ payload
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Rubik
6
+ module Session
7
+ class Replayer
8
+ DESTRUCTIVE_COMMANDS = ["apply", "sh", "rm", "execute"].freeze
9
+
10
+ attr_reader :shell, :replayed_events, :skipped_events
11
+
12
+ def initialize(shell)
13
+ @shell = shell
14
+ @replayed_events = []
15
+ @skipped_events = []
16
+ end
17
+
18
+ def replay_file(session_file_path)
19
+ return false unless File.exist?(session_file_path)
20
+
21
+ lines = File.readlines(session_file_path)
22
+ @shell&.instance_variable_set(:@replaying, true)
23
+ lines.each do |line|
24
+ next if line.strip.empty?
25
+
26
+ event = begin
27
+ JSON.parse(line)
28
+ rescue JSON::ParserError
29
+ nil
30
+ end
31
+ process_event(event) if event
32
+ end
33
+ true
34
+ ensure
35
+ @shell&.instance_variable_set(:@replaying, false)
36
+ end
37
+
38
+ private
39
+
40
+ def process_event(event)
41
+ type = event["type"]
42
+ payload = event["payload"]
43
+
44
+ return unless ["input", "command"].include?(type)
45
+
46
+ cmd_text = payload.is_a?(Hash) ? (payload["raw"] || payload["input"]) : payload.to_s
47
+
48
+ if destructive?(cmd_text)
49
+ @skipped_events << { event: event, reason: "[skipped: destructive]" }
50
+ else
51
+ @replayed_events << event
52
+ @shell.execute_input(cmd_text)
53
+ end
54
+ end
55
+
56
+ def destructive?(cmd_text)
57
+ stripped = cmd_text.to_s.strip
58
+ return false unless stripped.start_with?("!")
59
+
60
+ cmd_name = stripped[1..].split(/\s+/, 2).first.to_s.downcase
61
+ DESTRUCTIVE_COMMANDS.include?(cmd_name)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "session/recorder"
4
+ require_relative "session/replayer"
5
+ require_relative "session/manager"
6
+
7
+ module Rubik
8
+ module Session
9
+ end
10
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Rubik
6
+ module Skills
7
+ # Splits a SKILL.md into its YAML frontmatter mapping and markdown body.
8
+ # Deliberately a hand-rolled fence split + YAML.safe_load — the body is
9
+ # markdown (frequently containing "---" horizontal rules), so YAML stream
10
+ # loading would misparse nearly every real skill, and safe_load refuses
11
+ # object deserialization, keeping the open-trust model honest (a skill
12
+ # can never instantiate Ruby objects).
13
+ class Frontmatter
14
+ class Error < StandardError; end
15
+
16
+ # Opening fence must be the first line (spec); the non-greedy body
17
+ # match stops at the FIRST closing fence, so "---" rules inside the
18
+ # markdown body are preserved verbatim.
19
+ FENCE_RE = /\A---[ \t]*\n(.+?)\n---[ \t]*(?:\n|\z)/m
20
+
21
+ # => { frontmatter: Hash, body: String }; raises Frontmatter::Error on
22
+ # any structural problem (caller collects per-skill, never aborts a scan).
23
+ def self.parse(raw)
24
+ text = raw.to_s.delete("\r").sub(/\A\uFEFF/, "")
25
+ match = FENCE_RE.match(text)
26
+ raise Error, "missing '---' frontmatter fences" unless match
27
+
28
+ data = begin
29
+ YAML.safe_load(match[1])
30
+ rescue Psych::Exception => e
31
+ # Psych::Exception subsumes SyntaxError and AliasesNotEnabled
32
+ # (safe_load refuses aliases and object tags by default).
33
+ raise Error, "invalid YAML frontmatter: #{e.message}"
34
+ end
35
+ raise Error, "frontmatter must be a YAML mapping" unless data.is_a?(Hash)
36
+
37
+ { frontmatter: data, body: match.post_match.to_s.lstrip }
38
+ end
39
+ end
40
+ end
41
+ end