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,388 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../prompt"
5
+ require_relative "../prompt/conversation_rows"
6
+ require_relative "activity_log"
7
+ require_relative "shell_deps"
8
+ require_relative "step_executor"
9
+ require_relative "budget_manager"
10
+ require_relative "stream_manager"
11
+ require_relative "provider_resolver"
12
+ require_relative "provider_recovery"
13
+ require_relative "continuation_manager"
14
+ require_relative "goal_wait"
15
+ require_relative "subagent_notice_inbox"
16
+ require_relative "turn_executor"
17
+ require_relative "turn_executor/user_channel"
18
+ require_relative "tool_call_id"
19
+ require_relative "subagent/notices"
20
+
21
+ module Rubik
22
+ module Orchestrator
23
+ class Loop
24
+ attr_accessor :history, :invalid_tool_count, :last_turn_streamed, :context_warning_emitted
25
+ attr_reader :max_steps, :step_count, :model, :total_tokens, :warn_at_ratio, :runtime_manager, :loop_context, :context_limit, :max_output_tokens, :config
26
+ # Collaborator-facing state (Plan 54 D-5): loop state physically stays on Loop;
27
+ # extracted collaborators reach it through these accessors.
28
+ attr_reader :provider, :on_step, :tool_registry, :shell, :buffer_manager, :lsp_manager, :step_executor,
29
+ :budget_manager, :stream_manager, :activity
30
+
31
+ # Channel-owned counter (TurnExecutor::NudgePolicy); the public reader
32
+ # name is load-bearing (tests, REPL surfaces).
33
+ def empty_response_count = turn_executor.nudge_policy.count
34
+
35
+ def initialize(provider:, tool_registry:, max_steps: 40, warn_at_ratio: 0.75, model: nil, on_step: nil, shell: nil, context_limit: nil,
36
+ max_output_tokens: nil)
37
+ @provider = provider
38
+ @tool_registry = tool_registry
39
+ @max_steps = (max_steps || 40).to_i
40
+ @warn_at_ratio = (warn_at_ratio || 0.75).to_f
41
+ @model = model
42
+ @on_step = on_step
43
+ # The shell must be STABLE from construction through run(): the
44
+ # ivars below snapshot its services at initialize (execution uses
45
+ # these), while the system prompt re-reads the public readers at
46
+ # run() start (Prompt::Services.from_shell) — a service swapped
47
+ # in between would make prompt and execution disagree.
48
+ @shell = shell
49
+ @buffer_manager = shell&.buffer_manager
50
+ @snapshot_store = shell&.snapshot_store
51
+ @context_index = shell&.context_index
52
+ @lsp_manager = shell&.lsp_manager
53
+ @config = shell&.config
54
+ @root_dir = File.expand_path(shell&.buffer_manager&.root_dir || Dir.pwd)
55
+ @runtime_manager = shell&.runtime_manager
56
+ @context_limit = context_limit || 32_768
57
+ @max_output_tokens = (max_output_tokens || (@config.respond_to?(:fetch) ? @config.fetch("llm.default_max_output_tokens", 4_096) : 4_096)).to_i
58
+ @loop_context = nil
59
+ @step_count = 0
60
+ @history = []
61
+ @total_tokens = { input: 0, output: 0 }
62
+ @invalid_tool_count = 0
63
+ @last_turn_streamed = false
64
+ @context_warning_emitted = false
65
+ # Plan 61 D-15: per-run activity ledger feeding the turn summary.
66
+ @activity = ActivityLog.new
67
+ # Plan 87 E-1: collaborators reach loop state only through the
68
+ # ShellDeps façade, not through raw @loop reach-through.
69
+ # Review Öneri 3: provider-config resolution follows the same
70
+ # collaborator-extraction pattern (ProviderResolver, built lazily
71
+ # on first use — see #provider_resolver).
72
+ @shell_deps = ShellDeps.new(self)
73
+ @step_executor = StepExecutor.new(@shell_deps)
74
+ @budget_manager = BudgetManager.new(@shell_deps)
75
+ @stream_manager = StreamManager.new(@shell_deps)
76
+ @continuation_manager = ContinuationManager.new(config: @config)
77
+ @subagent_notice_inbox = SubagentNoticeInbox.new
78
+ @goal_mode = false
79
+ @goal_wait = nil
80
+ end
81
+
82
+ # §7.2 item 3 (ContinuationManager mold): the notice queue lives in
83
+ # SubagentNoticeInbox; Loop keeps the original public names as
84
+ # delegation shims — repl/lifecycle.rb routes by respond_to?(
85
+ # :enqueue_subagent_notice), so that name is load-bearing.
86
+ def enqueue_subagent_notice(notice) = subagent_notice_inbox.enqueue(notice)
87
+
88
+ def drain_subagent_notices_into_history!
89
+ notices = subagent_notice_inbox.drain_all
90
+ return notices if notices.empty?
91
+
92
+ merge_notice_block_into_history!(Rubik::Orchestrator::Subagent::Notices.history_block(notices))
93
+ notices
94
+ end
95
+
96
+ # The merge policy lives in Prompt::ConversationRows (the row-dialect
97
+ # single home, shared with the REPL injector and the compaction layer).
98
+ def merge_notice_block_into_history!(xml_payload)
99
+ Prompt::ConversationRows.append_notice!(@history, xml_payload)
100
+ end
101
+
102
+ def subagent_notice_inbox = (@subagent_notice_inbox ||= SubagentNoticeInbox.new)
103
+
104
+ # Goal-mode flag (run/continue_loop lifecycle above) and the
105
+ # premature-completion guard collaborator. Lazy so Class#allocate
106
+ # test doubles — which skip initialize — answer goal_mode? false and
107
+ # never build the collaborator off a nil @shell_deps.
108
+ def goal_mode? = @goal_mode == true
109
+
110
+ def goal_wait
111
+ @goal_wait ||= GoalWait.new(@shell_deps)
112
+ end
113
+
114
+ def continuation_manager
115
+ @continuation_manager ||= ContinuationManager.new(config: @config)
116
+ end
117
+
118
+ # Agent-turn-executor plan: response processing is shared with the
119
+ # subagent channel through TurnExecutor; Loop supplies the
120
+ # user-channel adapter. Built lazily on first use, NOT in
121
+ # initialize: several test doubles construct loops via
122
+ # Class#allocate + instance_variable_set (build_knob_loop), which
123
+ # skips initialize.
124
+ def turn_executor
125
+ @turn_executor ||= TurnExecutor.new(channel: TurnExecutor::UserChannel.new(self))
126
+ end
127
+
128
+ def extend_budget(extra_steps) = (@max_steps += extra_steps.to_i)
129
+
130
+ def run(task_prompt, model: nil, plan_only: false, goal_mode: false, initial_context: nil, conversation_history: [])
131
+ @step_count = 0
132
+ @history = []
133
+ # Goal-mode marker for the premature-completion guard (GoalWait):
134
+ # set per run so a reused Loop instance cannot inherit the flag;
135
+ # continue_loop deliberately keeps it (same run resumed).
136
+ @goal_mode = goal_mode
137
+ # Fresh inbox per run, aligned with the history reset; continue_loop
138
+ # deliberately keeps the queue (SubagentNoticeInbox contract docs).
139
+ subagent_notice_inbox.reset_for_new_run!
140
+ @total_tokens = { input: 0, output: 0 }
141
+ @invalid_tool_count = 0
142
+ turn_executor.reset!
143
+ @context_warning_emitted = false
144
+ @activity&.reset!
145
+ continuation_manager&.reset!
146
+ @loop_context = @runtime_manager&.context&.extend
147
+
148
+ begin
149
+ target_model = model || @model
150
+ opts = target_model ? { model: target_model } : {}
151
+ opts[:tools] = @tool_registry.schemas unless plan_only
152
+
153
+ mode = if plan_only
154
+ :plan
155
+ else
156
+ goal_mode ? :goal : :default
157
+ end
158
+
159
+ # Prompt view built once at the composition boundary: the
160
+ # orchestrator resolves coeffects (skills index, type registry)
161
+ # and the prompt layer only renders (candidate 2, Phase 2).
162
+ services = Rubik::Prompt::Services.from_shell(@shell,
163
+ skills_index: Rubik::Coeffects.get(@runtime_manager&.context, Rubik::Coeffects::SKILL_MANAGER)&.prompt_index,
164
+ type_registry: resolve_type_registry,
165
+ context_limit: @context_limit,
166
+ max_output_tokens: @max_output_tokens,
167
+ root_dir: @root_dir,
168
+ extra_context: initial_context)
169
+ system_prompt = Rubik::Prompt::ContextBuilder.build_system_prompt(
170
+ mode: mode,
171
+ tool_schemas: plan_only ? [] : @tool_registry.schemas,
172
+ native_tools: !plan_only,
173
+ services: services
174
+ )
175
+
176
+ formatted_task = Rubik::Prompt::ContextBuilder.format_user_task(task_prompt)
177
+
178
+ @history << { role: "system", content: system_prompt }
179
+
180
+ # Inject prior dialogue turns if provided
181
+ if conversation_history && !conversation_history.empty?
182
+ conversation_history.each do |turn|
183
+ r = turn[:role] || turn["role"]
184
+ c = turn[:content] || turn["content"]
185
+ next if r.nil? || c.nil? || c.to_s.strip.empty?
186
+
187
+ @history << { role: r.to_s, content: c.to_s }
188
+ end
189
+ end
190
+
191
+ @history << { role: "user", content: formatted_task }
192
+
193
+ execute_steps(opts: opts, plan_only: plan_only)
194
+ ensure
195
+ @loop_context&.dispose
196
+ end
197
+ end
198
+
199
+ def continue_loop(extra_steps = 20, plan_only: false)
200
+ # Retention seam — see SubagentNoticeInbox#keep_for_continuation.
201
+ subagent_notice_inbox.keep_for_continuation
202
+ extend_budget(extra_steps)
203
+ target_model = @model
204
+ opts = target_model ? { model: target_model } : {}
205
+ opts[:tools] = @tool_registry.schemas
206
+
207
+ # Plan 61 D-14: notifications travel as their own user message —
208
+ # appending to @history.last may target a tool result or a
209
+ # content-less assistant message, which provider formatters then
210
+ # mangle.
211
+ continuation_msg = "<step_budget_extended extra=\"#{extra_steps}\" new_max=\"#{@max_steps}\" " \
212
+ "current=\"#{@step_count}\" " \
213
+ "message=\"User approved extending step budget by #{extra_steps} steps. " \
214
+ "Continue working toward completion.\" />"
215
+ @history << { role: "user", content: continuation_msg }
216
+ execute_steps(opts: opts, plan_only: plan_only)
217
+ end
218
+
219
+ # Shared by both request paths (batch + streaming, Plan 63 Faz A):
220
+ # compact the history in place and emit the :emergency_compact event
221
+ # carrying the layer that produced the final history. Public because
222
+ # StreamManager's compact callable drives it through the ShellDeps
223
+ # façade. The compaction body lives in TurnExecutor#emergency_compact!
224
+ # — one home for both channels; the announce is channel-parameterized.
225
+ def emergency_compact_and_notify!(reason)
226
+ turn_executor.emergency_compact!(reason)
227
+ end
228
+
229
+ def safe_config(key, default)
230
+ return default unless @config.respond_to?(:fetch)
231
+
232
+ @config.fetch(key, default) || default
233
+ rescue KeyError, TypeError, NoMethodError
234
+ default
235
+ end
236
+
237
+ # Turn-result contract (public: the TurnExecutor user-channel adapter
238
+ # builds every terminal through it, and the shell drives
239
+ # continue_loop off the carried loop_instance).
240
+ def turn_result(success:, error: nil, output: nil)
241
+ r = { ok: success, steps: @step_count, tokens: @total_tokens, streamed: @last_turn_streamed == true, loop_instance: self }
242
+ r[:output] = output if output
243
+ r[:error] = error if error
244
+ r
245
+ end
246
+
247
+ private
248
+
249
+ # Registry-key + llm.* override resolution lives in ProviderResolver
250
+ # (review Öneri 3). Loop keeps thin private delegates with the
251
+ # original names/signatures so call sites and send()-based tests are
252
+ # untouched.
253
+ def provider_registry_key
254
+ provider_resolver.registry_key
255
+ end
256
+
257
+ def provider_llm_config(key, default)
258
+ provider_resolver.llm_config(key, default)
259
+ end
260
+
261
+ # Built lazily on first use, NOT in initialize: several test doubles
262
+ # construct loops via Class#allocate + instance_variable_set
263
+ # (build_knob_loop), which skips initialize. Reading @provider/
264
+ # @config at first use keeps that pattern working. Memoization is
265
+ # safe: the provider reference is immutable for the loop's lifetime
266
+ # and the config object is held by reference (values re-read per
267
+ # fetch — config reactivity preserved).
268
+ def provider_resolver
269
+ @provider_resolver ||= ProviderResolver.new(provider: @provider, config: @config)
270
+ end
271
+
272
+ def resolve_type_registry
273
+ if @runtime_manager&.context.respond_to?(:get)
274
+ sup = Rubik::Coeffects.get(@runtime_manager.context, Rubik::Coeffects::SUBAGENT_SUPERVISOR)
275
+ return sup.type_registry if sup&.type_registry
276
+ end
277
+
278
+ Rubik::Orchestrator::Subagent::TypeRegistry.new if @tool_registry&.get("invoke_subagent") && defined?(Rubik::Orchestrator::Subagent::TypeRegistry)
279
+ rescue StandardError => e
280
+ Rubik::Platform.debug_warn("type registry resolution failed (#{e.class}: #{e.message}); subagent typing degraded")
281
+ nil
282
+ end
283
+
284
+ def execute_steps(opts:, plan_only: false)
285
+ @invalid_tool_count = 0
286
+ turn_executor.reset!
287
+ warned = false
288
+
289
+ while @step_count < @max_steps
290
+ # Late-notice asymmetry is intentional: notices arriving after the
291
+ # final step drain nowhere — the user channel (turn-end flush) and
292
+ # manage_subagents wait remain their delivery path.
293
+ drain_subagent_notices_into_history!
294
+ @step_count += 1
295
+
296
+ warned = @budget_manager.evaluate_step_budget_warning!(warned)
297
+ verify_goal_ledger! if goal_mode?
298
+ @budget_manager.evaluate_context_compaction!
299
+ refresh_goal_ledger_row!
300
+
301
+ opts[:tools] = @tool_registry.schemas unless plan_only
302
+
303
+ prov_name = provider_registry_key || "custom"
304
+
305
+ @on_step&.call(
306
+ type: :thinking,
307
+ step: @step_count,
308
+ max_steps: @max_steps,
309
+ model: opts[:model] || @model,
310
+ provider: prov_name
311
+ )
312
+
313
+ res = send_llm_request_with_recovery(opts)
314
+ result = turn_executor.call(res, plan_only: plan_only)
315
+ return result if result
316
+ end
317
+
318
+ { ok: false, error: "Max steps budget exceeded (#{@max_steps})", steps: @step_count, tokens: @total_tokens, loop_instance: self }
319
+ end
320
+
321
+ # Goal-ledger adım-baı yenilemesi (goal-ledger planı A): compaction
322
+ # SONRADA, request ÖNCE — refresh her zaman güncel history referansını
323
+ # görür (BudgetManager history'yi yeni diziye bağlar) ve güncel Σ
324
+ # satırı her adımda garanti edilir. GOAL_LEDGER coeffect'i yoksa
325
+ # (knob OFF ya da !goal dışı) tek satırda no-op — davranış değişmez.
326
+ # Goal-ledger yeniden-doğrulama (plan B): :turn_finished goal
327
+ # çalışırken HİÇ fire etmez (senkron bloklamalı !goal) — turn sınırı
328
+ # execute_steps iterasyonudur, bu yüzden doğrudan hook. Hata
329
+ # izolasyonu: doğrulama asla akışı bozmaz.
330
+ def verify_goal_ledger!
331
+ Rubik::Orchestrator::GoalLedger::Verifier.at_step_boundary!(self)
332
+ rescue StandardError => e
333
+ # Dürüst kanal (Log.warn): debug_warn ENV-gated no-op'tur — doğrulama
334
+ # çöktüğünde drift tespiti sessizce duramaz (sessiz-yutma yasağı).
335
+ Rubik::Log.warn(:orchestrator, "goal ledger verification failed (#{e.class}: #{e.message}); step continues unverified")
336
+ end
337
+
338
+ def refresh_goal_ledger_row!
339
+ ledger = Rubik::Coeffects.get(@runtime_manager&.context, Rubik::Coeffects::GOAL_LEDGER)
340
+ ledger&.refresh_in_history!(@history)
341
+ rescue StandardError => e
342
+ Rubik::Log.warn(:orchestrator, "goal ledger refresh failed (#{e.class}: #{e.message}); step continues")
343
+ end
344
+
345
+ def send_llm_request_with_recovery(opts)
346
+ streaming_enabled = if opts.key?(:stream)
347
+ opts[:stream] == true
348
+ else
349
+ provider_llm_config("stream", true) != false
350
+ end
351
+
352
+ # Plan 61 D-1/D-3: the wire request must share the compaction math's
353
+ # assumptions. Ollama needs context_limit (sent as num_ctx) or its
354
+ # server silently clips the prompt head to the model default; strict
355
+ # providers need max_tokens or output falls back to tiny defaults
356
+ # (e.g. Anthropic's 1024).
357
+ effective_opts = opts.key?(:on_retry) ? opts.dup : opts.merge(on_retry: @stream_manager.llm_retry_notifier)
358
+ effective_opts[:context_limit] = @context_limit if @context_limit
359
+ effective_opts[:max_tokens] = @max_output_tokens if @max_output_tokens&.positive?
360
+ effective_opts[:stream_timeout] ||= provider_llm_config("stream_timeout", ProviderRecovery::STREAM_REQUEST_TIMEOUT)
361
+ effective_opts[:open_timeout] ||= provider_llm_config("open_timeout", ProviderRecovery::DEFAULT_OPEN_TIMEOUT)
362
+ effective_opts[:timeout] ||= effective_opts[:stream_timeout]
363
+ # Plan 90 completion: the llm.thinking_budget_ratio / llm.reasoning_effort
364
+ # knobs reach the adapters only through this bridge. Both default to nil
365
+ # (never sent); the key must stay ABSENT when unset — openai_compatible's
366
+ # reasoning_model? tests opts.key?(:reasoning_effort), so even a nil
367
+ # value would flip every model onto the reasoning payload branch.
368
+ ratio = provider_llm_config("thinking_budget_ratio", nil)
369
+ effective_opts[:thinking_budget_ratio] ||= ratio if ratio
370
+ effort = provider_llm_config("reasoning_effort", nil)
371
+ effective_opts[:reasoning_effort] ||= effort if effort
372
+
373
+ if streaming_enabled && @provider.respond_to?(:stream_chat)
374
+ @stream_manager.execute_streaming_request(effective_opts)
375
+ else
376
+ execute_batch_request(effective_opts)
377
+ end
378
+ end
379
+
380
+ def execute_batch_request(opts)
381
+ @last_turn_streamed = false
382
+ ProviderRecovery.with_recovery(config: @config, compact: method(:emergency_compact_and_notify!)) do
383
+ @provider.complete(@history, opts)
384
+ end
385
+ end
386
+ end
387
+ end
388
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ # Gerçek zamanlı LSP tampon-tanı geri beslemesi (Plan 56 D-5) —
6
+ # StepExecutor'dan dürüst çıkarım (satır bütçesi; storage-show-surface
7
+ # planı A'nın :user_output kancası yerine yer açar). Yazma araçlarının
8
+ # ardından tampon LSP'ye senkronize edilir; severity-1 tanılar varsa
9
+ # sonuç çıktısına düzeltme yönlendirmesi eklenir.
10
+ module LspFeedback
11
+ module_function
12
+
13
+ WRITER_TOOLS = ["write_file", "replace_file_content"].freeze
14
+
15
+ def refresh(deps:, tool_instance:, tool_args:, tool_result:)
16
+ return tool_result unless tool_result.status == :ok
17
+ return tool_result unless WRITER_TOOLS.include?(tool_instance&.name)
18
+ return tool_result unless deps.lsp_manager&.enabled?
19
+
20
+ written_file = tool_args["path"] || tool_args[:path]
21
+ return tool_result unless written_file && deps.buffer_manager
22
+
23
+ content = deps.buffer_manager.read(written_file)
24
+ synced = deps.lsp_manager.sync_buffer_file(written_file, content)
25
+ return tool_result unless synced && deps.lsp_manager.auto_diagnostics?
26
+
27
+ append_diagnostics(deps, written_file, tool_result)
28
+ rescue StandardError => e
29
+ # Loop exposes no logger; the old @loop.logger&.debug raised
30
+ # NoMethodError from inside this rescue itself (fresh-eye
31
+ # review, Plan 87). warn keeps the failure visible on stderr;
32
+ # ANSI stripping keeps LSP-server-controlled messages from
33
+ # injecting terminal escape sequences (red-team E1).
34
+ warn("LSP sync skipped: #{e.message.gsub(/\e\[[0-9;]*[A-Za-z]/, '')}")
35
+ tool_result
36
+ end
37
+
38
+ def append_diagnostics(deps, written_file, tool_result)
39
+ sleep 0.05
40
+ diags = deps.lsp_manager.diagnostics_for(written_file)
41
+ errors = diags.select { |d| d["severity"] == 1 }
42
+ return tool_result if errors.empty?
43
+
44
+ err_lines = errors.map { |e| " • Line #{(e.dig('range', 'start', 'line') || 0) + 1}: #{e['message']}" }.join("\n")
45
+ lsp_msg = "\n\n⚠️ [LSP Real-Time Compiler Diagnostics - #{errors.size} error(s)]:\n#{err_lines}\n" \
46
+ "Please fix these compilation/type errors before proceeding."
47
+ Rubik::Tools::Result.new(
48
+ tool_name: tool_result.tool_name, call_id: tool_result.call_id, status: tool_result.status,
49
+ output: "#{tool_result.output}#{lsp_msg}", duration_ms: tool_result.duration_ms,
50
+ truncated: tool_result.truncated
51
+ )
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../tools/catalog"
4
+ require_relative "tool_history"
5
+
6
+ module Rubik
7
+ module Orchestrator
8
+ # Maestro kapısı (kullanıcı kararı 2026-10-01, opencode-personal
9
+ # bulgusu): maestro HER modda saf orkestratördür — kök ajanın doğrudan
10
+ # yürütme araç çağrıları ÇALIŞTIRILMADAN reddedilir ve subagent'a
11
+ # yönlendirilir. Yalnız StepExecutor (Loop'un kök yolu) bu modülü
12
+ # içerir: subagent'lar StepRunner'ın kendi dispatch'inden gider, buraya
13
+ # asla uğramaz. Config subagents.maestro_gate ile aç/kapa (default
14
+ # açık); kapı canlı okur ama ajan config'i değiştiremez (workspace.rb
15
+ # Paket D: subagents.* operatör-only) — self-unlock yolu kapalı.
16
+ module MaestroGate
17
+ # Kapı sınıfı Tools::Catalog'un maestro_blocked projeksiyonundan türetil
18
+ # (tek kaynak: her aracın DECLARATION'ı); her :write/:execute araç beyanı
19
+ # açıkça vermek zorundadır — yeni bir mutasyon aracı sessizce kapıdan
20
+ # kaçamaz (no-silent-escape invariant'i,
21
+ # test_tool_declaration_catalog.rb). Yalnız YÜRÜTME araçları kapılı:
22
+ # salt-okunur keşif, storage, ask_question ve finish serbest.
23
+ # manage_task action-bazlıdır: start = run_command eşdeğeri arka plan
24
+ # shell'i; list/status/logs/send_input/kill orkestrasyon komşusu olarak
25
+ # serbest kalır.
26
+ def maestro_gate_blocks?(canonical_name, tool_args = {})
27
+ return false unless Rubik::Tools::Catalog.maestro_blocked?(canonical_name)
28
+ return false if canonical_name == "manage_task" &&
29
+ !["start"].include?((tool_args["action"] || tool_args[:action]).to_s.downcase.strip)
30
+
31
+ # Kapıyı çalışma anında config'den oku (!config set ile operatör
32
+ # aç/kapa mümkün). Gerçek Config dotted fetch'i çözer
33
+ # ("subagents.maestro_gate"); düz Hash (test doubles) nested tutar —
34
+ # dig fallback'i iki şekli de kapsar (dotted-key config gotchası).
35
+ cfg = @deps.shell&.config
36
+ return true unless cfg.respond_to?(:fetch)
37
+
38
+ raw = cfg.fetch("subagents.maestro_gate", nil)
39
+ raw = cfg.dig("subagents", "maestro_gate") if raw.nil? && cfg.respond_to?(:dig)
40
+ raw != false
41
+ end
42
+
43
+ # Red = yönlendirme: mesaj ne olduğunu, neden ve NASIL düzgün
44
+ # yapılacağını söyler (empathetic error, AGENTS.md §5). invalid-tool
45
+ # sayacına dokunmaz — bu çağrı geçersiz değil, devredilmiş bir istek.
46
+ def maestro_refusal(canonical_name, tool_name, call_id)
47
+ result = Rubik::Tools::Result.new(
48
+ tool_name: tool_name,
49
+ call_id: call_id,
50
+ status: :error,
51
+ error: "Maestro gate: '#{canonical_name}' is delegated to subagents.",
52
+ output: "Maestro doctrine: the maestro is a pure orchestrator and never executes '#{canonical_name}' itself — " \
53
+ "execution work belongs to subagents in every mode. This call was NOT executed. Dispatch a subagent instead: " \
54
+ "invoke_subagent with type_name 'coder' (file edits, tests, commands, background processes) or 'research' " \
55
+ "(read-only exploration), then collect results via manage_subagents (action: 'wait' or 'transcript') and " \
56
+ "steer with send_message, or hand follow-ups to a finished subagent with manage_subagents (action: 'redeploy').",
57
+ duration_ms: 0.0
58
+ )
59
+ Rubik::Orchestrator::ToolHistory.write(@deps.history, tool_call_id: call_id, tool_name: tool_name,
60
+ status: :error, duration_ms: 0.0, content: result.to_xml)
61
+ @deps.activity.record(canonical_name, {}, :error) if @deps.respond_to?(:activity) && @deps.activity
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ # Deep module (Candidate 03): the ONE home for provider-call recovery
6
+ # policy. Three call sites carried private copies of the same
7
+ # rescue/retry/compact control flow and drifted (a streaming retry that
8
+ # re-used the failed attempt's chunks, a silent subagent compaction,
9
+ # three config-gate spellings). This module owns everything the policy
10
+ # needs and nothing it does not.
11
+ #
12
+ # owns : overflow classification, auto-recover gate, retry-once
13
+ # control flow, compaction-config resolution, request-timeout
14
+ # defaults
15
+ # leaves : the provider call itself (the block); which history is
16
+ # compacted and what observability fires (the compact
17
+ # callable — each site's emergency_compact_and_notify!)
18
+ #
19
+ # Idiom (load-bearing): keep ALL per-attempt state inside the block.
20
+ # `retry` re-executes the begin, which re-yields, which re-runs the
21
+ # block with fresh block-locals — the structural fix for the streaming
22
+ # chunk-pollution bug. Interrupt is not a StandardError and escapes.
23
+ module ProviderRecovery
24
+ # Provider errors matching this are context-window overflows (OpenAI
25
+ # context_length_exceeded, Anthropic "prompt is too long", generic
26
+ # token-limit wording). Collapsed from the three former inline regexes.
27
+ #
28
+ # NOTE: a bare /context/ alternative is deliberately absent. Go-based
29
+ # provider proxies surface transport failures as "context deadline
30
+ # exceeded" / "context canceled" — those must fail (or retry at the
31
+ # transport layer), never trigger the emergency compaction this gate
32
+ # unwinds into, which is destructive and irreversible.
33
+ OVERFLOW_PATTERN = /context_length|context window|maximum context|prompt is too long|token limit|too many tokens/i
34
+
35
+ # Config gate for the automatic compaction+retry (default: on).
36
+ AUTO_RECOVER_KEY = "context.auto_recover_on_overflow"
37
+
38
+ # Config section every compaction call site resolves.
39
+ COMPACTION_CONFIG_KEY = "context"
40
+
41
+ # Named request-timeout defaults. Per-site values preserved on
42
+ # purpose: a subagent step is a short focused call; the shell's
43
+ # streaming turn may legitimately think longer.
44
+ DEFAULT_OPEN_TIMEOUT = 15
45
+ SUBAGENT_REQUEST_TIMEOUT = 90
46
+ STREAM_REQUEST_TIMEOUT = 120
47
+
48
+ module_function
49
+
50
+ # Executes the provider call with context-overflow recovery. On the
51
+ # first overflow-classified StandardError: run the site's compact
52
+ # callable (reason = the provider's error message), then retry the
53
+ # block exactly once. Everything else re-raises untouched, including
54
+ # Interrupt (not a StandardError) and errors raised by compact itself
55
+ # (fail loud — half-recovered state must surface, never be swallowed).
56
+ def with_recovery(config:, compact:)
57
+ retry_attempted = false
58
+ begin
59
+ yield
60
+ rescue StandardError => e
61
+ raise e unless !retry_attempted && auto_recover?(config) && overflow_error?(e)
62
+
63
+ retry_attempted = true
64
+ compact.call(e.message)
65
+ retry
66
+ end
67
+ end
68
+
69
+ # Defensive resolution of the "context" config section shared by every
70
+ # compaction call site: nil config, fetch-less objects, and null YAML
71
+ # sections all collapse to {}. One spelling replaces five.
72
+ def compaction_config(config)
73
+ cfg = config.respond_to?(:fetch) ? config.fetch(COMPACTION_CONFIG_KEY, {}) : {}
74
+ cfg.is_a?(Hash) ? cfg : {}
75
+ end
76
+
77
+ # ── implementation details (not interface) ────────────────────────
78
+
79
+ def auto_recover?(config)
80
+ config.respond_to?(:fetch) ? config.fetch(AUTO_RECOVER_KEY, true) : true
81
+ end
82
+
83
+ def overflow_error?(err)
84
+ err.message.to_s.match?(OVERFLOW_PATTERN)
85
+ end
86
+
87
+ private_class_method :auto_recover?, :overflow_error?
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ # Provider-scoped configuration resolution for the orchestrator loop
6
+ # (review Öneri 3; extracted from Loop's provider-config section).
7
+ #
8
+ # Owns two lookups:
9
+ # - registry_key: registry-style key of the active provider — the
10
+ # explicit provider name (downcased, the way the registry stores
11
+ # them) when it carries one, else the class name derived the way
12
+ # builtin providers are registered (Rubik::LLM::OllamaProvider ->
13
+ # "ollama"). An empty name counts as unnamed, so instances built
14
+ # outside the registry keep falling back.
15
+ # - llm_config: per-provider llm.* override — providers.<key>.<field>
16
+ # beats the global llm.<field>, so a provider-specific quirk (e.g. a
17
+ # broken SSE stack) can be tuned without flipping the knob for every
18
+ # provider. A scoped nil counts as unset — also the only way to
19
+ # clear an override back to the global value.
20
+ #
21
+ # The config object is held by reference and values are re-read on
22
+ # every fetch, so !config set / !provider set still apply from the
23
+ # very next turn (config reactivity, Plan 75).
24
+ class ProviderResolver
25
+ def initialize(provider:, config:)
26
+ @provider = provider
27
+ @config = config
28
+ end
29
+
30
+ # Registry-style key for the active provider: the explicit provider name
31
+ # when it carries one (downcased, the way the registry stores them),
32
+ # else the class name derived the way builtin providers are registered
33
+ # (Rubik::LLM::OllamaProvider -> "ollama"). An empty name counts as
34
+ # unnamed, so instances built outside the registry keep falling back.
35
+ def registry_key
36
+ explicit = @provider.respond_to?(:name) ? @provider.name.to_s.downcase : ""
37
+ return explicit unless explicit.empty?
38
+
39
+ klass_name = @provider.class.name
40
+ base = klass_name&.split("::")&.last
41
+ base&.delete_suffix("Provider")&.downcase
42
+ end
43
+
44
+ # Per-provider llm.* override: providers.<key>.<field> beats the global
45
+ # llm.<field>, so a provider-specific quirk (e.g. a broken SSE stack)
46
+ # can be tuned without flipping the knob for every provider. Re-read on
47
+ # each request: !config set / !provider set apply from the very next
48
+ # turn. A scoped nil counts as unset — also the only way to clear an
49
+ # override back to the global value.
50
+ def llm_config(key, default)
51
+ prov_key = registry_key
52
+ if prov_key.is_a?(String) && !prov_key.empty? && @config.respond_to?(:fetch)
53
+ val = @config.fetch("providers.#{prov_key}.#{key}", nil)
54
+ return val unless val.nil?
55
+ end
56
+
57
+ # Two-arg fetch, not safe_config: safe_config's `fetch(...) || default`
58
+ # would coerce an explicit false (llm.stream=false) into the truthy
59
+ # default, and a one-arg fetch on a plain-Hash config (tests) raises
60
+ # KeyError instead of falling through.
61
+ return default unless @config.respond_to?(:fetch)
62
+
63
+ @config.fetch("llm.#{key}", default)
64
+ rescue StandardError
65
+ default
66
+ end
67
+ end
68
+ end
69
+ end