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,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ # Narrow façade over Loop for the extracted collaborators (Plan 87 E-1,
6
+ # carrying Plan 86 D-5). Collaborators reach loop-held state only through
7
+ # this object instead of poking `@loop` directly (Law of Demeter).
8
+ #
9
+ # Design notes:
10
+ # - Reads are forwarded, not re-declared: capability queries such as
11
+ # `deps.respond_to?(:safe_config)` must keep the exact semantics the
12
+ # collaborators rely on with minimal test doubles (some doubles do not
13
+ # define every accessor), so the surface is forwarded through
14
+ # respond_to_missing?/method_missing guarded by the declared SURFACE.
15
+ # - Writes are an explicit command surface: history rebinding and the
16
+ # counter/flag mutations become named methods instead of leaking
17
+ # Loop's attr_accessors.
18
+ # - `loop_instance` exists for the turn-result contract only: result
19
+ # hashes carry the loop object itself so the shell can drive
20
+ # `continue_loop` afterwards.
21
+ class ShellDeps
22
+ # Read surface: symbols collaborators consume from Loop at call time.
23
+ SURFACE = [
24
+ :history, :activity, :config, :shell, :buffer_manager, :lsp_manager,
25
+ :tool_registry, :provider, :budget_manager, :on_step,
26
+ :step_count, :max_steps, :warn_at_ratio, :context_limit, :max_output_tokens,
27
+ :total_tokens, :invalid_tool_count, :last_turn_streamed,
28
+ :context_warning_emitted, :safe_config,
29
+ :goal_mode?, :goal_wait, :subagent_notice_inbox, :runtime_manager
30
+ ].freeze
31
+
32
+ # Collaborator constructor shim: accepts an already-built façade or a
33
+ # bare loop (test doubles) and wraps the latter.
34
+ def self.wrap(loop_or_deps)
35
+ loop_or_deps.is_a?(ShellDeps) ? loop_or_deps : ShellDeps.new(loop_or_deps)
36
+ end
37
+
38
+ def initialize(loop)
39
+ @loop = loop
40
+ end
41
+
42
+ # Turn-result contract (see class comment): the underlying Loop object.
43
+ def loop_instance
44
+ @loop
45
+ end
46
+
47
+ # ── Command surface ───────────────────────────────────────────────
48
+ # compact_history is pure and returns a new array, so the history
49
+ # reference itself must be rebound on Loop (BudgetManager compaction).
50
+ def replace_history!(new_history)
51
+ @loop.history = new_history
52
+ end
53
+
54
+ def reset_invalid_tools!
55
+ @loop.invalid_tool_count = 0
56
+ end
57
+
58
+ def record_invalid_tool!
59
+ @loop.invalid_tool_count += 1
60
+ end
61
+
62
+ def mark_last_turn_streamed!(streamed)
63
+ @loop.last_turn_streamed = streamed
64
+ end
65
+
66
+ def flag_context_warning!
67
+ @loop.context_warning_emitted = true
68
+ end
69
+
70
+ def emergency_compact_and_notify!(reason)
71
+ @loop.emergency_compact_and_notify!(reason)
72
+ end
73
+
74
+ def respond_to_missing?(name, include_private = false)
75
+ (SURFACE.include?(name) && @loop.respond_to?(name, include_private)) || super
76
+ end
77
+
78
+ def method_missing(name, *, &)
79
+ return @loop.public_send(name, *, &) if SURFACE.include?(name) && @loop.respond_to?(name)
80
+
81
+ super
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,328 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "finish_message_selector"
4
+ require_relative "auto_apply"
5
+ require_relative "tool_call_id"
6
+ require_relative "tool_history"
7
+ require_relative "tool_circuit_breaker"
8
+ require_relative "maestro_gate"
9
+ require_relative "lsp_feedback"
10
+
11
+ module Rubik
12
+ module Orchestrator
13
+ # Dispatches a single normalized tool call against the registry (Plan 54
14
+ # D-6). The executor is a stateless collaborator: every piece of loop
15
+ # state (history, counters, shell wiring, streamed flag) physically stays
16
+ # on Loop and is reached through the ShellDeps façade (Plan 87 E-1), so
17
+ # send-based tests and the res[:streamed] contract keep working unchanged.
18
+ class StepExecutor
19
+ include MaestroGate
20
+
21
+ ERROR_PREFIXES = [
22
+ "-- ",
23
+ "Error:",
24
+ "Error ",
25
+ "Usage:",
26
+ "Pre-flight verification failed",
27
+ "Bilinmeyen komut",
28
+ "Failed",
29
+ "Security blocked",
30
+ "Command error",
31
+ "[EXIT ",
32
+ "Test run [FAILED]",
33
+ "Lint [FAILED]",
34
+ "[CONFLICT]",
35
+ "[CONFLICT "
36
+ ].freeze
37
+
38
+ # Plan 87 E-1: state is reached through the ShellDeps façade. Accepts a
39
+ # bare loop for compatibility with existing test doubles (wrapped).
40
+ def initialize(loop_or_deps)
41
+ @deps = ShellDeps.wrap(loop_or_deps)
42
+ end
43
+
44
+ def execute_single_tool_call(tool_call, text_resp, auto_apply_mode = nil)
45
+ tool_name = tool_call["tool"] || tool_call[:tool]
46
+ tool_args = tool_call["args"] || tool_call[:args] || {}
47
+ call_id = tool_call["id"] || tool_call[:id] || ToolCallId.mint(@deps.step_count, unique: true)
48
+
49
+ tool_instance = @deps.respond_to?(:tool_registry) && @deps.tool_registry ? @deps.tool_registry.get(tool_name) : nil
50
+ canonical_name = tool_instance ? tool_instance.name : tool_name.to_s.downcase
51
+
52
+ # Auto-apply buffer before running tests or shell commands on disk if
53
+ # configured (decision: AutoApply; explicit mode = test injection seam)
54
+ if AutoApply.apply?(@deps.config, trigger: :pre_execution, tool: canonical_name, mode: auto_apply_mode) && @deps.buffer_manager&.dirty?
55
+ applied = AutoApply.apply_and_sync(@deps.buffer_manager, @deps.shell)
56
+ @deps.on_step&.call(type: :auto_apply, reason: "pre_execution", files: applied, step: @deps.step_count, max_steps: @deps.max_steps)
57
+ end
58
+
59
+ return handle_finish_tool(tool_args, text_resp, auto_apply_mode) if canonical_name == "finish"
60
+
61
+ # Maestro gate: maestro'nun doğrudan yürütme araçları reddedilir
62
+ # (detay ve config semantics: maestro_gate.rb).
63
+ if maestro_gate_blocks?(canonical_name, tool_args)
64
+ maestro_refusal(canonical_name, tool_name, call_id)
65
+ nil
66
+ elsif tool_instance
67
+ execute_registered_tool(tool_instance, tool_name, tool_args, call_id, auto_apply_mode)
68
+ nil
69
+ else
70
+ handle_invalid_tool(tool_name, call_id)
71
+ end
72
+ end
73
+
74
+ def handle_finish_tool(tool_args, text_resp, auto_apply_mode = nil)
75
+ if AutoApply.apply?(@deps.config, trigger: :finish, mode: auto_apply_mode) && @deps.buffer_manager&.dirty?
76
+ applied = AutoApply.apply_and_sync(@deps.buffer_manager, @deps.shell)
77
+ @deps.on_step&.call(type: :auto_apply, reason: "finish", files: applied, step: @deps.step_count, max_steps: @deps.max_steps)
78
+ end
79
+
80
+ clean_text_resp = clean_llm_text(text_resp)
81
+ tool_msg = (tool_args["message"] || tool_args[:message]).to_s.strip
82
+
83
+ # Review Finding 5: message-selection heuristic extracted to
84
+ # FinishMessageSelector (documented, named constants, unit-pinned).
85
+ final_msg = FinishMessageSelector.select(clean_text_resp, tool_msg)
86
+
87
+ # Plan 61: the marker must know where the content went — streamed
88
+ # turns already printed it above, batch turns render right below.
89
+ @deps.on_step&.call(type: :finish, message: final_msg, streamed: @deps.last_turn_streamed == true,
90
+ step: @deps.step_count, max_steps: @deps.max_steps)
91
+ # streamed must reflect the FINISHING turn's reality: its text may
92
+ # have been streamed as chunks before the finish call. A hardcoded
93
+ # false made the free-text render treat an already-printed answer as
94
+ # unprinted and print it a second time.
95
+ { ok: true, output: final_msg, steps: @deps.step_count, tokens: @deps.total_tokens,
96
+ streamed: @deps.last_turn_streamed == true, loop_instance: @deps.loop_instance }
97
+ end
98
+
99
+ def execute_registered_tool(tool_instance, tool_name, tool_args, call_id, auto_apply_mode = nil)
100
+ @deps.on_step&.call(type: :tool_call, tool: tool_name, args: tool_args, step: @deps.step_count, max_steps: @deps.max_steps, call_id: call_id)
101
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
102
+
103
+ # Opt-in circuit breaker (G19): when enabled and the circuit for this
104
+ # tool is open, raise into the normal error-result plumbing below so
105
+ # the loop sees a regular failed tool result without executing the
106
+ # wedged backend again. Disabled by default.
107
+ breaker = circuit_breaker_for
108
+ blocked_by_circuit = breaker && !breaker.allow?(tool_instance.name)
109
+
110
+ tool_result = begin
111
+ if blocked_by_circuit
112
+ raise ToolCircuitBreaker::CircuitOpenError,
113
+ "circuit open for '#{tool_instance.name}' — repeated failures; waiting out the cooldown before retrying"
114
+ end
115
+
116
+ raw_output = tool_instance.execute(tool_args)
117
+ t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
118
+ dur = ((t1 - t0) * 1000).round(1)
119
+
120
+ max_chars = @deps.shell&.config&.fetch("tools.max_output_chars", 12_000) || 12_000
121
+ max_lines = @deps.shell&.config&.fetch("tools.max_output_lines", 300) || 300
122
+ trunc_strat = @deps.shell&.config&.fetch("tools.truncation_strategy", "head_tail") || "head_tail"
123
+
124
+ if raw_output.is_a?(Rubik::Tools::Result)
125
+ trunc_res = Rubik::Tools::Truncator.truncate(raw_output.output, max_chars: max_chars, max_lines: max_lines, strategy: trunc_strat)
126
+ effective_dur = raw_output.duration_ms.positive? ? raw_output.duration_ms : dur
127
+ first_line = raw_output.output.lines.first.to_s.strip
128
+ first_line = nil if first_line.empty?
129
+ Rubik::Tools::Result.new(
130
+ tool_name: raw_output.tool_name.to_s.empty? ? tool_name : raw_output.tool_name,
131
+ call_id: raw_output.call_id.to_s.empty? ? call_id : raw_output.call_id,
132
+ status: raw_output.status,
133
+ error: raw_output.error || (raw_output.error? ? first_line : nil),
134
+ exit_code: raw_output.exit_code,
135
+ output: trunc_res[:text],
136
+ duration_ms: effective_dur,
137
+ truncated: raw_output.truncated || trunc_res[:truncated],
138
+ metadata: raw_output.metadata
139
+ )
140
+ elsif raw_output.respond_to?(:status) && raw_output.respond_to?(:output)
141
+ out_str = raw_output.output.to_s
142
+ trunc_res = Rubik::Tools::Truncator.truncate(out_str, max_chars: max_chars, max_lines: max_lines, strategy: trunc_strat)
143
+ st = raw_output.status.to_sym
144
+ first_line = out_str.lines.first.to_s.strip
145
+ first_line = nil if first_line.empty?
146
+ err = (raw_output.respond_to?(:error) && raw_output.error) || (st == :error ? first_line : nil)
147
+ Rubik::Tools::Result.new(
148
+ tool_name: tool_name,
149
+ call_id: call_id,
150
+ status: st,
151
+ error: err,
152
+ output: trunc_res[:text],
153
+ duration_ms: dur,
154
+ truncated: trunc_res[:truncated]
155
+ )
156
+ else
157
+ out_str = raw_output.to_s
158
+ trunc_res = Rubik::Tools::Truncator.truncate(out_str, max_chars: max_chars, max_lines: max_lines, strategy: trunc_strat)
159
+
160
+ is_err = if raw_output.respond_to?(:error?)
161
+ raw_output.error?
162
+ elsif raw_output.respond_to?(:failed?)
163
+ raw_output.failed?
164
+ elsif raw_output.is_a?(String)
165
+ ERROR_PREFIXES.any? { |pfx| raw_output.start_with?(pfx) }
166
+ else
167
+ false
168
+ end
169
+
170
+ first_line = out_str.lines.first.to_s.strip
171
+ first_line = nil if first_line.empty?
172
+
173
+ Rubik::Tools::Result.new(
174
+ tool_name: tool_name,
175
+ call_id: call_id,
176
+ status: is_err ? :error : :ok,
177
+ error: is_err ? first_line : nil,
178
+ output: trunc_res[:text],
179
+ duration_ms: dur,
180
+ truncated: trunc_res[:truncated]
181
+ )
182
+ end
183
+ rescue StandardError => e
184
+ t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
185
+ dur = ((t1 - t0) * 1000).round(1)
186
+ err_msg = "Error executing tool '#{tool_name}': #{e.class} - #{e.message}"
187
+ Rubik::Tools::Result.new(
188
+ tool_name: tool_name,
189
+ call_id: call_id,
190
+ status: :error,
191
+ error: e.message,
192
+ output: err_msg,
193
+ duration_ms: dur
194
+ )
195
+ end
196
+
197
+ # always-mode flush after every tool — was historically silent; the
198
+ # post_tool event makes the disk write visible like every other apply
199
+ if breaker && !blocked_by_circuit
200
+ if tool_result.status == :ok
201
+ breaker.record_success(tool_instance.name)
202
+ else
203
+ breaker.record_failure(tool_instance.name)
204
+ end
205
+ end
206
+
207
+ if AutoApply.apply?(@deps.config, trigger: :post_tool, mode: auto_apply_mode) && @deps.buffer_manager&.dirty?
208
+ applied = AutoApply.apply_and_sync(@deps.buffer_manager, @deps.shell)
209
+ @deps.on_step&.call(type: :auto_apply, reason: "post_tool", files: applied, step: @deps.step_count, max_steps: @deps.max_steps)
210
+ end
211
+
212
+ # Real-time LSP In-Buffer Diagnostics feedback loop (Plan 56 D-5,
213
+ # extracted to LspFeedback for the line budget).
214
+ tool_result = LspFeedback.refresh(deps: @deps, tool_instance: tool_instance,
215
+ tool_args: tool_args, tool_result: tool_result)
216
+
217
+ # Ekrana-bas sözleşmesi (storage-show-surface planı A): display
218
+ # çıktısı tam içerikle kullanıcıya :user_output olarak akar; model,
219
+ # history ve :tool_result özeti yalnız stub görür. Event birincil
220
+ # teslimat olduğu için onsuz stub yalan olur — kaynak yoksa warn.
221
+ if raw_output.respond_to?(:user_display?) && raw_output.user_display? && tool_result.status == :ok
222
+ sink = @deps.respond_to?(:on_step) ? @deps.on_step : nil
223
+ if sink
224
+ sink.call(type: :user_output, content: raw_output.to_s, label: raw_output.display_stub,
225
+ step: @deps.step_count, max_steps: @deps.max_steps)
226
+ else
227
+ warn("[show_file] no :user_output sink wired — display content dropped: #{raw_output.display_stub}")
228
+ end
229
+ tool_result = Rubik::Tools::Result.new(tool_name: tool_name, call_id: call_id, status: :ok,
230
+ output: raw_output.display_stub, duration_ms: tool_result.duration_ms)
231
+ end
232
+
233
+ @deps.on_step&.call(
234
+ type: :tool_result,
235
+ tool: tool_name,
236
+ output: tool_result.output,
237
+ status: tool_result.status,
238
+ duration_ms: tool_result.duration_ms,
239
+ step: @deps.step_count,
240
+ max_steps: @deps.max_steps,
241
+ call_id: call_id,
242
+ line_count: tool_result.output.to_s.lines.size,
243
+ byte_size: tool_result.output.to_s.bytesize,
244
+ tokens: @deps.total_tokens
245
+ )
246
+
247
+ Rubik::Orchestrator::ToolHistory.write(@deps.history, tool_call_id: call_id, tool_name: tool_name,
248
+ status: tool_result.status, duration_ms: tool_result.duration_ms,
249
+ content: tool_result.to_xml)
250
+ @deps.activity.record(tool_name, tool_args, tool_result.status) if @deps.respond_to?(:activity)
251
+ @deps.reset_invalid_tools!
252
+ tool_result
253
+ end
254
+
255
+ def handle_invalid_tool(tool_name, call_id)
256
+ @deps.record_invalid_tool!
257
+ @deps.activity.record(tool_name, {}, :error) if @deps.respond_to?(:activity)
258
+ if @deps.invalid_tool_count >= 2
259
+ return { ok: false, error: "Repeated invalid tool call '#{tool_name}'", steps: @deps.step_count,
260
+ tokens: @deps.total_tokens, loop_instance: @deps.loop_instance }
261
+ end
262
+
263
+ registry = @deps.respond_to?(:tool_registry) && @deps.tool_registry
264
+ all_tool_names = registry ? registry.list.map(&:name) : []
265
+ all_lookup_names = if registry
266
+ (registry.list.map(&:name) + (registry.respond_to?(:aliases_map) ? registry.aliases_map.keys : [])).uniq
267
+ else
268
+ []
269
+ end
270
+
271
+ matched_candidates = Rubik::Tools::FuzzyMatcher.best_matches(tool_name, all_lookup_names)
272
+ candidate_tools = matched_candidates.map { |c| registry&.get(c)&.name || c }.uniq
273
+ diagnostic_msg = Rubik::Tools::DiagnosticReporter.render_unknown_tool(tool_name, candidate_tools: candidate_tools, all_tools: all_tool_names)
274
+
275
+ err_result = Rubik::Tools::Result.new(
276
+ tool_name: tool_name,
277
+ call_id: call_id,
278
+ status: :error,
279
+ error: "Tool '#{tool_name}' does not exist.",
280
+ output: diagnostic_msg,
281
+ duration_ms: 0.0
282
+ )
283
+ Rubik::Orchestrator::ToolHistory.write(@deps.history, tool_call_id: call_id, tool_name: tool_name,
284
+ status: :error, duration_ms: 0.0, content: err_result.to_xml)
285
+ nil
286
+ end
287
+
288
+ private
289
+
290
+ # G19: one breaker per executor, built from the first enabled config
291
+ # sighting. Threshold/cooldown changes via live !config set are picked
292
+ # up on the next executor instantiation — acceptable for an opt-in
293
+ # guard (the executor is rebuilt per loop).
294
+ def circuit_breaker_for
295
+ return @circuit_breaker if @circuit_breaker
296
+
297
+ settings = @deps.shell&.config&.fetch("tools.circuit_breaker", nil)
298
+ return nil unless settings.is_a?(Hash) && settings["enabled"]
299
+
300
+ @circuit_breaker = ToolCircuitBreaker.new(
301
+ threshold: settings.fetch("threshold", 5),
302
+ cooldown_seconds: settings.fetch("cooldown_seconds", 60)
303
+ )
304
+ end
305
+
306
+ def clean_llm_text(text)
307
+ str = text.to_s.strip
308
+ return "" if str.empty?
309
+
310
+ # If whole text is raw JSON tool call e.g. {"tool": "finish", "args": {...}}
311
+ if str.start_with?("{") && str.end_with?("}")
312
+ parsed = begin
313
+ JSON.parse(str)
314
+ rescue JSON::ParserError
315
+ nil
316
+ end
317
+ return "" if parsed.is_a?(Hash) && (parsed.key?("tool") || parsed.key?(:tool))
318
+ # A {...} envelope that failed to parse is itself a malformed tool
319
+ # call; swallow it to "" rather than leaking the raw text as content.
320
+ return "" if parsed.nil?
321
+ end
322
+
323
+ # Strip ```json ... ``` blocks
324
+ str.gsub(/```(?:json)?\s*\{.*?\}\s*```/m, "").strip
325
+ end
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../llm/chunk_assembler"
4
+ require_relative "provider_recovery"
5
+
6
+ module Rubik
7
+ module Orchestrator
8
+ # Streaming request execution and chunk-to-response assembly (Plan 54
9
+ # D-6). Stateless collaborator over Loop-held state. Overflow recovery
10
+ # (retry-once after emergency compaction; Interrupt, not being a
11
+ # StandardError, escapes to the shell's console restoration) lives in
12
+ # ProviderRecovery — this class keeps the chunk pipeline and keeps ALL
13
+ # per-attempt state inside the recovered block.
14
+ class StreamManager
15
+ # Plan 87 E-1: state is reached through the ShellDeps façade. Accepts a
16
+ # bare loop for compatibility with existing test doubles (wrapped).
17
+ def initialize(loop_or_deps)
18
+ @deps = ShellDeps.wrap(loop_or_deps)
19
+ end
20
+
21
+ def execute_streaming_request(opts)
22
+ # Recovery policy lives in ProviderRecovery; the block owns ALL
23
+ # per-attempt state. `retry` re-yields the block, so a failed
24
+ # attempt's accumulated chunks can never pollute the retried
25
+ # response (the Candidate 03 fix). The config guard mirrors
26
+ # #process_stream_chunk: bare test doubles may not expose config.
27
+ ProviderRecovery.with_recovery(
28
+ config: @deps.respond_to?(:config) ? @deps.config : nil,
29
+ compact: ->(reason) { @deps.emergency_compact_and_notify!(reason) }
30
+ ) do
31
+ accumulated_chunks = []
32
+ streamed_text = false
33
+ res = @deps.provider.stream_chat(@deps.history, opts) do |chunk|
34
+ accumulated_chunks << chunk
35
+ streamed_text = true if process_stream_chunk(chunk)
36
+ end
37
+
38
+ if res.is_a?(Enumerator) && accumulated_chunks.empty?
39
+ res.each do |chunk|
40
+ accumulated_chunks << chunk
41
+ streamed_text = true if process_stream_chunk(chunk)
42
+ end
43
+ end
44
+
45
+ @deps.on_step&.call(type: :chunk_end, step: @deps.step_count, max_steps: @deps.max_steps) if streamed_text
46
+ @deps.mark_last_turn_streamed!(streamed_text)
47
+ build_response_from_chunks(accumulated_chunks, streamed_text: streamed_text)
48
+ end
49
+ end
50
+
51
+ def build_response_from_chunks(accumulated_chunks, streamed_text: false)
52
+ # Plan 61 D-4: assembly moved to Rubik::LLM::ChunkAssembler (shared
53
+ # with BaseProvider#complete); this delegate keeps the historical
54
+ # call signature for tests and any external consumers.
55
+ Rubik::LLM::ChunkAssembler.build(
56
+ accumulated_chunks,
57
+ streamed: streamed_text == true,
58
+ estimate_history: -> { @deps.budget_manager.estimate_tokens_from_history },
59
+ estimate_text: ->(t) { @deps.provider.respond_to?(:estimate_tokens) ? @deps.provider.estimate_tokens(t) : nil },
60
+ on_malformed_args: lambda { |tool_name, fragment|
61
+ @deps.on_step&.call(
62
+ type: :malformed_tool_args, tool: tool_name,
63
+ fragment: fragment.to_s[0, 200],
64
+ step: @deps.step_count, max_steps: @deps.max_steps
65
+ )
66
+ }
67
+ )
68
+ end
69
+
70
+ # Forwards HTTPClient retry notifications to the shell's on_step channel
71
+ # as :llm_retry step_info so the REPL prints live retry lines instead of
72
+ # appearing frozen. Custom providers that ignore opts[:on_retry] simply
73
+ # produce no retry output. Loop state is read at invocation time.
74
+ def llm_retry_notifier
75
+ lambda do |attempt, max, reason|
76
+ @deps.on_step&.call(type: :llm_retry, attempt: attempt, max: max, reason: reason,
77
+ step: @deps.step_count, max_steps: @deps.max_steps)
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ # One chunk, both delivery forms (stream block + Enumerator replay):
84
+ # resolve the reasoning-capture knob, forward deltas as step events,
85
+ # and report whether a text delta was emitted (drives streamed_text
86
+ # and chunk_end).
87
+ def process_stream_chunk(chunk)
88
+ capture_reasoning = if @deps.respond_to?(:safe_config)
89
+ @deps.safe_config("llm.capture_reasoning", true) != false
90
+ elsif @deps.respond_to?(:config) && @deps.config.respond_to?(:fetch)
91
+ @deps.config.fetch("llm.capture_reasoning", true) != false
92
+ else
93
+ true
94
+ end
95
+ if capture_reasoning &&
96
+ chunk.respond_to?(:reasoning_delta?) && chunk.reasoning_delta? && !chunk.reasoning_delta.to_s.empty?
97
+ @deps.on_step&.call(
98
+ type: :llm_thought_delta,
99
+ delta: chunk.reasoning_delta,
100
+ step: @deps.step_count,
101
+ max_steps: @deps.max_steps
102
+ )
103
+ end
104
+ emitted_text = false
105
+ if chunk.text_delta? && !chunk.text_delta.to_s.empty?
106
+ emitted_text = true
107
+ @deps.on_step&.call(
108
+ type: :chunk,
109
+ delta: chunk.text_delta,
110
+ step: @deps.step_count,
111
+ max_steps: @deps.max_steps
112
+ )
113
+ end
114
+ emitted_text
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ module Subagent
6
+ # Birth choreography for EVERY subagent run — spawn and redeploy alike.
7
+ # Extracted from supervisor.rb/reuse.rb to keep both under their
8
+ # module-hygiene caps (the messaging.rb precedent) and, more
9
+ # importantly, to make the unit-registration ORDERING RULE structural
10
+ # instead of comment-enforced: the kernel unit is registered BEFORE
11
+ # start_async at both sites, with a birth state DERIVED from instance
12
+ # state — a unit minted while the instance is settled (the redeploy
13
+ # entry: completed/failed) falls back to :pending, so it can never be
14
+ # born dead. start_async's own @unit&.sync_state! is then the single
15
+ # promotion edge (pending -> active), exactly as for fresh spawns.
16
+ # The old dead-born trap made reap_dead_kernel_units! dispose the LIVE
17
+ # child ctx, silently killing the ask_question channel of the
18
+ # redeployed run (pinned by test_redeploy_registers_fresh_unit_without_state_error).
19
+ module ActivationSupervisor
20
+ # Call UNDER the supervisor monitor (both birth sites already ran
21
+ # start_async under it — the worker is parked before spawn returns).
22
+ # Order is load-bearing: coeffect provides first (coeffect resolution
23
+ # happens at execute time, but a start_async completion must never
24
+ # race an unprovided asker), unit registration second (pre-start, so
25
+ # the kernel board sees a live unit), start_async last.
26
+ def activate_instance!(inst, child_ctx:)
27
+ # ask_question'ın subagent düzlemi: instance kendi child ctx'ine
28
+ # asker olarak publish edilir.
29
+ child_ctx&.provide(Rubik::Coeffects::SUBAGENT_QUESTION_ASKER, inst)
30
+ # send_message atfı: subagent→maestro mesajları gerçek gönderen
31
+ # id'siyle taşır (coeffect yoksa root default'u yanlış atfı yapar).
32
+ child_ctx&.provide(Rubik::Coeffects::SUBAGENT_SENDER_ID, inst.id)
33
+ eff_init = @init || (@root_context.respond_to?(:get) && Rubik::Coeffects.get(@root_context, Rubik::Coeffects::INIT))
34
+ eff_init&.register(Kernel::SubagentUnit.new(inst, context: child_ctx, birth_state: unit_birth_state(inst)))
35
+ inst.start_async { |completed_inst| handle_subagent_completion(completed_inst) }
36
+ inst
37
+ end
38
+
39
+ # Child-ctx mint + the one degrade warning. A root context without
40
+ # #extend (duck-type host) cannot carry the question/sender
41
+ # coeffects — the degradation is made visible, never silent.
42
+ def extend_root_context!(origin)
43
+ child_ctx = @root_context.respond_to?(:extend) ? @root_context.extend : nil
44
+ Rubik::Log.warn(:orchestrator, "subagent #{origin}: root context lacks #extend — no child ctx; subagent planes degrade") unless child_ctx
45
+ child_ctx
46
+ end
47
+
48
+ private
49
+
50
+ # The registration-order rule, DERIVED FROM INSTANCE STATE: a unit is
51
+ # born with a LIVE kernel state, never a terminal one. If the
52
+ # instance's current state maps through STATE_MAP into a terminal
53
+ # unit state (the redeploy entry: completed/failed), mint :pending
54
+ # instead — start_async's own @unit&.sync_state! (instance.rb) is
55
+ # then the single promotion edge. A born-:pending unit settling
56
+ # forward is a legal Unit#transition_to! edge; a born-terminal unit
57
+ # is frozen forever (sync_state! no-ops on dead).
58
+ def unit_birth_state(inst)
59
+ derived = Kernel::SubagentUnit::STATE_MAP.fetch(inst.state)
60
+ Kernel::Unit::TERMINAL_STATES.include?(derived) ? :pending : derived
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end