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,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "provider"
4
+ require_relative "client"
5
+ require_relative "formatter"
6
+
7
+ module Rubik
8
+ module LLM
9
+ class AnthropicProvider < BaseProvider
10
+ attr_reader :name, :model, :formatter, :headers, :api_key, :api_key_env
11
+
12
+ def initialize(name: nil, model: "claude-3-7-sonnet-20250219", formatter: AnthropicFormatter.new, headers: {},
13
+ api_key: nil, api_key_env: nil)
14
+ super()
15
+ @name = name.to_s.downcase
16
+ @model = model
17
+ @formatter = formatter || AnthropicFormatter.new
18
+ @headers = headers || {}
19
+ # Plan: Anthropic api_key / api_key_env were silently dropped from the
20
+ # provider spec, so every Anthropic deployment (custom or registered
21
+ # via config) failed auth with no diagnostic. Both a direct api_key and
22
+ # an env-var source are honored, mirroring OpenAICompatibleProvider.
23
+ @api_key = api_key
24
+ @api_key_env = api_key_env
25
+ end
26
+
27
+ # Resolve the Anthropic key from (in order of precedence): an explicit
28
+ # per-call opts[:api_key], the provider-level @api_key, and finally the
29
+ # environment variable named by @api_key_env (default ANTHROPIC_API_KEY).
30
+ def resolve_api_key(opts = {})
31
+ return opts[:api_key] if opts.key?(:api_key) && !opts[:api_key].nil?
32
+ return @api_key if @api_key && !@api_key.to_s.empty?
33
+
34
+ env_name = opts[:api_key_env] || @api_key_env || "ANTHROPIC_API_KEY"
35
+ ENV.fetch(env_name, nil)
36
+ end
37
+
38
+ def complete(messages, opts = {})
39
+ api_key = resolve_api_key(opts)
40
+ unless api_key && !api_key.to_s.empty?
41
+ raise ClientError, "No Anthropic API key available: set ANTHROPIC_API_KEY, pass api_key in config, " \
42
+ "or configure api_key_env for the 'anthropic' provider."
43
+ end
44
+
45
+ messages = Rubik::LLM::MessageSanitizer.normalize(messages)
46
+ target_model = opts[:model] || @model
47
+ # Allow endpoint override (opts[:endpoint] for tests/proxies)
48
+ endpoint = opts[:endpoint] || "https://api.anthropic.com/v1/messages"
49
+
50
+ headers = {
51
+ "x-api-key" => api_key,
52
+ "anthropic-version" => "2023-06-01"
53
+ }
54
+ @headers.each { |k, v| headers[k.to_s] = v.to_s }
55
+ headers.merge!(opts[:headers]) if opts[:headers].is_a?(Hash)
56
+
57
+ # Messages API contract: exactly one top-level `system` field is
58
+ # allowed, so multiple system messages are deliberately joined here,
59
+ # never sent inline.
60
+ system_msgs, user_msgs = messages.partition { |m| m[:role] == "system" || m["role"] == "system" }
61
+
62
+ system_text = system_msgs.map { |m| m[:content] || m["content"] }.join("\n\n")
63
+ payload = build_payload(target_model, user_msgs, system_text, opts, stream: false)
64
+
65
+ timeout = opts[:timeout] || opts[:stream_timeout] || 120
66
+ open_timeout = opts[:open_timeout] || 15
67
+
68
+ res_json = HTTPClient.post_json(
69
+ endpoint,
70
+ headers: headers,
71
+ payload: payload,
72
+ timeout: timeout,
73
+ open_timeout: open_timeout,
74
+ on_retry: opts[:on_retry]
75
+ )
76
+
77
+ content_blocks = res_json["content"] || []
78
+ text_blocks = content_blocks.select { |b| b["type"] == "text" }.map { |b| b["text"] }
79
+ text = text_blocks.join("\n")
80
+
81
+ thought_blocks = content_blocks.select { |b| b["type"] == "thinking" }.map { |b| b["thinking"] }
82
+ thought = thought_blocks.empty? ? nil : thought_blocks.join("\n")
83
+
84
+ tool_blocks = content_blocks.select { |b| b["type"] == "tool_use" }
85
+ parsed_tool_calls = tool_blocks.map do |b|
86
+ {
87
+ "id" => b["id"] || "call_#{rand(1000..9999)}",
88
+ "tool" => b["name"],
89
+ "args" => b["input"] || {}
90
+ }
91
+ end
92
+
93
+ usage = res_json["usage"] || {}
94
+
95
+ Response.new(
96
+ text: text,
97
+ usage: {
98
+ input_tokens: usage["input_tokens"] || 0,
99
+ output_tokens: usage["output_tokens"] || 0
100
+ },
101
+ raw: res_json,
102
+ tool_calls: parsed_tool_calls,
103
+ thought: thought
104
+ )
105
+ end
106
+
107
+ # Streaming variant. Yields Chunks. Without a block, returns an
108
+ # Enumerator (via `to_enum`) so the caller can chain `.each`.
109
+ def stream_chat(messages, opts = {}, &block)
110
+ api_key = resolve_api_key(opts)
111
+ unless api_key && !api_key.to_s.empty?
112
+ raise ClientError, "No Anthropic API key available: set ANTHROPIC_API_KEY, pass api_key in config, " \
113
+ "or configure api_key_env for the 'anthropic' provider."
114
+ end
115
+
116
+ messages = Rubik::LLM::MessageSanitizer.normalize(messages)
117
+ target_model = opts[:model] || @model
118
+ # Allow endpoint override (opts[:endpoint] for tests/proxies;
119
+ # default to the public Anthropic API).
120
+ endpoint = opts[:endpoint] || "https://api.anthropic.com/v1/messages"
121
+
122
+ headers = {
123
+ "x-api-key" => api_key,
124
+ "anthropic-version" => "2023-06-01"
125
+ }
126
+ @headers.each { |k, v| headers[k.to_s] = v.to_s }
127
+ headers.merge!(opts[:headers]) if opts[:headers].is_a?(Hash)
128
+
129
+ # Same Messages API contract as #complete: exactly one top-level
130
+ # `system` field is allowed, so multiple system messages are
131
+ # deliberately joined here rather than passed inline.
132
+ system_msgs, user_msgs = messages.partition { |m| m[:role] == "system" || m["role"] == "system" }
133
+ system_text = system_msgs.map { |m| m[:content] || m["content"] }.join("\n\n")
134
+
135
+ payload = build_payload(target_model, user_msgs, system_text, opts, stream: true)
136
+
137
+ return to_enum(:stream_chat, messages, opts) unless block
138
+
139
+ timeout = opts[:idle_timeout] || opts[:timeout] || opts[:stream_timeout] || 120
140
+ open_timeout = opts[:open_timeout] || 15
141
+
142
+ HTTPClient.post_stream_json(
143
+ endpoint,
144
+ headers: headers,
145
+ payload: payload,
146
+ timeout: timeout,
147
+ open_timeout: open_timeout,
148
+ idle_timeout: opts[:idle_timeout]
149
+ ) do |event|
150
+ chunk = @formatter.parse_stream_event(event)
151
+ next if chunk.nil?
152
+
153
+ block.call(chunk)
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ def build_payload(target_model, user_msgs, system_text, opts, stream: false)
160
+ max_tokens = (opts[:max_tokens] || 1024).to_i
161
+ payload = {
162
+ model: target_model,
163
+ messages: format_messages(user_msgs),
164
+ max_tokens: max_tokens
165
+ }
166
+ payload[:stream] = true if stream
167
+ payload[:system] = system_text unless system_text.to_s.empty?
168
+ payload[:tools] = format_tools(opts[:tools]) if opts[:tools] && !opts[:tools].empty?
169
+
170
+ thinking_opt = opts[:thinking]
171
+ thinking_disabled = [false, :disabled].include?(thinking_opt)
172
+ ratio = (opts[:thinking_budget_ratio] || 0.5).to_f
173
+
174
+ if thinking_opt.is_a?(Hash)
175
+ payload[:thinking] = thinking_opt
176
+ elsif !thinking_disabled && (thinking_opt == true || target_model.to_s.include?("claude-3-7-sonnet"))
177
+ if max_tokens > 2048
178
+ budget = [(max_tokens * ratio).to_i, 1024].max
179
+ budget = [budget, max_tokens - 1024].min
180
+ payload[:thinking] = { type: "enabled", budget_tokens: budget } if budget >= 1024
181
+ end
182
+ end
183
+
184
+ if payload[:thinking]
185
+ payload[:temperature] = 1.0 if opts.key?(:temperature)
186
+ elsif opts.key?(:temperature)
187
+ payload[:temperature] = opts[:temperature]
188
+ end
189
+
190
+ payload
191
+ end
192
+
193
+ def format_messages(messages)
194
+ @formatter.format_messages(messages)
195
+ end
196
+
197
+ def format_tools(tools)
198
+ @formatter.format_tools(tools)
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Red-team: JSON.parse is used in the args paths; without this require a
4
+ # standalone load (or any load order where json is not yet loaded) turns
5
+ # every parse into a NameError that the StandardError rescue silently
6
+ # converts to empty args — tool arguments vanish without a trace.
7
+ require "json"
8
+
9
+ require_relative "provider"
10
+
11
+ module Rubik
12
+ module LLM
13
+ class Chunk
14
+ # Incremental, single-pass assembler (Plan 87 E-2, carrying Plan 86
15
+ # D-1): each pushed chunk updates O(1) accumulators — text/thought
16
+ # buffers, tool-call slots, last-writer-wins usage/raw/finish state —
17
+ # so batch callers never buffer the chunk list. #finish produces the
18
+ # exact Response the multi-pass ChunkAssembler.build produces; the
19
+ # equivalence is pinned by the golden snapshots and the build ↔
20
+ # incremental equivalence test in test_chunk_assembler.rb.
21
+ #
22
+ # Known accepted divergence (same class as Red-team B3): build runs
23
+ # the tool_use_delta pass over ALL chunks before the raw pass, while
24
+ # push interleaves both per chunk. For same-index tool calls arriving
25
+ # as one raw message and one delta with DIFFERENT ids, the resulting
26
+ # slot order may differ; runtime matching is by call id, not order.
27
+ class Assembler
28
+ def initialize
29
+ @text = +""
30
+ @thought = +""
31
+ @tool_calls_map = {}
32
+ @last_usage = nil
33
+ @last_raw = nil
34
+ @finish_raw = nil
35
+ @pushed = false
36
+ end
37
+
38
+ # Whether any chunk reached the assembler (used by #complete to
39
+ # decide the Enumerator drain fallback).
40
+ def pushed?
41
+ @pushed
42
+ end
43
+
44
+ def push(chunk)
45
+ @pushed = true
46
+ @text << chunk.text_delta.to_s unless chunk.text_delta.nil?
47
+ @thought << chunk.reasoning_delta.to_s if chunk.respond_to?(:reasoning_delta?) && chunk.reasoning_delta? && !chunk.reasoning_delta.nil?
48
+
49
+ deltas = chunk.tool_use_delta
50
+ unless deltas.nil?
51
+ deltas_array = deltas.is_a?(Array) ? deltas : [deltas]
52
+ deltas_array.each { |d| process_tool_delta(d) }
53
+ end
54
+
55
+ process_raw_tool_calls(chunk.raw) if chunk.raw.is_a?(Hash)
56
+
57
+ @last_usage = chunk.usage if chunk.usage && !chunk.usage.empty?
58
+ @last_raw = chunk.raw if chunk.raw && !chunk.raw.empty?
59
+ @finish_raw = chunk.raw if chunk.raw.is_a?(Hash) && extract_finish_value(chunk.raw)
60
+ self
61
+ end
62
+
63
+ def finish(streamed: false, estimate_history: nil, estimate_text: nil, on_malformed_args: nil)
64
+ tool_calls, degenerate = parsed_tool_calls_with_degenerate(on_malformed_args)
65
+ @text = append_degenerate_text(degenerate)
66
+ final_usage = resolve_usage(@text, estimate_history, estimate_text)
67
+
68
+ Rubik::LLM::Response.new(
69
+ text: @text,
70
+ usage: final_usage,
71
+ raw: promoted_raw,
72
+ tool_calls: tool_calls,
73
+ streamed: streamed == true,
74
+ thought: @thought.empty? ? nil : @thought
75
+ )
76
+ end
77
+
78
+ private
79
+
80
+ def process_tool_delta(delta)
81
+ return unless delta.is_a?(Hash)
82
+
83
+ base_idx = delta["index"] || delta[:index] || 0
84
+ d_id = delta["id"] || delta[:id]
85
+ d_tool = delta["tool"] || delta[:tool] || delta["name"] || delta[:name]
86
+
87
+ key = resolve_slot(base_idx, d_id)
88
+ @tool_calls_map[key]["id"] ||= d_id
89
+ @tool_calls_map[key]["tool"] ||= d_tool
90
+ @tool_calls_map[key]["args_json"] += (delta["arguments_delta"] || delta[:arguments_delta]).to_s if delta["arguments_delta"] || delta[:arguments_delta]
91
+ @tool_calls_map[key]["args"] = delta["args"] || delta[:args] if delta["args"] || delta[:args]
92
+ end
93
+
94
+ def process_raw_tool_calls(raw)
95
+ if raw["type"] == "content_block_start" && raw["content_block"].is_a?(Hash)
96
+ cb = raw["content_block"]
97
+ if cb["type"] == "tool_use"
98
+ key = resolve_slot(raw["index"] || 0, cb["id"])
99
+ @tool_calls_map[key]["id"] ||= cb["id"]
100
+ @tool_calls_map[key]["tool"] ||= cb["name"]
101
+ end
102
+ end
103
+
104
+ return unless raw["message"] && raw["message"]["tool_calls"].is_a?(Array)
105
+
106
+ raw["message"]["tool_calls"].each_with_index do |tc, idx|
107
+ fn = tc["function"] || {}
108
+ key = resolve_slot(idx, tc["id"])
109
+ @tool_calls_map[key]["id"] ||= tc["id"]
110
+ @tool_calls_map[key]["tool"] ||= fn["name"]
111
+ @tool_calls_map[key]["args"] ||= fn["arguments"]
112
+ end
113
+ end
114
+
115
+ # Red-team B2: üç montaj yolu (tool_use_delta, content_block_start, raw
116
+ # message) aynı slot-çözme mantığını kullansın. Aynı index üzerinde dolu ve
117
+ # farklı bir id görüldüğünde ayrı bir slot açılır: InternLM/lmdeploy ve bazı
118
+ # stream'ler aynı index'i yeni bir logical call için yeniden kullanabilir;
119
+ # katıştırmak arguments'ları karıştırıp bozuk JSON'a yol açar. Boş/nil id bir
120
+ # call'ı tanımlamaz — split'i tetiklemez.
121
+ #
122
+ # Red-team notları (bilinen sınırlar):
123
+ # - B3: `tool_calls_map` insertion-order üretir; interleaved delta sırası
124
+ # `values` sırasını değiştirebilir. Runtime'da zararsızdır çünkü sonraki
125
+ # eşleşme call id/tool_call_id üzerindendir, sıra değil.
126
+ # - B4: aynı index üzerinde id'siz iki FARKLI call ayırt edilemez (bilgi
127
+ # yok). lmdeploy her call'a en az bir id ya da benzersiz index verdiği
128
+ # sürece oluşmaz.
129
+ # - B5: composite key `"#{base_idx}:#{d_id}"`; d_id içinde ':' olsa bile
130
+ # farklı key'ler ayrı kalır (çakışma yok, çünkü tam-string eşleşmesi).
131
+ def resolve_slot(base_idx, d_id)
132
+ key = base_idx
133
+ if @tool_calls_map.key?(base_idx) &&
134
+ !d_id.nil? && !d_id.to_s.empty? &&
135
+ !@tool_calls_map[base_idx]["id"].nil? &&
136
+ @tool_calls_map[base_idx]["id"] != d_id
137
+ key = "#{base_idx}:#{d_id}"
138
+ end
139
+ @tool_calls_map[key] ||= { "id" => nil, "tool" => nil, "args_json" => "", "args" => nil }
140
+ key
141
+ end
142
+
143
+ def parsed_tool_calls_with_degenerate(on_malformed_args)
144
+ parsed_tool_calls = @tool_calls_map.values.map do |tc_data|
145
+ args = tc_data["args"]
146
+ malformed = nil
147
+ if args.nil?
148
+ args_str = tc_data["args_json"].strip
149
+ if args_str.empty?
150
+ args = {}
151
+ else
152
+ begin
153
+ parsed = JSON.parse(args_str)
154
+ if parsed.is_a?(Hash)
155
+ args = parsed
156
+ else
157
+ args = {}
158
+ malformed = args_str
159
+ end
160
+ rescue JSON::ParserError
161
+ on_malformed_args&.call(tc_data["tool"], args_str)
162
+ args = {}
163
+ malformed = args_str
164
+ end
165
+ end
166
+ elsif args.is_a?(String)
167
+ begin
168
+ parsed = JSON.parse(args)
169
+ if parsed.is_a?(Hash)
170
+ args = parsed
171
+ else
172
+ malformed = args
173
+ args = {}
174
+ end
175
+ rescue JSON::ParserError
176
+ on_malformed_args&.call(tc_data["tool"], args)
177
+ malformed = args
178
+ args = {}
179
+ end
180
+ elsif !args.is_a?(Hash)
181
+ malformed = args.to_s
182
+ args = {}
183
+ end
184
+
185
+ row = {
186
+ "id" => tc_data["id"] || "call_#{rand(1000..9999)}",
187
+ "tool" => tc_data["tool"],
188
+ "args" => args || {}
189
+ }
190
+ # Issue #55 3.4: the executor turns this marker into the
191
+ # model-visible error row quoting the rejected payload — the
192
+ # degrade must not be silent at both layers.
193
+ row["malformed_args"] = malformed if malformed
194
+ row
195
+ end
196
+ # Red-team B1: tool name'siz yarım call'ı sessizce silmek, üretilmiş
197
+ # arguments'ı da yok eder ve (text boşken) "Empty response from model"a
198
+ # yol açabilir. Arguments'ı metne bağlayarak bilgiyi koruyor ve modelin
199
+ # boş olmayan bir yanıt görmesini sağlıyoruz; gerçekten boş call'lar elenir.
200
+ degenerate = []
201
+ parsed_tool_calls = parsed_tool_calls.reject do |tc|
202
+ if tc["tool"].nil? || tc["tool"].to_s.empty?
203
+ args = tc["args"]
204
+ degenerate << args if args.is_a?(Hash) && !args.empty?
205
+ true
206
+ else
207
+ false
208
+ end
209
+ end
210
+ [parsed_tool_calls, degenerate]
211
+ end
212
+
213
+ def append_degenerate_text(degenerate)
214
+ return @text if degenerate.empty?
215
+
216
+ appended = degenerate.map { |a| "[Incomplete tool call — arguments: #{a.to_json}]" }.join(" ")
217
+ @text.to_s.empty? ? appended : "#{@text}\n#{appended}"
218
+ end
219
+
220
+ def resolve_usage(text, estimate_history, estimate_text)
221
+ in_tokens = @last_usage && (@last_usage[:input_tokens] || @last_usage["input_tokens"])
222
+ out_tokens = @last_usage && (@last_usage[:output_tokens] || @last_usage["output_tokens"])
223
+
224
+ in_tokens ||= estimate_history.respond_to?(:call) ? estimate_history.call : nil
225
+ out_tokens = estimate_text.call(text) || (text.length / 4.0).ceil if out_tokens.nil? && estimate_text.respond_to?(:call)
226
+
227
+ if in_tokens || out_tokens
228
+ { input_tokens: in_tokens || 0, output_tokens: out_tokens || 0 }
229
+ else
230
+ {}
231
+ end
232
+ end
233
+
234
+ def promoted_raw
235
+ last_raw = @last_raw || {}
236
+ if @finish_raw && !extract_finish_value(last_raw)
237
+ last_raw = last_raw.dup
238
+ finish_val = @finish_raw["finish_reason"] || @finish_raw.dig("choices", 0, "finish_reason")
239
+ last_raw["finish_reason"] = finish_val if finish_val
240
+ stop_val = @finish_raw["stop_reason"] || @finish_raw.dig("delta", "stop_reason")
241
+ last_raw["stop_reason"] = stop_val if stop_val
242
+ last_raw["done_reason"] = @finish_raw["done_reason"] if @finish_raw["done_reason"]
243
+ end
244
+ last_raw
245
+ end
246
+
247
+ def extract_finish_value(raw)
248
+ return nil unless raw.is_a?(Hash)
249
+
250
+ raw["finish_reason"] || raw.dig("choices", 0, "finish_reason") ||
251
+ raw["stop_reason"] || raw.dig("delta", "stop_reason") || raw["done_reason"]
252
+ end
253
+ end
254
+ end
255
+
256
+ # Assembles a final Response from accumulated stream Chunks (Plan 61
257
+ # D-4). Single source of truth shared by Orchestrator::StreamManager
258
+ # (live agent loop) and BaseProvider#complete (batch fallback for
259
+ # stream-only providers), so both paths produce identical text,
260
+ # tool_calls and usage — provider-agnostic assembly is the invariant.
261
+ #
262
+ # Optional callbacks:
263
+ # estimate_history:: ->( ) { Integer } input-token fallback (loop history estimate)
264
+ # estimate_text:: ->(text) { Integer } output-token fallback (provider estimator)
265
+ # on_malformed_args:: ->(tool_name, fragment) — D-10 diagnostics for
266
+ # tool-call arguments that arrived truncated/unparseable; the call
267
+ # still executes with {} args so the model sees the tool's complaint.
268
+ module ChunkAssembler
269
+ module_function
270
+
271
+ # Plan 87 E-2: build feeds the chunk list through the incremental
272
+ # Chunk::Assembler — one assembly logic, two consumption styles.
273
+ def build(chunks, streamed: false, estimate_history: nil, estimate_text: nil, on_malformed_args: nil)
274
+ assembler = Chunk::Assembler.new
275
+ chunks.each { |chunk| assembler.push(chunk) }
276
+ assembler.finish(
277
+ streamed: streamed,
278
+ estimate_history: estimate_history,
279
+ estimate_text: estimate_text,
280
+ on_malformed_args: on_malformed_args
281
+ )
282
+ end
283
+ end
284
+ end
285
+ end