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,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "commands/base"
4
+ require_relative "commands/system"
5
+ require_relative "commands/buffer"
6
+ require_relative "commands/session"
7
+ require_relative "commands/workspace"
8
+ require_relative "commands/lsp"
9
+ require_relative "commands/agent"
10
+ require_relative "commands/plugins"
11
+ require_relative "commands/mcp"
12
+ require_relative "commands/skills"
13
+ require_relative "commands/tasks"
14
+ require_relative "commands/subagents"
15
+ require_relative "commands/storage"
16
+ require_relative "commands/pruner"
17
+ require_relative "commands/dmesg"
18
+
19
+ module Rubik
20
+ module REPL
21
+ module Commands
22
+ MODULES = [
23
+ System,
24
+ Buffer,
25
+ Session,
26
+ Workspace,
27
+ Lsp,
28
+ Agent,
29
+ Plugins,
30
+ Mcp,
31
+ Skills,
32
+ Tasks,
33
+ Subagents,
34
+ Storage,
35
+ Pruner,
36
+ Dmesg
37
+ ].freeze
38
+
39
+ def self.register_all(shell)
40
+ MODULES.each { |mod| mod.register(shell) }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "service_catalog"
4
+
5
+ module Rubik
6
+ module REPL
7
+ # Plan 63 Faz B: pure construction layer extracted from Shell#initialize.
8
+ # Candidate 05: WHAT gets built, in which order, and with which wiring
9
+ # facts lives in ServiceCatalog (the single declaration point); this
10
+ # module only resolves the effective root, seeds the override scope,
11
+ # folds the catalog and packs the Parts struct. Every part stays
12
+ # injectable for tests through the Shell's optional parts: kwarg (and
13
+ # through this module's config/event_bus/buffer_manager/snapshot_store
14
+ # override kwargs, which adopt their catalog entry verbatim).
15
+ module Composition
16
+ Parts = ServiceCatalog.parts_struct
17
+
18
+ def self.build(config:, root_dir: nil, event_bus: nil, buffer_manager: nil, snapshot_store: nil, dmesg: nil, dispatcher: nil, init: nil)
19
+ effective_root = File.expand_path(root_dir || buffer_manager&.root_dir || config&.root_dir || Dir.pwd)
20
+ scope = ServiceCatalog::Scope.new(
21
+ root_dir: effective_root,
22
+ overrides: {
23
+ config: config, event_bus: event_bus,
24
+ buffer_manager: buffer_manager, snapshot_store: snapshot_store,
25
+ dmesg: dmesg, dispatcher: dispatcher, init: init
26
+ }.compact
27
+ )
28
+ Parts.new(**ServiceCatalog.build(scope))
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module REPL
5
+ # Wires reactive configuration observers to Shell subsystems.
6
+ # Returns an array of cleanup procs (inverses) that can be unrolled on shutdown.
7
+ module ConfigWiring
8
+ class << self
9
+ def wire(shell)
10
+ config = shell.config
11
+ return [] unless config.respond_to?(:on_change)
12
+
13
+ cleanups = []
14
+
15
+ cleanups << config.on_change("workspace") do |key, val, _old|
16
+ wire_workspace_change(shell, key, val)
17
+ end
18
+
19
+ cleanups << config.on_change("context") do |key, val, _old|
20
+ wire_context_change(shell, key, val)
21
+ end
22
+
23
+ cleanups << config.on_change("lsp") do |key, val, _old|
24
+ wire_lsp_change(shell, key, val)
25
+ end
26
+
27
+ ["llm", "models", "model", "providers", "router"].each do |prefix|
28
+ cleanups << config.on_change(prefix) do |_key, _val, _old|
29
+ wire_llm_change(shell)
30
+ end
31
+ end
32
+
33
+ cleanups << config.on_change("ui") do |_key, _val, _old|
34
+ shell.refresh_ui! if shell.respond_to?(:refresh_ui!)
35
+ end
36
+
37
+ cleanups << config.on_change("skills") do |_key, _val, _old|
38
+ shell.skill_manager&.apply_config!(tool_registry: shell.tool_registry)
39
+ end
40
+
41
+ cleanups << config.on_change("security.command_allowlist") do |_key, val, _old|
42
+ wire_security_change(shell, val)
43
+ end
44
+
45
+ cleanups << config.on_change("security.allow_safe_probes") do |_key, val, _old|
46
+ next unless shell.guard.respond_to?(:allow_safe_probes=)
47
+
48
+ shell.guard.allow_safe_probes = ([true, "true"].include?(val))
49
+ end
50
+
51
+ # Audit destination is live-reconfigurable too (nil/blank disables,
52
+ # Guard#audit_log_path= normalizes) — otherwise a mid-session
53
+ # !config set security.audit_log_path would confirm silently while
54
+ # events kept flowing to the old path.
55
+ cleanups << config.on_change("security.audit_log_path") do |_key, val, _old|
56
+ shell.guard.audit_log_path = val if shell.guard.respond_to?(:audit_log_path=)
57
+ end
58
+
59
+ cleanups << config.on_change("network.allow") do |_key, val, _old|
60
+ config.set("sandbox.allow_network", val, persist: false) if config.fetch("sandbox.allow_network", nil) != val
61
+ end
62
+
63
+ cleanups << config.on_change("sandbox.allow_network") do |_key, val, _old|
64
+ config.set("network.allow", val, persist: false) if config.fetch("network.allow", nil) != val
65
+ end
66
+
67
+ cleanups
68
+ end
69
+
70
+ private
71
+
72
+ def wire_workspace_change(shell, key, typed_val)
73
+ case key
74
+ when "workspace.additional_roots"
75
+ return unless typed_val.is_a?(Array)
76
+
77
+ exp_roots = typed_val.map { |r| File.expand_path(r.to_s, shell.buffer_manager&.root_dir || Dir.pwd) }
78
+ current_roots = shell.buffer_manager&.additional_roots || []
79
+ new_roots = exp_roots - current_roots
80
+ removed_roots = current_roots - exp_roots
81
+
82
+ new_roots.each { |r| shell.buffer_manager&.add_root(r) }
83
+ removed_roots.each { |r| shell.buffer_manager&.remove_root(r) }
84
+
85
+ if (new_roots.any? || removed_roots.any?) &&
86
+ shell.config.fetch("workspace.auto_index_additional", true) && shell.context_index
87
+ shell.context_index.additional_roots = shell.buffer_manager&.additional_roots
88
+ shell.context_index.full_scan
89
+ shell.warm_ast_graph(async: false) if shell.respond_to?(:warm_ast_graph)
90
+ end
91
+ when "workspace.allowed_external_dirs"
92
+ return unless typed_val.is_a?(Array)
93
+
94
+ exp_dirs = typed_val.map { |d| File.expand_path(d.to_s, shell.buffer_manager&.root_dir || Dir.pwd) }
95
+ current_dirs = shell.buffer_manager&.allowed_external_dirs || []
96
+ new_dirs = exp_dirs - current_dirs
97
+ removed_dirs = current_dirs - exp_dirs
98
+
99
+ new_dirs.each do |d|
100
+ shell.buffer_manager.add_allowed_dir(d) if shell.buffer_manager.respond_to?(:add_allowed_dir)
101
+ shell.sandbox_runner&.add_allowed_dir(d)
102
+ end
103
+ removed_dirs.each do |d|
104
+ shell.buffer_manager.remove_allowed_dir(d) if shell.buffer_manager.respond_to?(:remove_allowed_dir)
105
+ shell.sandbox_runner&.remove_allowed_dir(d)
106
+ end
107
+ when "workspace.allow_external_writes"
108
+ shell.buffer_manager.allow_external_writes = typed_val if shell.buffer_manager.respond_to?(:allow_external_writes=)
109
+ end
110
+ end
111
+
112
+ def wire_context_change(shell, key, _typed_val)
113
+ return unless ["context.include", "context.exclude"].include?(key)
114
+ return unless shell.context_index.respond_to?(:update_file_filters)
115
+
116
+ inc = shell.config.fetch("context.include", [])
117
+ exc = shell.config.fetch("context.exclude", [])
118
+ shell.context_index.update_file_filters(includes: inc, excludes: exc)
119
+ shell.warm_ast_graph(async: false) if shell.respond_to?(:warm_ast_graph)
120
+ end
121
+
122
+ def wire_lsp_change(shell, key, typed_val)
123
+ shell.lsp_manager&.stop_all if key == "lsp.enabled" && typed_val == false
124
+ end
125
+
126
+ # Reactive command-allowlist wiring (Plan: komut-allowlist): config is
127
+ # the single source of truth; the live Guard is reconfigured in place —
128
+ # zero restart (Plan 75 pattern).
129
+ def wire_security_change(shell, typed_val)
130
+ return unless typed_val.is_a?(Array)
131
+ return unless shell.guard.respond_to?(:set_allowed_commands)
132
+
133
+ shell.guard.set_allowed_commands(typed_val)
134
+ end
135
+
136
+ def wire_llm_change(shell)
137
+ shell.refresh_llm_registry! if shell.respond_to?(:refresh_llm_registry!)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module REPL
5
+ # Interactive confirmation helpers mixed into Shell so every y/n prompt
6
+ # (max-steps continue, !plan approval) routes through Reline with one
7
+ # consistent notion of "interactive". Mixing raw #gets with Reline wedges
8
+ # the prompt on Windows, so all confirmations live here.
9
+ module Confirmations
10
+ # Asks a yes/no question. Non-interactive contexts (tests, piped IO,
11
+ # test-capture tees) short-circuit to the default answer WITHOUT
12
+ # reading input. Returns the downcased answer String ("y"/"n"/...) so
13
+ # call sites keep their %w[y yes evet] handling. Ctrl+C, EOF, or a bare
14
+ # Enter all select the default answer instead of unwinding the loop.
15
+ def ask_yes_no(prompt, default: "n")
16
+ default_answer = default.to_s.downcase
17
+ return default_answer unless interactive_input?
18
+
19
+ # Fast single-key approval on color-capable TTYs (UI::Approvals);
20
+ # plain terminals keep the classic Reline line prompt.
21
+ return Rubik::UI::Approvals.keypress_yes_no(self, prompt, default: default_answer, theme: @ui_theme) if @ui_theme&.color?
22
+
23
+ hint = default_answer.start_with?("y") ? "[Y/n]" : "[y/N]"
24
+ answer = @reline.readline("#{prompt} #{hint}: ", false)
25
+ answer = default_answer if answer.nil? # EOF (Ctrl+D/Ctrl+Z) -> default
26
+ answer = answer.to_s.strip.downcase
27
+ answer.empty? ? default_answer : answer
28
+ rescue Interrupt
29
+ Rubik::REPL::InterruptHandler.restore_console_after_interrupt
30
+ @output.puts "^C"
31
+ default_answer
32
+ end
33
+
34
+ # True only when the Shell is attached to a live interactive console.
35
+ # A TeeIO wrapping REAL output (production command capture: its StringIO
36
+ # target makes test_capture? true) still counts as interactive; only
37
+ # tees whose every target is a StringIO (test harness) do not.
38
+ def interactive_input?
39
+ return false if ENV["RUBIK_TEST"] == "1"
40
+
41
+ out = @output
42
+ if out.is_a?(TeeIO)
43
+ real_targets = out.targets.compact.grep_v(StringIO)
44
+ return false if real_targets.empty?
45
+
46
+ out = real_targets.first
47
+ end
48
+ return false if out.is_a?(StringIO)
49
+ return false if out.respond_to?(:test_capture?) && out.test_capture?
50
+
51
+ input = @input || $stdin
52
+ input.respond_to?(:tty?) && input.tty?
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Rubik
6
+ module REPL
7
+ class Hooks
8
+ KNOWN_EVENTS = [:file_changed, :test_failed, :before_command, :after_command].freeze
9
+
10
+ def initialize(logger: Logger.new($stderr))
11
+ @hooks = Hash.new { |h, k| h[k] = [] }
12
+ @logger = logger
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def register(event_name, &block)
17
+ return unless block_given?
18
+
19
+ event_key = event_name.to_sym
20
+ @mutex.synchronize do
21
+ @hooks[event_key] << block
22
+ end
23
+ block
24
+ end
25
+
26
+ def unregister(event_name, token)
27
+ return unless token
28
+
29
+ event_key = event_name.to_sym
30
+ @mutex.synchronize do
31
+ @hooks[event_key].delete(token)
32
+ end
33
+ nil
34
+ end
35
+
36
+ def trigger(event_name, *)
37
+ event_key = event_name.to_sym
38
+ listeners = @mutex.synchronize { @hooks[event_key].dup }
39
+ listeners.each do |hook|
40
+ hook.call(*)
41
+ rescue StandardError => e
42
+ @logger.error("Hook error on event '#{event_name}': #{e.message}")
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require_relative "tee_io"
5
+
6
+ module Rubik
7
+ module REPL
8
+ # Öneri 3 (Kademe 2): Shell'den ayrılan girdi yürütme ve çıktı yakalama
9
+ # sorumluluğu. with_output/capture_output I/O geçişini; execute_command
10
+ # bang-komut dispatch'ini (kayıt, truncation, konuşma geçmişi işleme);
11
+ # execute_free_text serbest metin turn'inin tam yaşam döngüsünü kodlar
12
+ # (route → loop kurulum → devam sorusu → araç-geçmişi birleştirme →
13
+ # kayıt → akış/render → hata/iptal temizliği).
14
+ # Durum Shell'de yaşar (@output, @recorder, @conversation_history,
15
+ # @active_loop, ...); bu modül yalnızca yürütme politikasını kodlar.
16
+ module InputExecution
17
+ def with_output(new_io)
18
+ orig = @output
19
+ @output = new_io
20
+ @terminal_guard.output = new_io if @terminal_guard
21
+ yield
22
+ ensure
23
+ @output = orig
24
+ @terminal_guard.output = orig if @terminal_guard
25
+ end
26
+
27
+ def capture_output(&)
28
+ StringIO.new.tap { |io| with_output(io, &) }.string
29
+ end
30
+
31
+ # Per-dispatch closed view handed to bang-command handlers (see
32
+ # CommandContext). Cheap by design — holds only the shell reference;
33
+ # the capture-aware IO is read live off the shell, so it reflects
34
+ # with_output's swap at call time.
35
+ def command_context
36
+ Rubik::REPL::CommandContext.new(self)
37
+ end
38
+
39
+ def execute_command(parsed)
40
+ # Plan 109 staleness rule: this turn's context already includes the
41
+ # idle-injected notices in @conversation_history — a surviving
42
+ # pending wake would turn the next stray Enter into a redundant
43
+ # notice turn.
44
+ clear_pending_wake_notices! if respond_to?(:clear_pending_wake_notices!)
45
+ cmd_name = @plugin_manager ? @plugin_manager.resolve_alias(parsed.command_name) : parsed.command_name.to_s.downcase.delete_prefix("!")
46
+ cmd_meta = @commands[cmd_name]
47
+
48
+ if cmd_meta
49
+ @hooks.trigger(:before_command, parsed.command_name, parsed.args)
50
+
51
+ # Lifecycle cmds + goal (which self-manages its own history turns)
52
+ untracked_commands = ["quit", "exit", "clear", "reset", "goal"]
53
+ if untracked_commands.include?(parsed.command_name.downcase)
54
+ begin
55
+ cmd_meta[:handler].call(parsed.args, command_context)
56
+ rescue Interrupt
57
+ Rubik::REPL::InterruptHandler.restore_console_after_interrupt
58
+ @output.puts "\n⚠️ [Rubik] Komut kullanıcı tarafından iptal edildi (^C)."
59
+ end
60
+ else
61
+ captured = StringIO.new
62
+ cmd_interrupted = false
63
+ with_output(TeeIO.new(@output, captured)) do
64
+ # Built INSIDE the capture swap so ctx.out is the TeeIO — the
65
+ # handler's prints reach the recorder and LLM-history truncation.
66
+ cmd_meta[:handler].call(parsed.args, command_context)
67
+ rescue Interrupt
68
+ cmd_interrupted = true
69
+ Rubik::REPL::InterruptHandler.restore_console_after_interrupt
70
+ @output.puts "\n⚠️ [Rubik] Komut kullanıcı tarafından iptal edildi (^C)."
71
+ ensure
72
+ # A command (!plan/!goal) may leave a live spinner if it died mid-wait.
73
+ @ui_renderer&.spinner&.kill
74
+ end
75
+ output_text = captured.string.strip
76
+ if !cmd_interrupted && !output_text.empty?
77
+ clean_raw = Rubik::Security::Redactor.redact(parsed.raw)
78
+ @recorder&.record_repl_command(clean_raw, output_text)
79
+ # Plan 61 D-8: bound what a bang command leaves in LLM memory; the recorder keeps the full text.
80
+ trunc = Rubik::Tools::Truncator.truncate(output_text, max_chars: @config.fetch("tools.max_output_chars", 12_000), strategy: :head_tail)
81
+ @conversation_history << { role: "user", content: clean_raw.delete_prefix("!") } # bangs stay out of LLM history; recorder keeps the literal
82
+ @conversation_history << { role: "assistant", content: trunc[:truncated] ? trunc[:text] : output_text }
83
+ trim_conversation_window!
84
+ end
85
+ end
86
+
87
+ @hooks.trigger(:after_command, parsed.command_name, parsed.args)
88
+ reap_dead_kernel_units! if respond_to?(:reap_dead_kernel_units!)
89
+ else
90
+ available = @commands.keys.sort.map { |c| "!#{c}" }.join(", ")
91
+ @output.puts "Bilinmeyen komut: !#{parsed.command_name}. Kullanılabilir komutlar: #{available} (!help'e bakın)."
92
+ end
93
+ end
94
+
95
+ # Plan 109 Faz B4: marker prefix makes the system-initiated origin
96
+ # visible in the recorder/session (user decision: honest provenance).
97
+ NOTICE_TURN_MARKER = "[reactive subagent wake-up]"
98
+
99
+ # Plan 109 Faz B4: the autonomous notice-evaluation turn. The queued
100
+ # notices ALREADY ride @conversation_history as
101
+ # <subagent_notifications> XML (idle-branch injection); the drained
102
+ # payload only builds a roster so the model can target ids
103
+ # immediately — the prompt never duplicates the XML. Reentrancy is
104
+ # excluded by construction: inside execute_free_text, @active_loop
105
+ # is set before orchestrator.run, so any event arriving mid-turn
106
+ # takes the ACTIVE path (loop inbox), never the idle path — no
107
+ # recursive wake. Wakeloop fix 2026-10-09: the residual idle-path
108
+ # windows (an event landing before @active_loop is set, or in the
109
+ # ensure tail after it is cleared) are closed by @autonomous_turn —
110
+ # route_subagent_notice's idle branch still injects and prints but
111
+ # does not arm the wake while this flag is up, so an autonomous turn
112
+ # can never chain into another one.
113
+ def execute_subagent_notice_turn!
114
+ notices = drain_pending_wake_notices!
115
+ return false if notices.empty?
116
+
117
+ roster = notices.map { |n| "#{n[:id]}:#{n[:kind]}" }.uniq.join(", ")
118
+ # Wakeloop fix 2026-10-09: an AUTONOMOUS turn must not spawn another
119
+ # one. Stranded notices this turn leaves behind still reach history
120
+ # (no loss), but they do not re-arm the wake — the prompt comes back
121
+ # and the backlog waits for the user's next Enter.
122
+ begin
123
+ @autonomous_turn = true
124
+ execute_free_text(
125
+ "#{NOTICE_TURN_MARKER} Background subagent activity arrived while the prompt was idle " \
126
+ "(#{roster}). Evaluate the <subagent_notifications> blocks in your conversation history: " \
127
+ "extend the budget of paused-but-promising subagents (manage_subagents action: 'extend_budget'), " \
128
+ "steer with send_message, retrieve finished results (manage_subagents action: 'wait'), " \
129
+ "wrap up or kill stale work, or report outcomes to the user. The user did not type this " \
130
+ "turn — it fired automatically. If nothing needs your attention, finish with a one-line status summary."
131
+ )
132
+ ensure
133
+ @autonomous_turn = false
134
+ end
135
+ true
136
+ end
137
+
138
+ def execute_free_text(text)
139
+ # Wakeloop fix 2026-10-09: ^C settles the wake debt instead of
140
+ # letting the interrupted turn's own ensure re-arm the cycle. Bound
141
+ # FIRST: every line between here and the rescue clauses can raise
142
+ # (recorder file IO, bus emit), and the ensure reads this local —
143
+ # an unbound read would mask the in-flight exception with a
144
+ # NameError, breaking the "cleanup must never mask" contract.
145
+ interrupted = false
146
+ # Plan 109 staleness rule (see execute_command): a real turn's
147
+ # context already includes the idle-injected notices. The notice
148
+ # turn itself drains first, so this clear is a no-op for it.
149
+ clear_pending_wake_notices! if respond_to?(:clear_pending_wake_notices!)
150
+ @recorder&.record_user_turn(text)
151
+ @event_bus.emit(:prompt_received, prompt: text)
152
+
153
+ renderer = md_stream = orchestrator = nil
154
+ routed = resolve_turn_route(text)
155
+ provider = routed[:provider]
156
+
157
+ if provider.nil?
158
+ spec_str = routed[:spec].is_a?(Hash) ? "#{routed[:spec]['provider']}/#{routed[:spec]['name']}" : routed[:spec].to_s
159
+ @output.puts "Hata: Model sağlayıcısı başlatılamadı (#{spec_str})."
160
+ record_failed_turn!(text, "failed", "no LLM provider available (#{spec_str})", nil)
161
+ return
162
+ end
163
+
164
+ @runtime_manager.provide(:llm_client, provider)
165
+
166
+ # Pruner apply seam (context-pruner 2026-10-09): budayıcı bir plan
167
+ # aşamaladıysa pencereyi turn tohumlanmadan ÖNCE takas et (knob
168
+ # kapalıysa no-op; !plan'ın execute_free_text devri ve otonom notice
169
+ # turn'leri buradan geçer).
170
+ apply_staged_prune_plan!
171
+
172
+ max_steps = @config.fetch("agent.max_steps", 40).to_i
173
+ warn_ratio = @config.fetch("agent.warn_at_ratio", 0.75).to_f
174
+ theme = self.theme
175
+ renderer = @ui_renderer || Rubik::UI::Renderer.new(config: @config, max_steps: max_steps, output: @output, theme: theme)
176
+ md_enabled = @config.fetch("ui.markdown", "live") != "off"
177
+ md_stream = Rubik::UI::MarkdownStream.new(theme, enabled: md_enabled)
178
+
179
+ orchestrator = orchestrator_loop(
180
+ provider: provider,
181
+ model: routed[:model],
182
+ max_steps: max_steps,
183
+ warn_at_ratio: warn_ratio,
184
+ context_limit: routed[:context_limit],
185
+ max_output_tokens: routed[:max_output_tokens],
186
+ on_step: lambda { |step_info|
187
+ case step_info[:type]&.to_sym
188
+ when :chunk
189
+ renderer.render(step_info) # stops spinner before raw output
190
+ md_stream.push(step_info[:delta]) { |seg| @output.print seg }
191
+ @output.flush if @output.respond_to?(:flush)
192
+ when :chunk_end
193
+ renderer.render(step_info)
194
+ md_stream.finish { |seg| @output.print seg }
195
+ @output.flush if @output.respond_to?(:flush)
196
+ when :tool_call
197
+ @recorder&.record_tool_call(step_info[:tool], step_info[:args], call_id: step_info[:call_id])
198
+ formatted = renderer.render(step_info)
199
+ @output.puts formatted if formatted && !formatted.empty?
200
+ @output.flush if @output.respond_to?(:flush)
201
+ when :tool_result
202
+ @recorder&.record_tool_result(step_info[:tool], step_info[:output], status: step_info[:status] || :ok, call_id: step_info[:call_id])
203
+ formatted = renderer.render(step_info)
204
+ @output.puts formatted if formatted && !formatted.empty?
205
+ @output.flush if @output.respond_to?(:flush)
206
+ else
207
+ formatted = renderer.render(step_info)
208
+ if formatted && !formatted.empty?
209
+ @output.puts formatted
210
+ @output.flush if @output.respond_to?(:flush)
211
+ end
212
+ end
213
+ }
214
+ )
215
+ @active_loop = orchestrator
216
+ res = orchestrator.run(text, conversation_history: @conversation_history)
217
+
218
+ while !res[:ok] && res[:error]&.include?("Max steps budget exceeded") && @config.fetch("agent.ask_continue_on_limit", true)
219
+ @output.puts "\n⚠️ [Rubik::Agent] #{res[:error]}"
220
+ ans = ask_yes_no(" [Rubik::Agent] +20 adım ile devam edilsin mi?", default: "n")
221
+ break unless ["y", "yes", "evet"].include?(ans)
222
+
223
+ @output.puts " [Rubik::Agent] Adım bütçesi +20 artırılıyor ve görev sürdürülüyor..."
224
+ res = orchestrator.continue_loop(20)
225
+
226
+ end
227
+
228
+ # Plan 85 D-7 (kök neden): turn içi @history'deki tool ara adımlarını
229
+ # conversation_history'ye kopyala — böylece continue'da model turn sırasındaki
230
+ # bağlamı görür ve "sıfırdan keşfe" başlamaz. res[:loop_instance].history,
231
+ # turn_result'ın döndürdüğü Loop örneğinin @history'sidir.
232
+ merge_turn_tool_history!(
233
+ res[:loop_instance]&.history,
234
+ max_entries: @config.fetch("context.merge_turn_tool_max_entries", 20).to_i
235
+ )
236
+
237
+ if res[:ok]
238
+ record_turn_outcome!(text, res[:output].to_s, tokens: res[:tokens], streamed: res[:streamed] == true)
239
+
240
+ # Only print the final answer when it was not already streamed
241
+ # live; batch answers get full markdown rendering on capable TTYs.
242
+ # res[:streamed] reflects the FINISHING turn (chunks streamed
243
+ # before a finish tool call count as printed).
244
+ if res[:streamed] != true && !res[:output].to_s.empty?
245
+ rendered = Rubik::UI::MarkdownStream.render_document(res[:output], theme, enabled: md_enabled)
246
+ @output.puts rendered
247
+ end
248
+
249
+ @output.puts "\n[Rubik] In-memory buffer updated. Type `!diff` to view, `!apply` to save." if @buffer_manager.dirty?
250
+
251
+ # Kök-turn kroniği (root-turn planı 2026-10-04): başarılı turn da
252
+ # vakanüvis'e düşer — subagent'siz geçen oturumlar kroniğe girsin.
253
+ # Bu dalın SON deyimi olmalı: sonrasındaki bir istisna (^C, render
254
+ # hatası) rescue merdivenine düşüp record_failed_turn! ile İKİNCİ
255
+ # bir :turn_finished üretmesin — tek turn, tek kronik satırı.
256
+ emit_turn_finished(kind: "success", prompt: text, outcome: res[:output].to_s,
257
+ orchestrator: orchestrator, tokens: res[:tokens])
258
+ else
259
+ @output.puts "Görev tamamlanamadı: #{res[:error]}"
260
+ record_failed_turn!(text, "incomplete", res[:error], orchestrator)
261
+ end
262
+ rescue Interrupt
263
+ interrupted = true
264
+ # The user cancelled: queued wake debts settle here. Each queued
265
+ # notice already lives in @conversation_history (idle path injects
266
+ # at route time), so draining loses nothing the model needed.
267
+ clear_pending_wake_notices! if respond_to?(:clear_pending_wake_notices!)
268
+ Rubik::REPL::InterruptHandler.restore_console_after_interrupt
269
+ # Dedicated event type: restore_session must NOT hydrate this as a
270
+ # dialogue turn (the user_turn recorded at method entry already
271
+ # carries the question; a repl_command-style pair would duplicate it).
272
+ @recorder&.record("cancelled_turn",
273
+ "question" => text.to_s.strip[0, 80],
274
+ "reason" => "Interrupted by user (^C)")
275
+ @output.puts "\n⚠️ [Rubik::Agent] Görev kullanıcı tarafından iptal edildi (^C)."
276
+ record_failed_turn!(text, "cancelled", "Interrupted by user (^C)", orchestrator)
277
+ rescue Rubik::LLM::ClientError => e
278
+ # e.message may carry binary bytes (raw HTTP error bodies). Printing
279
+ # or recording those would raise INSIDE this rescue clause, bypassing
280
+ # every sibling handler and surfacing as a bare
281
+ # Encoding::CompatibilityError at the REPL prompt instead of the real
282
+ # LLM error. Sanitize before use.
283
+ message = Rubik::Platform.safe_utf8(e.message)
284
+ @output.puts "[Rubik::LLM Hatası] #{message}"
285
+ record_failed_turn!(text, "failed", message, orchestrator)
286
+ rescue StandardError => e
287
+ message = Rubik::Platform.safe_utf8(e.message)
288
+ @output.puts "[Hata] #{e.class}: #{message}"
289
+ record_failed_turn!(text, "failed", "#{e.class}: #{message}", orchestrator)
290
+ ensure
291
+ @active_loop = nil
292
+ # Flush a half-streamed markdown tail so an interrupt never eats the last
293
+ # line. Cleanup must never mask the in-flight exception (Interrupt, LLM
294
+ # error): each step reports its own failure to stderr instead of raising.
295
+ begin
296
+ md_stream&.finish { |seg| @output.print seg }
297
+ rescue StandardError => e
298
+ warn "[rubik] markdown tail flush failed: #{e.class}: #{e.message}"
299
+ end
300
+ begin
301
+ renderer&.spinner&.kill
302
+ rescue StandardError => e
303
+ warn "[rubik] spinner cleanup failed: #{e.class}: #{e.message}"
304
+ end
305
+ flush_subagent_notices! if respond_to?(:flush_subagent_notices!)
306
+ # Session review B3: notices stranded in the ended loop's inbox never
307
+ # reached the model channel and owed no wake — an idle maestro learned
308
+ # about a budget pause only at the next manual prompt (11 min in the
309
+ # reported session). Deliver them into history and arm the wake.
310
+ # Wakeloop fix 2026-10-09: NOT after an interrupt (the user cancelled;
311
+ # the cycle must not resume) and NOT from an autonomous notice turn
312
+ # (a synthetic turn must never spawn another — the prompt comes back).
313
+ if respond_to?(:deliver_stranded_loop_notices!)
314
+ deliver_stranded_loop_notices!(
315
+ orchestrator,
316
+ arm_wake: !interrupted && !@autonomous_turn
317
+ )
318
+ end
319
+ reap_dead_kernel_units! if respond_to?(:reap_dead_kernel_units!)
320
+ end
321
+ end
322
+ end
323
+ end