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,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../prompt/context_compactor"
4
+ require_relative "../coeffects"
5
+
6
+ module Rubik
7
+ module REPL
8
+ # Plan 61: cross-turn conversational memory policy, included by Shell.
9
+ # Covers sticky route resolution (D-7), per-turn outcome recording for
10
+ # success/failure/cancel (D-2), and the token-budgeted conversation
11
+ # window (D-12). All state lives on Shell; this module only encodes
12
+ # the policy.
13
+ module TurnRecorder
14
+ MAX_CONVERSATION_ENTRIES = Rubik::Prompt::ContextCompactor::DEFAULT_MAX_CONVERSATION_ENTRIES
15
+
16
+ # Vakanüvis kök-turn olay adı — harness'in TURN_EVENT sabitiyle eş
17
+ # pinlenir (testler iki taraftan kilitler; isim çelişkisi koşuldan
18
+ # önce patlar).
19
+ TURN_EVENT_TARGET = :turn_finished
20
+
21
+ # Plan 61 D-7: sticky routing. Once a conversation has started,
22
+ # free-text turns keep the established provider/model instead of
23
+ # re-running keyword classification every turn (mid-conversation
24
+ # model/style/context-limit churn). `!model` resets the stamp via
25
+ # sync_default_provider; explicit task types (!plan/!goal) never
26
+ # touch it. Disable with `!config set router.sticky false`.
27
+ def resolve_turn_route(text)
28
+ sticky = @config.fetch("router.sticky", true) != false
29
+ return @conversation_route_stamp if sticky && @conversation_route_stamp && !@conversation_history.empty?
30
+
31
+ routed = @router.route(task_type: :auto, prompt: text)
32
+ @conversation_route_stamp = routed if routed && routed[:provider]
33
+ routed
34
+ end
35
+
36
+ # Plan 61 D-2: every free-text turn — success, failure, cancel —
37
+ # leaves a user/assistant pair in the conversation window so the
38
+ # model keeps the thread and !resume restores it. Failures record a
39
+ # tagged trace plus the attempted-work summary so the next turn knows
40
+ # what was already done.
41
+ def record_turn_outcome!(user_text, assistant_content, tokens: nil, streamed: nil)
42
+ return if user_text.to_s.strip.empty?
43
+ return if assistant_content.nil? || assistant_content.to_s.strip.empty?
44
+
45
+ @conversation_history << { role: "user", content: user_text }
46
+ @conversation_history << { role: "assistant", content: assistant_content.to_s }
47
+ trim_conversation_window!
48
+ @recorder&.record_assistant_response(assistant_content.to_s, tokens: tokens, streamed: streamed)
49
+ rescue StandardError => e
50
+ Rubik::Platform.debug_warn("turn outcome recording failed (#{e.class}: #{e.message}); turn lost from conversation memory")
51
+ nil
52
+ end
53
+
54
+ def record_failed_turn!(user_text, kind, reason, orchestrator)
55
+ return if @config.fetch("context.log_failed_turns", true) == false
56
+
57
+ label = case kind
58
+ when "cancelled" then "[Turn cancelled by user (^C)]"
59
+ when "incomplete" then "[Turn incomplete: #{reason}]"
60
+ else "[Turn failed: #{reason}]"
61
+ end
62
+ summary = if orchestrator.respond_to?(:activity)
63
+ orchestrator.activity.summary(orchestrator.buffer_manager).to_s.strip
64
+ else
65
+ ""
66
+ end
67
+
68
+ # Plan 85 D-3: kronolojik tool listesi (D-2'deki chronological_summary).
69
+ chronicle_enabled = @config.fetch("context.tool_chronicle_in_history", true)
70
+ max_entries = @config.fetch("context.tool_chronicle_max_entries", 15).to_i
71
+ tool_chronicle = if chronicle_enabled &&
72
+ orchestrator.respond_to?(:activity) &&
73
+ orchestrator.activity.respond_to?(:chronological_summary)
74
+ chron = orchestrator.activity.chronological_summary(max_entries: max_entries).strip
75
+ chron.empty? ? "" : "\nTool calls made:\n#{chron}"
76
+ else
77
+ ""
78
+ end
79
+
80
+ parts = []
81
+ parts << label
82
+ parts << "Partial progress before this outcome:\n#{summary}" unless summary.empty?
83
+ parts << tool_chronicle unless tool_chronicle.empty?
84
+ body = "#{parts.first}#{parts[1..].map { |p| "\n#{p}" }.join}"
85
+
86
+ # R9: log_failed_turns kapalıysa başarısız turn hiçbir yere
87
+ # kaydedilmez — kronik de dahil (least surprise).
88
+ emit_turn_finished(kind: kind, prompt: user_text, outcome: body, orchestrator: orchestrator)
89
+ record_turn_outcome!(user_text, body)
90
+ rescue StandardError => e
91
+ Rubik::Platform.debug_warn("failed-turn recording crashed (#{e.class}: #{e.message}); failure context lost")
92
+ nil
93
+ end
94
+
95
+ # Vakanüvis kök-turn kapısı (root-turn planı 2026-10-04): her REPL
96
+ # turn'ünün sonu deterministik tek satıra indirgenip bus'a düşer.
97
+ # Sessizlik sözleşmesi aynen — :turn_finished WAKE_KINDS dışındadır,
98
+ # tek abone vakanüvis harness'idir; notice yayılmaz. Emisyon asla
99
+ # turn'ü bozmaz: bus yoksa no-op, patlarsa debug_warn.
100
+ def emit_turn_finished(kind:, prompt:, outcome:, orchestrator: nil, tokens: nil)
101
+ return unless @event_bus.respond_to?(:post)
102
+
103
+ # Süs bozulursa olay düşmesin: tally degradasyonu kendi rescue'sunda
104
+ # ("") kalır, post için ayrılan dış rescue bozulmuş bus'u yakalar.
105
+ tools = begin
106
+ orchestrator.respond_to?(:activity) ? orchestrator.activity.tool_tally : ""
107
+ rescue StandardError => e
108
+ Rubik::Platform.debug_warn("tool tally failed (#{e.class}: #{e.message}); chronicling without tools")
109
+ ""
110
+ end
111
+ # Loop'ün token sayacı Hash'tir ({input:, output:}) — kronik satırı
112
+ # tek skaler ister; Hash toplamı alınır (Numeric olmayan üye sessizce
113
+ # elenir — üretici asla böyle göndermez, savunma derinliği), diğer
114
+ # şekiller to_i'ye iner. Düzleşme burada (üretici) VE turn_line'da
115
+ # (derinlemesine savunma) yaşar: iki taraf da tek başına dayanıklı.
116
+ tokens_val = tokens.is_a?(Hash) ? tokens.values.filter_map { |v| v.to_i if v.respond_to?(:to_i) }.sum : tokens.to_i
117
+ @event_bus.post(TURN_EVENT_TARGET,
118
+ { kind: kind.to_s, prompt: prompt.to_s, outcome: outcome.to_s,
119
+ tools: tools.to_s, tokens: tokens_val }, subsystem: :repl)
120
+ rescue StandardError => e
121
+ Rubik::Platform.debug_warn("turn-finished emission failed (#{e.class}: #{e.message}); turn not chronicled")
122
+ nil
123
+ end
124
+
125
+ # Plan 61 D-12: the long-lived REPL memory is trimmed by a token
126
+ # budget (plus an absolute entry cap) so one huge turn cannot evict
127
+ # the whole thread, and eviction order stays oldest-first.
128
+ def trim_conversation_window!
129
+ ceiling = begin
130
+ spec = current_model_spec
131
+ @llm_registry.effective_input_limit_for(spec["provider"], spec["name"])
132
+ rescue StandardError => e
133
+ Rubik::Platform.debug_warn("input ceiling lookup failed (#{e.class}: #{e.message}); using nil budget")
134
+ nil
135
+ end
136
+ @conversation_history = Rubik::Prompt::ContextCompactor.trim_history_to_budget(
137
+ @conversation_history, ceiling: ceiling, max_entries: MAX_CONVERSATION_ENTRIES
138
+ )
139
+ rescue StandardError => e
140
+ Rubik::Log.warn(:repl, "conversation trim failed (#{e.class}: #{e.message}); window may exceed token budget")
141
+ nil
142
+ end
143
+
144
+ # Plan 85 D-7 (kök neden): turn sonunda turn içi @history'deki tool ara
145
+ # adımlarını (role: "tool" mesajları) conversation_history'ye kopyalar;
146
+ # hata veya başarı fark etmez. Böylece continue'da model turn sırasındaki
147
+ # tüm bağlamı görür ve "sıfırdan keşfe" başlamaz.
148
+ # loop.rb'ye dokunmaz: caller res[:loop_instance].history üzerinden turn
149
+ # içi @history'yi okur (turn_result zaten loop_instance + history accessor
150
+ # döndürür).
151
+ def merge_turn_tool_history!(turn_history, max_entries: 20)
152
+ return false if turn_history.nil? || turn_history.empty?
153
+ return false unless @config.fetch("context.merge_turn_tool_history", true)
154
+
155
+ # Yalnızca tool sonucu taşıyan mesajları filtrele; en SON max_entries
156
+ # adımı al — bir sonraki turn'ün ihtiyacı olan, turn'in güncel arazi
157
+ # durumudur (hangi edit'ler tuttu, son hatalar), turn'in başı değil.
158
+ tool_msgs = turn_history.select do |m|
159
+ role = m[:role] || m["role"]
160
+ role == "tool" || m[:tool_calls] || m["tool_calls"]
161
+ end.last(max_entries)
162
+ return false if tool_msgs.empty?
163
+
164
+ @conversation_history.concat(tool_msgs)
165
+
166
+ # conversation_history artık tam tool sonuçları taşıdığından trim zorunludur.
167
+ trim_conversation_window!
168
+ true
169
+ rescue StandardError => e
170
+ Rubik::Platform.debug_warn("tool-history merge failed (#{e.class}: #{e.message}); next turn starts without tool context")
171
+ nil
172
+ end
173
+
174
+ # Plan 61 D-13: the model spec the CURRENT conversation is bound to
175
+ # (sticky stamp when present), falling back to the default role.
176
+ def current_model_spec
177
+ stamp = @conversation_route_stamp
178
+ return stamp[:spec] if stamp.is_a?(Hash) && stamp[:spec].is_a?(Hash) && stamp[:spec]["provider"]
179
+
180
+ @config.model_spec(:default)
181
+ end
182
+
183
+ # Pruner apply seam (context-pruner 2026-10-09): turn thread'inde,
184
+ # orchestrator.run pencereyi tohumlamadan HEMEN ÖNCE çağrılır — hem
185
+ # execute_free_text (input_execution.rb) hem !goal (agent_execution.rb
186
+ # ctx.apply_staged_prune_plan! üzerinden). Kopyala-doğrula-takas:
187
+ # take_applicable_plan! yeni dizi döner; herhangi bir ihlalde nil ve
188
+ # pencere olduğu gibi kalır. Replay sırasında atlanır (yeniden
189
+ # oynatılan oturumu budamak kaydı çarpıtır); pruner kapalıysa (coeffect
190
+ # yok) no-op'tur.
191
+ def apply_staged_prune_plan!
192
+ return if @replaying
193
+
194
+ harness = Rubik::Coeffects.get(@runtime_manager&.context, Rubik::Coeffects::PRUNER)
195
+ return unless harness
196
+
197
+ new_window, receipt = harness.take_applicable_plan!(@conversation_history)
198
+ return unless new_window
199
+
200
+ @conversation_history = new_window
201
+ line = "[Pruner] Applied staged plan: dropped #{receipt[:dropped]} unit(s), " \
202
+ "summarized #{receipt[:summarized]} (#{receipt[:skipped_stale]} stale op(s) skipped) — " \
203
+ "window ~#{receipt[:before_tokens]} -> ~#{receipt[:after_tokens]} tokens."
204
+ if @terminal_guard
205
+ @terminal_guard.print_safe_notice(line)
206
+ else
207
+ @output.puts(line)
208
+ end
209
+ rescue StandardError => e
210
+ Rubik::Log.warn(:repl, "staged prune apply failed (#{e.class}: #{e.message}); window untouched")
211
+ nil
212
+ end
213
+
214
+ # Session continuity (Plan 34/61): hydrate a recorded session into
215
+ # the live conversation window.
216
+ def resume_session(identifier = :latest)
217
+ root_d = @buffer_manager&.root_dir || Dir.pwd
218
+ home_d = @config&.home_dir || ENV.fetch("HOME", nil)
219
+
220
+ target_file = if identifier == :latest || identifier.to_s.downcase == "latest"
221
+ Rubik::Session::Manager.find_latest_session(root_d, home_d, exclude_file: @recorder&.session_file, config: @config)
222
+ else
223
+ Rubik::Session::Manager.find_session(identifier, root_d, home_d, config: @config)
224
+ end
225
+
226
+ return { ok: false, error: "No past session found matching '#{identifier}'" } unless target_file
227
+
228
+ res = Rubik::Session::Manager.restore_session(self, target_file)
229
+ @output.puts "[Rubik::Session] Resumed session: #{File.basename(target_file)} (#{res[:turns_hydrated]} dialogue turns hydrated, 0 API tokens used)" if res[:ok]
230
+ res
231
+ end
232
+ end
233
+ end
234
+ end
data/lib/rubik/repl.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "repl/command_parser"
4
+ require_relative "repl/hooks"
5
+ require_relative "repl/step_formatter"
6
+ require_relative "repl/commands"
7
+ require_relative "repl/terminal_guard"
8
+ require_relative "repl/pruner"
9
+ require_relative "repl/shell"
10
+
11
+ module Rubik
12
+ module REPL
13
+ end
14
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../ast"
4
+ require_relative "../coeffects"
5
+ require_relative "service_registration"
6
+
7
+ require_relative "../platform"
8
+
9
+ module Rubik
10
+ module Runtime
11
+ module ASTService
12
+ module_function
13
+
14
+ def register(context_or_manager, ast_graph, cache_store: nil)
15
+ ServiceRegistration.call(context_or_manager) do |ctx|
16
+ next ast_graph unless ctx
17
+
18
+ ctx.provide(Rubik::Coeffects::AST_GRAPH, ast_graph)
19
+
20
+ ctx.on_dispose do
21
+ begin
22
+ ast_graph.save_to_cache(cache_store) if cache_store && ast_graph.respond_to?(:save_to_cache)
23
+ rescue StandardError => e
24
+ # Report but keep draining: a failed cache save must not abort
25
+ # the LIFO unwind, and silence here would leak a stale symbol
26
+ # cache with no trace.
27
+ warn "[ASTService] cache save failed: #{e.class}: #{e.message}"
28
+ end
29
+ ast_graph.clear if ast_graph.respond_to?(:clear)
30
+ end
31
+
32
+ ast_graph
33
+ end
34
+ end
35
+
36
+ # Drop graph entries for files inside the project root that the context
37
+ # scanner no longer reports — e.g. subtrees that became covered by an
38
+ # ignore rule (the scanner's whole-subtree pruning is stronger than the
39
+ # legacy one-level fnmatch filter). Without this, symbols loaded from a
40
+ # pre-upgrade cache would linger in the graph and be re-saved forever.
41
+ # Guarded on a non-empty scan so a failed scan can never wipe the graph.
42
+ # Files outside the primary root (additional roots, external dirs) are
43
+ # untouched: the context index may not cover them.
44
+ # Returns the number of purged entries.
45
+ def purge_stale_entries!(ast_graph, context_index, root_dir)
46
+ files = context_index.files
47
+ return 0 if files.empty?
48
+
49
+ is_win = Rubik::Platform.windows?
50
+ norm_root = Rubik::Platform.normalize_path_raw(root_dir).chomp("/")
51
+ root_prefix = "#{norm_root}/"
52
+ root_prefix_cmp = is_win ? root_prefix.downcase : root_prefix
53
+
54
+ known_keys = if is_win
55
+ files.keys.to_h { |k| [k.downcase, true] }
56
+ else
57
+ files.keys.to_h { |k| [k, true] }
58
+ end
59
+
60
+ supported = Rubik::AST::Extractor.supported_extensions
61
+ stale = []
62
+
63
+ ast_graph.file_symbols.each_key do |path|
64
+ norm = Rubik::Platform.normalize_path_raw(path)
65
+ norm_cmp = is_win ? norm.downcase : norm
66
+ next unless norm_cmp.start_with?(root_prefix_cmp)
67
+
68
+ rel = norm[root_prefix.length..]
69
+ rel_key = is_win ? rel.downcase : rel
70
+ stale << path if supported.include?(File.extname(rel).downcase) && !known_keys.key?(rel_key)
71
+ end
72
+
73
+ stale.each { |path| ast_graph.remove_file(path) }
74
+ stale.size
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+ require_relative "../coeffects"
5
+
6
+ module Rubik
7
+ module Runtime
8
+ class CoeffectMissingError < StandardError; end
9
+
10
+ # Raised when a disposed context is asked to create new scope. Unlike a
11
+ # late inverse (which can run now), there is no meaningful fallback for
12
+ # extend, so it fails fast (AGENTS.md Fail Fast template).
13
+ class ContextDisposedError < StandardError; end
14
+
15
+ class Context
16
+ attr_reader :parent
17
+
18
+ # Legacy alias -> canonical key, derived from the Rubik::Coeffects SSOT
19
+ # (AGENTS.md §1.B). Providers publish canonical keys only; consumers may
20
+ # still ask with documented legacy names (e.g. plugin DSL inject: [:tools]).
21
+ CANONICAL_OF = Rubik::Coeffects::ALIAS_TO_CANONICAL
22
+ # Canonical key -> legacy alias spellings (forward half of the SSOT).
23
+ LEGACY_ALIASES = Rubik::Coeffects::LEGACY_ALIASES
24
+
25
+ def initialize(parent: nil)
26
+ @parent = parent
27
+ @monitor = Monitor.new
28
+ @services = {}
29
+ # Delete-inverse handles keyed by service / child, so explicit
30
+ # removals (unprovide, child prune) can also retract the inverse
31
+ # from the Omega accumulator instead of leaving dead no-ops behind.
32
+ @service_inverses = {}
33
+ @child_inverses = {}
34
+ @disposables = []
35
+ @children = []
36
+ @disposed = false
37
+ @last_dispose_errors = []
38
+ end
39
+
40
+ def register_disposable(cleanup_fn = nil, &block)
41
+ inverse = cleanup_fn || block
42
+ return unless inverse
43
+
44
+ @monitor.synchronize do
45
+ if @disposed
46
+ # Ω is sealed: the context is already torn down, so there is no
47
+ # later drain that would run this inverse. Execute it now instead
48
+ # of silently storing it in a dead accumulator (leaked cleanup).
49
+ run_inverse(inverse)
50
+ else
51
+ @disposables << inverse
52
+ end
53
+ end
54
+ inverse
55
+ end
56
+ alias on_dispose register_disposable
57
+
58
+ # Number of pending inverse operations in the Omega accumulator.
59
+ # Exposed for runtime visualization (!agents panel, !runtime).
60
+ def disposable_count
61
+ @monitor.synchronize { @disposables.size }
62
+ end
63
+
64
+ def services
65
+ @monitor.synchronize { @services.dup.freeze }
66
+ end
67
+
68
+ def service_names
69
+ @monitor.synchronize { @services.keys }
70
+ end
71
+
72
+ def last_dispose_errors
73
+ @monitor.synchronize { @last_dispose_errors.dup }
74
+ end
75
+
76
+ def provide(name, service)
77
+ key = name.to_s.downcase
78
+ @monitor.synchronize do
79
+ already_provided = @services.key?(key)
80
+ @services[key] = service
81
+ return if already_provided
82
+
83
+ # Exactly one delete-inverse per key for the context's lifetime:
84
+ # re-providing a service swaps the value but never stacks a second
85
+ # inverse into the Omega accumulator (AGENTS.md zero-restart rule).
86
+ # The handle is kept so unprovide can retract it symmetrically.
87
+ @service_inverses[key] = register_disposable { @services.delete(key) }
88
+ end
89
+ end
90
+
91
+ # Removes a service AND its delete-inverse from the accumulator, the
92
+ # symmetric counterpart of provide. Callers own the reactive surface:
93
+ # unlike Runtime::Manager#provide, removal triggers no reconciliation.
94
+ # Re-providing the key afterwards registers a fresh inverse, so
95
+ # provide/unprovide cycles return Ω to its baseline (zero-restart).
96
+ def unprovide(name)
97
+ key = name.to_s.downcase
98
+ @monitor.synchronize do
99
+ return unless @services.key?(key)
100
+
101
+ @services.delete(key)
102
+ inverse = @service_inverses.delete(key)
103
+ @disposables.delete(inverse) if inverse
104
+ end
105
+ end
106
+
107
+ def require(name)
108
+ value = resolve_coeffect(name.to_s.downcase)
109
+ return value unless value.nil?
110
+
111
+ raise CoeffectMissingError, "Required coeffect '#{name}' is not satisfied in the context."
112
+ end
113
+
114
+ def get(name, default = nil)
115
+ val = resolve_coeffect(name.to_s.downcase)
116
+ val.nil? ? default : val
117
+ end
118
+
119
+ # Walk the context chain for a literal key. Returns nil when the key is
120
+ # absent (services are always live objects, never nil, in this runtime).
121
+ # Per-hop locking: each context holds only its own monitor during the
122
+ # local lookup and releases it before recursing to the parent, so
123
+ # chained reads never nest child→parent locks (disposal nests only
124
+ # parent→child; the reverse order would risk deadlock).
125
+ def resolve(key)
126
+ found, local = @monitor.synchronize do
127
+ [@services.key?(key), @services[key]]
128
+ end
129
+ return local if found && !local.nil?
130
+
131
+ @parent&.resolve(key)
132
+ end
133
+
134
+ def satisfied?(dependencies)
135
+ Array(dependencies).all? { |dep| has?(dep) }
136
+ end
137
+
138
+ def has?(name)
139
+ !resolve_coeffect(name.to_s.downcase).nil?
140
+ end
141
+
142
+ # Single-pass coeffect resolution: per hop, under one monitor hold, all
143
+ # three spellings of the requested key are tried atomically — the
144
+ # literal key (a third party may have provided the alias itself), its
145
+ # canonical form (the request used a legacy name), then its legacy
146
+ # aliases (the request used the canonical name but a provider
147
+ # registered the short form). Scoping wins over spelling: the nearest
148
+ # provider wins regardless of which spelling it registered under.
149
+ # Per-hop locking and the parent→child lock order: see `resolve`.
150
+ # Protected (not private): the parent-chain recursion needs an
151
+ # explicit receiver — the same boundary as prune_child, so external
152
+ # callers get NoMethodError (closes the accidentally-public
153
+ # resolve_with_alias surface).
154
+ def resolve_coeffect(key)
155
+ local = @monitor.synchronize do
156
+ if @services.key?(key)
157
+ @services[key]
158
+ elsif (canonical = CANONICAL_OF[key]) && @services.key?(canonical)
159
+ @services[canonical]
160
+ else
161
+ aliases = LEGACY_ALIASES[key]
162
+ found = nil
163
+ aliases&.each do |alias_key|
164
+ # key? probing, never truthiness (test_coeffects_falsy).
165
+ next unless @services.key?(alias_key)
166
+
167
+ found = @services[alias_key]
168
+ break
169
+ end
170
+ found
171
+ end
172
+ end
173
+ return local unless local.nil?
174
+
175
+ @parent&.resolve_coeffect(key)
176
+ end
177
+ protected :resolve_coeffect
178
+
179
+ def extend
180
+ child = nil
181
+ @monitor.synchronize do
182
+ if @disposed
183
+ raise ContextDisposedError,
184
+ "Cannot extend a disposed context. To fix, check `disposed?` before extending or create a fresh context."
185
+ end
186
+
187
+ child = Context.new(parent: self)
188
+ @children << child
189
+ # Keep the cascade-inverse handle so a child disposed before its
190
+ # parent (epoch close, per-turn loop context, MCP reconnect) can
191
+ # prune both itself and the dead inverse out of this context.
192
+ @child_inverses[child] = register_disposable { child.dispose unless child.disposed? }
193
+ end
194
+ child
195
+ end
196
+
197
+ def disposed?
198
+ @disposed
199
+ end
200
+
201
+ # Internal counterpart of extend: a disposed child removes itself and
202
+ # its dead cascade inverse from this context. Extension contexts open
203
+ # and close constantly (per-turn loop contexts, MCP reconnects, tool
204
+ # epochs, fiber reactivations) — without the prune every cycle would
205
+ # leave a dead child and a no-op inverse in the accumulator forever
206
+ # (AGENTS.md zero-restart rule). Protected: only Context itself may
207
+ # strip a child's bookkeeping, so external code cannot unhook a live
208
+ # child (leak footgun). Must be called AFTER the child released its
209
+ # own monitor, so child disposal never nests child→parent locks
210
+ # against the parent→child order used by dispose. No-op on a disposed
211
+ # parent (its own drain owns the bookkeeping by then).
212
+ def prune_child(child)
213
+ @monitor.synchronize do
214
+ return if @disposed
215
+
216
+ @children.delete(child)
217
+ inverse = @child_inverses.delete(child)
218
+ @disposables.delete(inverse) if inverse
219
+ end
220
+ end
221
+ protected :prune_child
222
+
223
+ def dispose
224
+ parent = nil
225
+ deferred_interrupt = nil
226
+ @monitor.synchronize do
227
+ return if @disposed
228
+
229
+ @disposed = true
230
+ parent = @parent
231
+ # Reset BEFORE the child cascade so signals adopted from children
232
+ # (record_deferred_signal) survive into the final
233
+ # last_dispose_errors instead of being wiped by this reset.
234
+ @last_dispose_errors = []
235
+ dmesg = resolve_dmesg
236
+ # Counted before the cascade: disposing children prune their
237
+ # inverses from our accumulator (pre-cascade size is what the
238
+ # "beginning" log reports).
239
+ dmesg&.log(:context, :info, "Beginning context dispose with #{@disposables.size} inverse(s)")
240
+
241
+ deferred_interrupt = dispose_children
242
+ drained, drain_signal = drain_inverses(dmesg: dmesg)
243
+ deferred_interrupt ||= drain_signal
244
+
245
+ dmesg&.log(:context, :info, "Context dispose complete; #{drained} inverse(s) executed")
246
+ end
247
+ # Release the child's monitor before reaching the parent (lock
248
+ # order), then prune the parent's record of this child.
249
+ parent&.prune_child(self)
250
+ # Re-surface the FIRST deferred signal only after the unwind is
251
+ # fully complete — Ctrl+C during teardown still reaches the
252
+ # caller (repo convention: Interrupt is never swallowed), never
253
+ # at the cost of a half-drained accumulator.
254
+ raise deferred_interrupt if deferred_interrupt
255
+ end
256
+ alias dispose! dispose
257
+
258
+ private
259
+
260
+ # Phase 1: dispose child contexts first (parent→child lock order;
261
+ # children never hold their lock while reaching back to the parent).
262
+ # Each child's prune is a no-op here (@disposed sealed the bookkeeping),
263
+ # so the descending iteration never observes mutation. A child's
264
+ # deferred Interrupt/SystemExit is adopted as (possibly) the FIRST
265
+ # signal of this cascade; returns the first deferred child signal.
266
+ def dispose_children
267
+ deferred = nil
268
+ @children.reverse_each do |child|
269
+ child.dispose unless child.disposed?
270
+ rescue Interrupt, SystemExit => e
271
+ record_deferred_signal(e)
272
+ deferred ||= e
273
+ end
274
+ @children.clear
275
+ deferred
276
+ end
277
+
278
+ # Phase 2: execute accumulated inverses in strict reverse order
279
+ # (LIFO — Omega accumulator). A failing inverse never aborts the unwind
280
+ # (AGENTS.md §5): record it, report, keep draining. Interrupt/SystemExit
281
+ # get the same complete-drain policy: the signal is recorded and
282
+ # deferred, the drain ALWAYS finishes, and the first signal is re-raised
283
+ # by dispose after bookkeeping and the parent prune. Late registration
284
+ # racing the drain is monitor-serialized (pop loop, or the sealed
285
+ # immediate-run path). Returns [drained, first_signal].
286
+ def drain_inverses(dmesg:)
287
+ drained = 0
288
+ deferred = nil
289
+ begin
290
+ while (inverse = @disposables.pop)
291
+ begin
292
+ run_inverse(inverse, dmesg: dmesg)
293
+ drained += 1
294
+ rescue Interrupt, SystemExit => e
295
+ record_deferred_signal(e)
296
+ deferred ||= e
297
+ end
298
+ end
299
+ ensure
300
+ @services.clear
301
+ @service_inverses.clear
302
+ @child_inverses.clear
303
+ end
304
+ [drained, deferred]
305
+ end
306
+
307
+ def resolve_dmesg
308
+ d = @services["dmesg"] || resolve("dmesg")
309
+ d if d.respond_to?(:log)
310
+ rescue StandardError
311
+ nil
312
+ end
313
+
314
+ def record_deferred_signal(exception)
315
+ @last_dispose_errors << exception
316
+ Rubik::Log.warn(:runtime, "dispose inverse raised #{exception.class} (deferred until drain completes): #{exception.message}")
317
+ end
318
+
319
+ # Runs one inverse, never letting a failure abort the unwind (AGENTS.md §5):
320
+ # record it, report to stderr, keep going. Interrupt/SystemExit are NOT
321
+ # rescued here: the drain loop in dispose defers them so the unwind
322
+ # completes first (complete-drain-then-re-raise policy), while the
323
+ # sealed immediate-run path in register_disposable lets them propagate
324
+ # as-is — there is no accumulator left to protect there.
325
+ def run_inverse(inverse, dmesg: nil)
326
+ inverse.call
327
+ rescue StandardError => e
328
+ @last_dispose_errors << e
329
+ Rubik::Log.warn(:runtime, "dispose inverse failed: #{e.class}: #{e.message}")
330
+ dmesg ||= resolve_dmesg
331
+ dmesg&.log(:context, :err, "Dispose inverse failed: #{e.class}: #{e.message}")
332
+ end
333
+ end
334
+ end
335
+ end