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,376 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../platform"
5
+ require_relative "tool_call_id"
6
+ require_relative "tool_history"
7
+ require_relative "provider_recovery"
8
+
9
+ module Rubik
10
+ module Orchestrator
11
+ # THE shared response-processing spine for both agent channels (the
12
+ # agent-turn-executor plan): Loop (user channel) and Subagent::StepRunner
13
+ # (subagent channel) become thin parameterizations of this executor —
14
+ # neither carries its own copy of "one provider response → history
15
+ # mutations → (nudge | final text | tool dispatch) → turn decision".
16
+ #
17
+ # Stateless-per-call: one instance per channel, built lazily by its
18
+ # owner (the provider_resolver precedent keeps Class#allocate-built
19
+ # test doubles working). Channel policy lives in the adapters (Channel
20
+ # base + UserChannel + SubagentChannel); request assembly stays
21
+ # channel-native on purpose — stream vs batch and their recovery stacks
22
+ # genuinely differ, so the channel hands the executor the raw response
23
+ # and ProviderRecovery keeps wrapping the provider CALL at its three
24
+ # sites. The compaction actuator it drives (emergency_compact!) has
25
+ # exactly one home: here.
26
+ class TurnExecutor
27
+ class << self
28
+ # JSON-text tool-call fallback (Plan 61): a non-tool-native model's
29
+ # JSON reply parses to one tool call. Degenerate model text — a
30
+ # bare "```json" fence, an empty fence, or a valid-JSON non-object
31
+ # — degrades to nil (no tool call; the text is treated as output)
32
+ # instead of raising into the step loop: fallback text is
33
+ # model-controlled, so the same bounded policy as parse_json_args
34
+ # applies.
35
+ def parse_tool_call(text)
36
+ cleaned = text.to_s.gsub("\\n", "\n")
37
+ json_str = if cleaned.include?("```json")
38
+ cleaned.split("```json").last.to_s.split("```").first.to_s.strip
39
+ elsif cleaned.start_with?("{") && cleaned.end_with?("}")
40
+ cleaned
41
+ end
42
+ return nil if json_str.nil? || json_str.empty?
43
+
44
+ parsed = JSON.parse(json_str)
45
+ parsed.is_a?(Hash) ? parsed : nil
46
+ rescue JSON::ParserError
47
+ nil
48
+ end
49
+
50
+ def extract_finish_reason(res)
51
+ return nil unless res.respond_to?(:raw) && res.raw.is_a?(Hash)
52
+
53
+ raw = res.raw
54
+ raw["finish_reason"] || raw.dig("choices", 0, "finish_reason") ||
55
+ raw["stop_reason"] || raw.dig("delta", "stop_reason") || raw["done_reason"]
56
+ end
57
+
58
+ # Registry-canonical finish detection: a call is the finish call when
59
+ # the registry resolves it to the finish tool — an unregistered case
60
+ # variant ("Finish") downcases to finish instead of dispatching an
61
+ # unknown tool. A nil registry degrades to the downcase compare.
62
+ def finish_call?(tool_registry, tool_call)
63
+ t_name = tool_call["tool"] || tool_call[:tool]
64
+ resolved = tool_registry.respond_to?(:get) ? tool_registry.get(t_name) : nil
65
+ (resolved ? resolved.name : t_name.to_s.downcase) == "finish"
66
+ end
67
+ end
68
+
69
+ def initialize(channel:)
70
+ @channel = channel
71
+ end
72
+
73
+ attr_reader :channel
74
+
75
+ # Channel-owned counter (Loop's public empty_response_count reader
76
+ # delegates here).
77
+ def nudge_policy = channel.nudge_policy
78
+
79
+ # Per-run reset of channel-owned counters (empty-response strikes).
80
+ def reset!
81
+ channel.nudge_policy.reset!
82
+ end
83
+
84
+ # THE seam. Processes one provider response: normalize tool calls
85
+ # (native + JSON fallback, channel-minted ids), truncation hook,
86
+ # plan-mode branch, empty-nudge, assistant row, finish detection,
87
+ # tool-dispatch loop.
88
+ #
89
+ # Returns nil to continue the step loop, or the terminal Hash (the
90
+ # channel's own result contract) to stop it.
91
+ def call(res, plan_only: false)
92
+ channel.track_usage(res)
93
+
94
+ # safe_utf8 before strip: degraded providers can echo invalid byte
95
+ # sequences (inside JSON string values JSON.parse would even accept
96
+ # them) — strip regexes the text and would kill the turn on the
97
+ # spot. Model-controlled text is sanitized at this one boundary.
98
+ text_resp = Platform.safe_utf8(channel.response_text(res)).to_s.strip
99
+ tool_calls = normalize_tool_calls(res, text_resp)
100
+
101
+ handled = channel.handle_truncation(res, text_resp, tool_calls, self.class.extract_finish_reason(res).to_s)
102
+ return nil if handled == :consumed
103
+ return handled if handled.is_a?(Hash)
104
+
105
+ return plan_turn(res, text_resp) if plan_only
106
+ return text_turn(res, text_resp) if tool_calls.empty?
107
+
108
+ tool_turn(res, text_resp, tool_calls)
109
+ end
110
+
111
+ # Emergency compaction, ONE home for both channels: the compaction
112
+ # math stays in Prompt::EmergencyCompactor.compact!, the announce is
113
+ # channel-parameterized. Returns the layer that produced the final
114
+ # history (the Loop seam's documented contract).
115
+ def emergency_compact!(reason)
116
+ limits = channel.limits
117
+ layer = Rubik::Prompt::EmergencyCompactor.compact!(
118
+ channel.history, context_limit: limits[:context_limit], max_output_tokens: limits[:max_output_tokens],
119
+ config: ProviderRecovery.compaction_config(limits[:config]),
120
+ absorb: channel.absorb_directive
121
+ )
122
+ channel.announce_compaction(reason, layer)
123
+ layer
124
+ end
125
+
126
+ # Capped at the same 200 chars the streaming diagnostics path
127
+ # (:malformed_tool_args) shows the UI.
128
+ MALFORMED_ARGS_FRAGMENT_CAP = 200
129
+
130
+ private
131
+
132
+ # One shared normalizer for both channels (wire-identical: every
133
+ # provider path already emits string-keyed {"id","tool","args"} —
134
+ # ChunkAssembler and the three batch providers). Builds fresh
135
+ # normalized hashes with a channel-minted id per call; the JSON-text
136
+ # fallback (Plan 61) parses a non-native model's JSON reply into one
137
+ # call.
138
+ def normalize_tool_calls(res, text_resp)
139
+ raw_calls = channel.raw_tool_calls(res).to_a
140
+ if raw_calls.empty?
141
+ parsed = self.class.parse_tool_call(text_resp)
142
+ raw_calls = parsed ? [parsed] : []
143
+ end
144
+
145
+ raw_calls.map.with_index do |tc, idx|
146
+ id = tc["id"] || tc[:id] || channel.mint_call_id(idx)
147
+ name = tc["tool"] || tc[:tool] || tc["name"] || tc[:name]
148
+ normalize_one_tool_call(tc, id, name)
149
+ end
150
+ end
151
+
152
+ # Undecodable args (unparsable JSON string, valid non-object, native
153
+ # non-Hash payload) are NOT dispatched as {}: the row carries
154
+ # "malformed_args" and tool_turn writes a canonical error row quoting
155
+ # the rejected payload (issue #55 3.4) instead of producing a
156
+ # downstream missing-parameter or guard error the model cannot
157
+ # connect to the real cause. The marker may also arrive pre-attached
158
+ # from a provider-layer parse (chunk_assembler, openai_compatible,
159
+ # ollama degrade sites). Absent/nil args stay a legal {} dispatch
160
+ # (tools without parameters).
161
+ def normalize_one_tool_call(call, id, name)
162
+ raw_args = call["args"] || call[:args] || call["arguments"] || call[:arguments]
163
+ provider_fragment = call["malformed_args"] || call[:malformed_args]
164
+ row = { "id" => id, "tool" => name }
165
+ case raw_args
166
+ when String
167
+ parsed = parse_json_args(raw_args)
168
+ if parsed.is_a?(Hash)
169
+ row["args"] = parsed
170
+ else
171
+ row["args"] = {}
172
+ row["malformed_args"] = malformed_args_fragment(raw_args)
173
+ end
174
+ when Hash
175
+ row["args"] = raw_args
176
+ when nil
177
+ row["args"] = {}
178
+ else
179
+ row["args"] = {}
180
+ row["malformed_args"] = malformed_args_fragment(raw_args.to_s)
181
+ end
182
+ row["malformed_args"] = malformed_args_fragment(provider_fragment) if provider_fragment && !row["malformed_args"]
183
+ row
184
+ end
185
+
186
+ def malformed_args_fragment(raw)
187
+ # safe_utf8 first: raw model-controlled args can carry invalid byte
188
+ # sequences (JSON.parse tolerates them inside string values), and
189
+ # gsub with a regex source raises ArgumentError on those — the one
190
+ # raise this bounded-error path must never produce.
191
+ fragment = Platform.safe_utf8(raw).to_s.gsub(/\s+/, " ").strip
192
+ fragment.length > MALFORMED_ARGS_FRAGMENT_CAP ? "#{fragment[0, MALFORMED_ARGS_FRAGMENT_CAP]}..." : fragment
193
+ end
194
+
195
+ def parse_json_args(raw)
196
+ JSON.parse(raw)
197
+ rescue JSON::ParserError
198
+ nil
199
+ end
200
+
201
+ def plan_turn(res, text_resp)
202
+ return channel.empty_response_turn(res, channel.plan_nudge_message) if text_resp.empty?
203
+
204
+ final_text_turn(res, text_resp)
205
+ end
206
+
207
+ def text_turn(res, text_resp)
208
+ return channel.empty_response_turn(res) if text_resp.empty?
209
+
210
+ final_text_turn(res, text_resp)
211
+ end
212
+
213
+ def final_text_turn(res, text_resp)
214
+ channel.nudge_policy.reset!
215
+ channel.final_text_result(channel.final_text(res, text_resp))
216
+ end
217
+
218
+ def tool_turn(res, text_resp, tool_calls)
219
+ channel.nudge_policy.reset!
220
+ channel.append_tool_call_row(channel.final_text(res, text_resp), tool_calls)
221
+ channel.announce_llm_thought(text_resp) if !text_resp.empty? && tool_calls.none? { |tc| self.class.finish_call?(channel.tool_registry, tc) }
222
+
223
+ tool_calls.each do |tc|
224
+ # finish is exempt (pinned by test_orchestrator_stream): its args
225
+ # are optional and the fallback text carries the message, so a
226
+ # malformed finish must still complete the turn gracefully instead
227
+ # of error-rowing the completion signal into a retry cycle.
228
+ if tc["malformed_args"] && !self.class.finish_call?(channel.tool_registry, tc)
229
+ channel.malformed_args_row(tc)
230
+ next
231
+ end
232
+ result = channel.execute_tool_call(tc, text_resp)
233
+ return result if result
234
+ end
235
+ nil
236
+ end
237
+
238
+ # The per-channel policy adapter. Base class carries the shared
239
+ # mechanics with no-op defaults where a channel legitimately has
240
+ # nothing to do; channel-specific terminals raise
241
+ # NotImplementedError — a missing terminal is a construction bug,
242
+ # not a runtime condition (fail loud at the boundary).
243
+ class Channel
244
+ # ── nudge policy (channel-owned so the owner's accessor delegates) ──
245
+ def nudge_policy
246
+ @nudge_policy ||= NudgePolicy.new
247
+ end
248
+
249
+ # Default empty-response policy: the shared two-strike nudge. One
250
+ # nudge row per empty, tool-less response; terminal failure on the
251
+ # second consecutive strike.
252
+ def empty_response_turn(res, nudge_message = empty_nudge_message(res))
253
+ count = nudge_policy.register_empty!
254
+ return empty_response_failure if nudge_policy.strike?(count)
255
+
256
+ history << { role: "user", content: nudge_message }
257
+ nil
258
+ end
259
+
260
+ # ── required state ──
261
+ def history = raise NotImplementedError, "channel must expose the mutable conversation history"
262
+
263
+ def tool_registry = nil
264
+
265
+ def limits
266
+ raise NotImplementedError, "channel must expose { context_limit:, max_output_tokens:, config: }"
267
+ end
268
+
269
+ def mint_call_id(_idx)
270
+ raise NotImplementedError,
271
+ "channel must mint ToolCallId ids (unique vs deterministic is per-channel doctrine)"
272
+ end
273
+
274
+ # ── response shaping ──
275
+ # The channel's native tool-call source from a raw provider
276
+ # response. Main: the Response#tool_calls? predicate gates access;
277
+ # the subagent's Struct/duck mock providers expose the array
278
+ # attribute directly, so that channel overrides this hook.
279
+ def raw_tool_calls(res)
280
+ res.respond_to?(:tool_calls?) && res.tool_calls? ? res.tool_calls : []
281
+ end
282
+
283
+ def response_text(res) = res.text.to_s
284
+
285
+ def final_text(_res, text) = text
286
+
287
+ def track_usage(_res) = nil
288
+
289
+ # -> nil (not applicable) | :consumed (rows+nudge appended; executor
290
+ # continues) | Hash (terminal error).
291
+ def handle_truncation(_res, _text, _tool_calls, _finish_reason) = nil
292
+
293
+ # ── nudge texts ──
294
+ def empty_nudge_message(_res) = "Your last response was empty. Continue the task or call the finish tool."
295
+
296
+ def plan_nudge_message
297
+ "Plan mode: tool calls are disabled. Respond with the numbered implementation plan as plain text."
298
+ end
299
+
300
+ # ── announce ──
301
+ def announce_compaction(_reason, _layer) = nil
302
+
303
+ def announce_llm_thought(_text) = nil
304
+
305
+ # Goal-ledger observation projection (plan D): related-optional
306
+ # capability — base channel is structurally inert (subagent planes
307
+ # never absorb); UserChannel overrides.
308
+ def absorb_directive = nil
309
+
310
+ # ── terminals & dispatch ──
311
+ def empty_response_failure
312
+ raise NotImplementedError, "channel must build its terminal empty-response failure Hash"
313
+ end
314
+
315
+ def final_text_result(_final_text)
316
+ raise NotImplementedError, "channel must build its text-completion terminal Hash"
317
+ end
318
+
319
+ def append_tool_call_row(_final_text, _tool_calls)
320
+ raise NotImplementedError, "channel must append the assistant tool-call row"
321
+ end
322
+
323
+ def on_finish(_args, _fallback_text)
324
+ raise NotImplementedError, "channel must implement its finish semantics"
325
+ end
326
+
327
+ def execute_tool_call(_tool_call, _text)
328
+ raise NotImplementedError, "channel must dispatch tool calls"
329
+ end
330
+
331
+ # Canonical error row for a tool call whose String args failed to
332
+ # parse (issue #55 3.4): the model sees its own rejected payload,
333
+ # the tool_call_id stays paired (strict providers 400 on orphaned
334
+ # ids), and the tool never dispatches with degraded {} args.
335
+ def malformed_args_row(tool_call)
336
+ ToolHistory.write(history,
337
+ tool_call_id: tool_call["id"],
338
+ tool_name: tool_call["tool"].to_s,
339
+ status: :error,
340
+ duration_ms: 0.0,
341
+ content: "Malformed tool call: arguments were not parsable as a JSON object, so the " \
342
+ "tool was NOT executed. Rejected payload (whitespace-collapsed, first " \
343
+ "#{MALFORMED_ARGS_FRAGMENT_CAP} chars): #{tool_call['malformed_args']}. " \
344
+ "Re-issue the call with complete, valid JSON arguments.")
345
+ nil
346
+ end
347
+ end
348
+
349
+ # Two-strike empty-response counter, channel-owned so the owner
350
+ # object (Loop) can delegate its public reader.
351
+ class NudgePolicy
352
+ DEFAULT_STRIKE_LIMIT = 2
353
+
354
+ attr_reader :strike_limit, :count
355
+
356
+ def initialize(strike_limit = DEFAULT_STRIKE_LIMIT)
357
+ @strike_limit = strike_limit
358
+ @count = 0
359
+ end
360
+
361
+ # Registers one empty response and returns the running count.
362
+ def register_empty!
363
+ @count += 1
364
+ end
365
+
366
+ def reset!
367
+ @count = 0
368
+ end
369
+
370
+ def strike?(count = @count)
371
+ count >= @strike_limit
372
+ end
373
+ end
374
+ end
375
+ end
376
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "orchestrator/loop"
4
+ require_relative "orchestrator/goal_state"
5
+ require_relative "orchestrator/goal_ledger"
6
+ require_relative "orchestrator/subagent/supervisor"
7
+
8
+ module Rubik
9
+ module Orchestrator
10
+ end
11
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Platform
5
+ # File system operations (atomic writes, newline normalization).
6
+ # Extracted from lib/rubik/platform.rb (Plan 86 D-4).
7
+ module FileSystem
8
+ def self.included(base)
9
+ base.extend self
10
+ end
11
+
12
+ def atomic_write(target_path, content)
13
+ dir = File.dirname(target_path)
14
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
15
+ tmp_file = "#{target_path}.tmp.#{::Process.pid}.#{rand(1_000_000)}"
16
+
17
+ File.open(tmp_file, "wb") do |f|
18
+ f.write(content)
19
+ f.flush
20
+ begin
21
+ f.fsync
22
+ rescue StandardError, ScriptError
23
+ nil
24
+ end
25
+ end
26
+
27
+ # Atomic replace: rename tmp_file -> target_path without unlinking first
28
+ # (closing TOCTOU). Retry for Windows transient locks.
29
+ retries = 3
30
+ begin
31
+ File.rename(tmp_file, target_path)
32
+ rescue SystemCallError => e
33
+ if retries.positive?
34
+ retries -= 1
35
+ sleep 0.02
36
+ retry
37
+ end
38
+ debug_warn("atomic_write fallback for #{target_path}: #{e.class}: #{e.message}")
39
+ File.binwrite(target_path, content)
40
+ ensure
41
+ begin
42
+ FileUtils.rm_f(tmp_file)
43
+ rescue StandardError, ScriptError
44
+ nil
45
+ end
46
+ end
47
+ end
48
+
49
+ def normalize_newlines(content)
50
+ content.to_s.gsub("\r\n", "\n")
51
+ end
52
+
53
+ # UTF-8 read path for project files: tags the bytes as UTF-8 regardless
54
+ # of the OS default external encoding (Windows codepages must not leak
55
+ # into Rubik's pipeline) and scrubs invalid sequences so the model,
56
+ # history, and regex consumers never receive broken encoding. All
57
+ # buffer hash computations share this path, so a broken-encoding file
58
+ # shifts its digest once and then stays stable.
59
+ def read_text(path)
60
+ Rubik::Platform.safe_utf8(File.read(path, encoding: "UTF-8"))
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Platform
5
+ # Continuously drains a child-process IO (stderr) so a chatty child
6
+ # never fills the OS pipe buffer (~64KB) and deadlocks writing
7
+ # diagnostics while the parent waits on stdout. Discards content; the
8
+ # optional on_line observer is a future debug-forwarding hook.
9
+ # Lifecycle contract mirrors the existing reader threads (MCP stdio,
10
+ # LSP stdout): kill + bounded join, never raises from #stop.
11
+ class IoDrain
12
+ JOIN_TIMEOUT = 0.5
13
+
14
+ def self.start(io, on_line: nil)
15
+ new(io, on_line: on_line).tap(&:start)
16
+ end
17
+
18
+ def initialize(io, on_line: nil)
19
+ @io = io
20
+ @on_line = on_line
21
+ @thread = nil
22
+ @stopped = false
23
+ end
24
+
25
+ def start
26
+ @stopped = false
27
+ @thread = Thread.new do
28
+ while (line = @io.gets)
29
+ @on_line&.call(line)
30
+ end
31
+ rescue IOError, Errno::EBADF, Errno::EPIPE => e
32
+ # Expected while #stop closes the pipes underneath us. Any other
33
+ # time it is the watchdog dying while the child may still flood
34
+ # the pipe — the exact deadlock this class exists to prevent —
35
+ # so it must leave a trace.
36
+ warn "[rubik] stderr drain died unexpectedly (#{e.class}: #{e.message})" unless @stopped
37
+ end
38
+ self
39
+ end
40
+
41
+ def stop
42
+ @stopped = true
43
+ thread = @thread
44
+ @thread = nil
45
+ return unless thread
46
+
47
+ thread.kill
48
+ thread.join(JOIN_TIMEOUT)
49
+ nil
50
+ end
51
+ end
52
+ end
53
+ end