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,324 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../platform"
5
+
6
+ module Rubik
7
+ module REPL
8
+ class StepFormatter
9
+ LEVEL_QUIET = 0
10
+ LEVEL_NORMAL = 1
11
+ LEVEL_VERBOSE = 2
12
+ LEVEL_DEBUG = 3
13
+
14
+ # Canonical argument keys considered the "primary" operand of a tool
15
+ # call, shared with UI::Renderer (Plan 51, Decision D2).
16
+ PRIMARY_ARG_KEYS = ["path", "file", "target_file", "filename", "command", "cmd", "query", "pattern", "message", "code", "url", "name", "role",
17
+ "input"].freeze
18
+
19
+ attr_reader :level, :max_steps
20
+
21
+ def self.normalize_level(val)
22
+ case val.to_s.downcase.strip
23
+ when "quiet", "0" then LEVEL_QUIET
24
+ when "verbose", "2" then LEVEL_VERBOSE
25
+ when "debug", "trace", "3" then LEVEL_DEBUG
26
+ else LEVEL_NORMAL
27
+ end
28
+ end
29
+
30
+ def initialize(config: nil, verbosity: nil, max_steps: nil)
31
+ @config = config
32
+ @max_steps = max_steps
33
+ raw_verb = verbosity || config&.verbosity || "normal"
34
+ @level = self.class.normalize_level(raw_verb)
35
+ end
36
+
37
+ def verbosity
38
+ case @level
39
+ when LEVEL_QUIET then "quiet"
40
+ when LEVEL_VERBOSE then "verbose"
41
+ when LEVEL_DEBUG then "debug"
42
+ else "normal"
43
+ end
44
+ end
45
+
46
+ def format(step_info)
47
+ return nil unless step_info.is_a?(Hash)
48
+
49
+ type = step_info[:type]&.to_sym
50
+ case type
51
+ when :thinking
52
+ format_thinking(step_info)
53
+ when :tool_call
54
+ format_tool_call(step_info)
55
+ when :tool_result
56
+ format_tool_result(step_info)
57
+ when :llm_thought
58
+ format_llm_thought(step_info)
59
+ when :step_warning
60
+ format_step_warning(step_info)
61
+ when :llm_retry
62
+ format_llm_retry(step_info)
63
+ when :auto_apply
64
+ format_auto_apply(step_info)
65
+ when :emergency_compact
66
+ format_emergency_compact(step_info)
67
+ when :goal_wait
68
+ format_goal_wait(step_info)
69
+ when :finish
70
+ format_finish(step_info)
71
+ when :message
72
+ format_message(step_info)
73
+ when :chunk, :chunk_end
74
+ nil
75
+ end
76
+ end
77
+
78
+ def format_thinking(step_info)
79
+ return nil if @level == LEVEL_QUIET
80
+
81
+ parts = thinking_parts(step_info)
82
+ target_str = parts[:target].empty? ? "" : " (#{parts[:target]})"
83
+
84
+ case @level
85
+ when LEVEL_NORMAL
86
+ " ⏳ [Step #{parts[:step]}/#{parts[:max]}] Thinking...#{target_str}"
87
+ when LEVEL_VERBOSE
88
+ " ⏳ [Step #{parts[:step]}/#{parts[:max]}] Querying LLM#{target_str}..."
89
+ when LEVEL_DEBUG
90
+ "[DEBUG:thinking] step=#{parts[:step]}/#{parts[:max]} provider=#{step_info[:provider]} model=#{step_info[:model]}"
91
+ end
92
+ end
93
+
94
+ # Semantic decomposition shared with UI::Renderer: step budget parts
95
+ # and the "provider/model" target label of a :thinking event.
96
+ def thinking_parts(step_info)
97
+ step = step_info[:step] || step_info[:current] || 1
98
+ max = step_info[:max_steps] || step_info[:max] || @max_steps || "?"
99
+ target = [step_info[:provider], step_info[:model]].compact.reject { |s| s.to_s.strip.empty? }.join("/")
100
+ { step: step, max: max, target: target }
101
+ end
102
+
103
+ def summarize_primary_arg(args)
104
+ return "" unless args.is_a?(Hash) && !args.empty?
105
+
106
+ matched_key = PRIMARY_ARG_KEYS.find { |k| args.key?(k) || args.key?(k.to_sym) }
107
+
108
+ if matched_key
109
+ "#{matched_key}: #{format_arg_value(primary_arg_value(args))}"
110
+ else
111
+ k, v = args.first
112
+ "#{k}: #{format_arg_value(v)}"
113
+ end
114
+ end
115
+
116
+ # Raw value of the primary operand (no key prefix, no quoting) — the
117
+ # semantic twin of #summarize_primary_arg consumed by UI::Renderer.
118
+ def primary_arg_value(args)
119
+ return "" unless args.is_a?(Hash) && !args.empty?
120
+
121
+ matched_key = PRIMARY_ARG_KEYS.find { |k| args.key?(k) || args.key?(k.to_sym) }
122
+ key = matched_key || args.first[0]
123
+ args.key?(key) ? args[key] : args[key.to_sym]
124
+ end
125
+
126
+ def format_arg_value(val)
127
+ val.is_a?(String) ? "\"#{truncate_str(val, 50)}\"" : val.to_s
128
+ end
129
+
130
+ def summarize_result(output, step_info = {})
131
+ return step_info[:summary] if step_info[:summary] && !step_info[:summary].to_s.empty?
132
+
133
+ lines = output.to_s.lines
134
+ if lines.empty?
135
+ "ok"
136
+ elsif lines.size > 1
137
+ "#{lines.size} lines"
138
+ else
139
+ line = lines.first.strip
140
+ line.length > 60 ? "#{line[0..57]}..." : line
141
+ end
142
+ end
143
+
144
+ # First printable line of an assistant thought with fenced JSON tool
145
+ # blocks stripped; nil when nothing human-readable remains. Shared
146
+ # with UI::Renderer (Plan 51, Decision D2).
147
+ def thought_first_line(content)
148
+ clean_content = content.to_s.sub(/```(?:json)?\s*\{.*?\}\s*```/m, "").strip
149
+ return nil if clean_content.empty?
150
+
151
+ clean_content.lines.first.to_s.strip
152
+ end
153
+
154
+ private
155
+
156
+ def truncate_str(str, max_len = 50)
157
+ return str if str.length <= max_len
158
+
159
+ "#{str[0...(max_len - 3)]}..."
160
+ end
161
+
162
+ def format_tool_call(step_info)
163
+ return nil if @level == LEVEL_QUIET
164
+
165
+ step = step_info[:step] || step_info[:current] || 1
166
+ max = step_info[:max_steps] || step_info[:max] || @max_steps || "?"
167
+ tool = step_info[:tool]
168
+ args = step_info[:args] || {}
169
+ call_id = step_info[:call_id]
170
+
171
+ case @level
172
+ when LEVEL_NORMAL
173
+ summary = summarize_primary_arg(args)
174
+ if summary.empty?
175
+ " ▶ [Step #{step}/#{max}] #{tool}"
176
+ else
177
+ " ▶ [Step #{step}/#{max}] #{tool} (#{summary})"
178
+ end
179
+ when LEVEL_VERBOSE
180
+ " ▶ [Step #{step}/#{max}] Calling #{tool} with args: #{Rubik::Platform.safe_json_generate(args)}"
181
+ when LEVEL_DEBUG
182
+ "[DEBUG:tool_call] step=#{step}/#{max} tool=#{tool} call_id=#{call_id} args=#{Rubik::Platform.safe_json_generate(args)}"
183
+ end
184
+ end
185
+
186
+ def format_tool_result(step_info)
187
+ return nil if @level == LEVEL_QUIET
188
+
189
+ tool = step_info[:tool]
190
+ status = (step_info[:status] || :ok).to_sym
191
+ duration_ms = step_info[:duration_ms] || 0.0
192
+ output = step_info[:output].to_s
193
+
194
+ case @level
195
+ when LEVEL_NORMAL
196
+ if status == :ok
197
+ summary_line = summarize_result(output, step_info)
198
+ " ✔ [Obs] #{summary_line} (#{duration_ms}ms)"
199
+ else
200
+ err = step_info[:error]
201
+ err = output.lines.first.to_s.strip if err.nil? || err.empty?
202
+ err = "Command failed" if err.empty?
203
+ if err.start_with?("Security blocked:") || output.start_with?("Security blocked:")
204
+ sec_line = output.lines.first.to_s.strip
205
+ hint = output[/!(?:allow-cmd|allow-dir)\s+[^\n]+/]&.strip
206
+ msg = hint ? "#{sec_line} [#{hint}]" : sec_line
207
+ " ⚠ [Security] #{msg}"
208
+ else
209
+ " ✖ [Error] #{err} (#{duration_ms}ms)"
210
+ end
211
+ end
212
+ when LEVEL_VERBOSE
213
+ lines_size = step_info[:line_count] || output.lines.size
214
+ byte_size = step_info[:byte_size] || output.bytesize
215
+ if status == :ok
216
+ header = " ✔ [Obs] #{lines_size} lines (#{byte_size} B, #{duration_ms}ms)"
217
+ else
218
+ err = step_info[:error]
219
+ err = output.lines.first.to_s.strip if err.nil? || err.empty?
220
+ err = "Command failed" if err.empty?
221
+ if err.start_with?("Security blocked:") || output.start_with?("Security blocked:")
222
+ sec_line = output.lines.first.to_s.strip
223
+ hint = output[/!(?:allow-cmd|allow-dir)\s+[^\n]+/]&.strip
224
+ msg = hint ? "#{sec_line} [#{hint}]" : sec_line
225
+ return " ⚠ [Security] #{msg}"
226
+ end
227
+ header = " ✖ [Error] #{err} (#{duration_ms}ms)"
228
+ end
229
+
230
+ if output.empty?
231
+ header
232
+ else
233
+ preview = output.lines.first(5).map { |l| " #{l.chomp}" }.join("\n")
234
+ "#{header}\n ↳ Preview:\n#{preview}"
235
+ end
236
+ when LEVEL_DEBUG
237
+ "[DEBUG:tool_result] tool=#{tool} status=#{status} duration=#{duration_ms}ms\n#{output}"
238
+ end
239
+ end
240
+
241
+ # First printable line of an assistant thought with fenced JSON tool
242
+ # blocks stripped; nil when nothing human-readable remains. Shared
243
+ # with UI::Renderer (Plan 51, Decision D2).
244
+ def format_llm_thought(step_info)
245
+ return nil if @level == LEVEL_QUIET || @level == LEVEL_NORMAL
246
+
247
+ first_line = thought_first_line(step_info[:content])
248
+ return nil if first_line.nil? || first_line.empty?
249
+
250
+ " 💭 [Thought] #{first_line}"
251
+ end
252
+
253
+ def format_step_warning(step_info)
254
+ current = step_info[:current] || step_info[:step]
255
+ max = step_info[:max] || step_info[:max_steps] || @max_steps
256
+ remaining = step_info[:remaining] || (max && current ? (max.to_i - current.to_i) : 0)
257
+ " ⚠️ [Rubik::Agent] Step budget warning: #{current}/#{max} steps used (#{remaining} remaining)."
258
+ end
259
+
260
+ def format_llm_retry(step_info)
261
+ return nil if @level == LEVEL_QUIET
262
+
263
+ attempt = step_info[:attempt] || "?"
264
+ max = step_info[:max] || "?"
265
+ reason = step_info[:reason].to_s
266
+ if @level == LEVEL_DEBUG
267
+ "[DEBUG:llm_retry] step=#{step_info[:step] || '?'}/#{step_info[:max_steps] || '?'} retry=#{attempt}/#{max} reason=#{reason}"
268
+ else
269
+ " ↻ [Rubik::LLM] retry #{attempt}/#{max} (#{reason})"
270
+ end
271
+ end
272
+
273
+ def format_auto_apply(step_info)
274
+ return nil if @level == LEVEL_QUIET
275
+
276
+ files = step_info[:files] || []
277
+ " ⚡ [Rubik::Buffer] Auto-applied #{files.size} modified file(s) to disk: #{files.join(', ')}"
278
+ end
279
+
280
+ def format_emergency_compact(_step_info)
281
+ " ⚠️ [Rubik::Context] Context overflow auto-recovery: applied emergency compaction to history."
282
+ end
283
+
284
+ # Goal-mode implicit wait (GoalWait): the run is holding itself open
285
+ # on live subagents instead of accepting a premature completion.
286
+ def format_goal_wait(step_info)
287
+ return nil if @level == LEVEL_QUIET
288
+
289
+ timeout = step_info[:timeout]
290
+ wait_note = timeout.to_i.positive? ? "waiting up to #{timeout}s" : "no blocking wait (agent.goal_wait_timeout=0)"
291
+ " ⏳ [Rubik::Goal] #{step_info[:active]} subagent(s) still active — goal held open (#{wait_note})."
292
+ end
293
+
294
+ # The full answer is rendered exactly once elsewhere (live chunks or
295
+ # the post-loop markdown render); this header stays to ONE line so the
296
+ # whole text is never printed twice. The suffix must tell the truth
297
+ # about WHERE the content went: streamed turns show "(+N more lines)"
298
+ # — the text is already above; batch turns say "(full answer below)"
299
+ # because the post-loop render follows immediately. A missing :streamed
300
+ # key defaults to streamed (the dominant interactive path).
301
+ def format_finish(step_info)
302
+ return nil if @level == LEVEL_QUIET
303
+
304
+ message = (step_info[:message] || "Completed").to_s.strip
305
+ return " 🏁 [Rubik::Agent] Completed" if message.empty?
306
+
307
+ streamed = step_info.key?(:streamed) ? step_info[:streamed] == true : true
308
+ return " 🏁 [Rubik::Agent] Completed (full answer below)" unless streamed
309
+
310
+ lines = message.lines
311
+ first = lines.first.to_s.strip
312
+ suffix = lines.count > 1 ? " (+#{lines.count - 1} more lines)" : ""
313
+ " 🏁 [Rubik::Agent] Completed: #{first}#{suffix}"
314
+ end
315
+
316
+ def format_message(step_info)
317
+ return nil if @level == LEVEL_QUIET || @level == LEVEL_NORMAL || @level == LEVEL_VERBOSE
318
+
319
+ content = step_info[:content]
320
+ "[DEBUG:message] #{content}"
321
+ end
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../ui/capability"
4
+
5
+ module Rubik
6
+ module REPL
7
+ class TeeIO
8
+ attr_reader :targets
9
+
10
+ def initialize(*targets)
11
+ @targets = targets.compact
12
+ end
13
+
14
+ def string_io?
15
+ @targets.any?(StringIO)
16
+ end
17
+
18
+ # Public predicate for test capture detection. Returns true when
19
+ # the TeeIO is being used to capture output into a StringIO
20
+ # buffer (typically in tests). The Shell and agent commands
21
+ # use this to short-circuit interactive yes/no prompts to
22
+ # "no" without inspecting the implementation details.
23
+ def test_capture?
24
+ string_io? || @targets.empty?
25
+ end
26
+
27
+ def puts(*args)
28
+ dispatch(:puts, args)
29
+ end
30
+
31
+ def print(*args)
32
+ dispatch(:print, args)
33
+ end
34
+
35
+ def printf(*args)
36
+ dispatch(:printf, args)
37
+ end
38
+
39
+ # Writes data to all underlying IO targets simultaneously.
40
+ # StringIO targets (session capture, conversation recording) receive
41
+ # ANSI-stripped content so recordings and LLM context stay clean.
42
+ #
43
+ # @param args [Array<String>] one or more strings to write
44
+ # @return [void]
45
+ def write(*args)
46
+ dispatch(:write, args)
47
+ end
48
+
49
+ def flush
50
+ ordered_targets.each do |target|
51
+ target.flush if target.respond_to?(:flush)
52
+ rescue StandardError => e
53
+ drop_failed_target(target, :flush, e)
54
+ end
55
+ nil
56
+ end
57
+
58
+ # Returns "" by design when every recorder has been detached by the
59
+ # failure policy — a degraded transcript, never a raise.
60
+ def string
61
+ target = @targets.find { |t| t.respond_to?(:string) }
62
+ target&.string || ""
63
+ end
64
+
65
+ def reopen(*)
66
+ @targets.each { |t| t.reopen(*) if t.respond_to?(:reopen) }
67
+ end
68
+
69
+ private
70
+
71
+ # User-facing streams first, recorders (StringIO captures) last: the
72
+ # recorder must survive a dead user stream and keep its copy of the
73
+ # transcript. Ordering only affects failure isolation — tee semantics
74
+ # deliver identical bytes to every target either way.
75
+ def ordered_targets
76
+ recorders, users = @targets.partition { |t| t.is_a?(StringIO) }
77
+ users + recorders
78
+ end
79
+
80
+ def dispatch(method, args)
81
+ ordered_targets.each do |target|
82
+ if target.is_a?(StringIO)
83
+ target.public_send(method, *strip_args(args))
84
+ else
85
+ target.public_send(method, *args)
86
+ end
87
+ rescue StandardError => e
88
+ # One broken target (closed pipe, EPIPE) must never kill the tee:
89
+ # warn once, detach it, keep the remaining targets — the recorder
90
+ # keeps its copy even when the user stream dies.
91
+ drop_failed_target(target, method, e)
92
+ end
93
+ end
94
+
95
+ def drop_failed_target(target, method, exception)
96
+ @targets.delete(target)
97
+ warn "[rubik] tee target failed during ##{method} (#{exception.class}: #{exception.message}); detached from output"
98
+ end
99
+
100
+ def strip_args(args)
101
+ args.map { |a| a.is_a?(String) ? Rubik::UI::Capability.strip_ansi(a) : a }
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module Rubik
6
+ module REPL
7
+ # Guard protecting Reline input buffer and screen from asynchronous background notices (Plan 105).
8
+ # When Reline is actively reading, notices clear the current line (\r\e[2K), print cleanly,
9
+ # and call Reline.redisplay to restore the prompt and in-progress user input buffer.
10
+ # When idle, notices are output directly. Supports quiet inbox buffering.
11
+ class TerminalGuard
12
+ include MonitorMixin
13
+
14
+ attr_accessor :quiet, :output, :reline
15
+
16
+ def initialize(output: $stdout, reline: Reline, quiet: false)
17
+ super()
18
+ @output = output
19
+ @reline = reline
20
+ @quiet = quiet
21
+ @reading = false
22
+ @collecting = false
23
+ @inbox = []
24
+ end
25
+
26
+ def reading?
27
+ synchronize { @reading }
28
+ end
29
+
30
+ def with_reading
31
+ synchronize { @reading = true }
32
+ yield
33
+ ensure
34
+ synchronize { @reading = false }
35
+ end
36
+
37
+ def print_safe_notice(text, force: false)
38
+ return false if text.nil? || text.to_s.empty?
39
+
40
+ clean_text = text.to_s.sub(/\A\n+/, "")
41
+
42
+ synchronize do
43
+ if @quiet && !force
44
+ @inbox << clean_text
45
+ return false
46
+ end
47
+
48
+ if @reading
49
+ @output.print "\r\e[2K"
50
+ @output.puts clean_text
51
+ redisplay_cleanly
52
+ else
53
+ @output.puts clean_text
54
+ end
55
+ @output.flush if @output.respond_to?(:flush)
56
+ true
57
+ end
58
+ end
59
+
60
+ def quiet?
61
+ synchronize { @quiet }
62
+ end
63
+
64
+ def inbox
65
+ synchronize { @inbox.dup }
66
+ end
67
+
68
+ def flush_inbox!
69
+ synchronize do
70
+ items = @inbox.dup
71
+ @inbox.clear
72
+ items
73
+ end
74
+ end
75
+
76
+ def flush_notices!
77
+ items = flush_inbox!
78
+ return false if items.empty?
79
+
80
+ synchronize do
81
+ if @reading
82
+ @output.print "\r\e[2K"
83
+ items.each { |item| @output.puts item }
84
+ redisplay_cleanly
85
+ else
86
+ items.each { |item| @output.puts item }
87
+ end
88
+ @output.flush if @output.respond_to?(:flush)
89
+ end
90
+ true
91
+ end
92
+
93
+ # Plan 109: true when Reline is blocked reading AND the in-progress
94
+ # line is empty — the only state where an injected Enter submits an
95
+ # EMPTY line and cannot truncate user typing. respond_to? guards
96
+ # follow the redisplay_cleanly pattern: a double without
97
+ # line_buffer cannot prove emptiness, so it reads as "not idle"
98
+ # (never wake).
99
+ def idle_and_empty?
100
+ synchronize do
101
+ return false unless @reading
102
+ return false unless @reline.respond_to?(:line_buffer)
103
+
104
+ @reline.line_buffer.to_s.strip.empty?
105
+ end
106
+ end
107
+
108
+ # Plan 109: wake a blocked empty readline by injecting an Enter.
109
+ # Reline 0.7.0's IO gates (ANSI/Windows/Dumb) all poll and re-check
110
+ # their ungetc buffer (10ms/100ms/100ms), so the injection reaches
111
+ # the blocked reader on every gate. idle_and_empty? is re-evaluated
112
+ # inside this critical section (Monitor reentrancy makes the nested
113
+ # synchronize free); the residual window between check and gate
114
+ # consumption is inherent to Reline's thread model and accepted. A
115
+ # failing ungetc degrades to false — never raises into the kworker.
116
+ # with_reading never holds this monitor during readline, so this
117
+ # cannot deadlock with the reader.
118
+ def safe_wakeup!
119
+ synchronize do
120
+ return false if @collecting
121
+ return false unless idle_and_empty?
122
+ return false unless @reline.respond_to?(:ungetc)
123
+
124
+ begin
125
+ @reline.ungetc("\n".ord)
126
+ rescue StandardError
127
+ return false
128
+ end
129
+ true
130
+ end
131
+ end
132
+
133
+ # Plan 109: paste collectors (explicit !paste block, paste-burst
134
+ # join) set this so a reactive wake can never inject a blank Enter
135
+ # into the user's pasted text — the wake defers to the next regular
136
+ # prompt instead.
137
+ def with_collecting
138
+ synchronize { @collecting = true }
139
+ yield
140
+ ensure
141
+ synchronize { @collecting = false }
142
+ end
143
+
144
+ private
145
+
146
+ def redisplay_cleanly
147
+ if @reline.respond_to?(:line_editor) && @reline.line_editor
148
+ le = @reline.line_editor
149
+ le.send(:clear_rendered_screen_cache) if le.respond_to?(:clear_rendered_screen_cache, true)
150
+ end
151
+ @reline.redisplay if @reline.respond_to?(:redisplay)
152
+ rescue StandardError
153
+ nil
154
+ end
155
+ end
156
+ end
157
+ end