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,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ # The Loop-side mailbox for subagent notices (fresh-scan card 4;
6
+ # ContinuationManager mold: plain constructor, own state, eager init in
7
+ # Loop#initialize plus a lazy reader for Loop.allocate test doubles).
8
+ #
9
+ # Boundary: QUEUE-ONLY. This class owns the thread-safe queue, the
10
+ # notice-shape validation at entry, and the run/continuation lifecycle
11
+ # of that queue. It deliberately does NOT render notices
12
+ # (Subagent::Notices owns the vocabulary) and does NOT merge into
13
+ # history (Loop owns @history): drain_all hands back validated notice
14
+ # hashes and the caller renders.
15
+ #
16
+ # Threading contract (inherited verbatim from the pre-extraction Loop
17
+ # code; see also repl/lifecycle.rb handle_subagent_event):
18
+ # - enqueue runs on subagent worker threads (the bus emit is
19
+ # synchronous): validation plus a bare Queue#push, never a lock a
20
+ # REPL thread might hold while routing an event.
21
+ # - drain_all / reset_for_new_run! run on the turn thread between
22
+ # steps and may race a concurrent enqueue safely: pop(true) raises
23
+ # ThreadError on empty, the race-safe terminator for the
24
+ # check-then-act window a conditional loop would miss.
25
+ # - reset_for_new_run! REASSIGNS the queue (faithful to Loop's old
26
+ # `@subagent_notices = Queue.new`), it does not clear it. A notice
27
+ # pushed to the old Queue object in the microscopic window between
28
+ # the previous run's last drain and the reassign is stranded on the
29
+ # dead queue: accepted by design — the user channel already rendered
30
+ # its line at enqueue time, and manage_subagents wait remains the
31
+ # model's recovery path.
32
+ class SubagentNoticeInbox
33
+ def initialize
34
+ @queue = Queue.new
35
+ end
36
+
37
+ # Entry point and the ONLY mutation path: validate, then push. The
38
+ # REPL routing seam reaches this via Loop#enqueue_subagent_notice
39
+ # with Notices.normalize output; this raise is the safety net against
40
+ # the legacy string dialect. Message pinned verbatim by tests.
41
+ def enqueue(notice)
42
+ raise ArgumentError, "subagent notice must be a Hash with :kind, got #{notice.inspect}" unless notice.is_a?(Hash) && notice[:kind]
43
+
44
+ @queue.push(notice)
45
+ end
46
+
47
+ # Pops everything pending (FIFO) and returns the notice hashes.
48
+ # Empty-after-drain by construction; a second call returns [].
49
+ def drain_all
50
+ notices = []
51
+ loop { notices << @queue.pop(true) }
52
+ rescue ThreadError
53
+ notices
54
+ end
55
+
56
+ # Non-blocking emptiness probe (GoalWait's wake check). Advisory
57
+ # only: an enqueue racing this check is caught on the next poll or
58
+ # drain — Queue#pop remains the authoritative consumer.
59
+ def empty?
60
+ @queue.empty?
61
+ end
62
+
63
+ # Per-run lifecycle (Loop#run, aligned with the @history reset):
64
+ # stale notices from a previous run on a cached Loop instance must
65
+ # never drain into the fresh history. Reassigns — see the class
66
+ # comment for the stranding window this preserves on purpose.
67
+ def reset_for_new_run!
68
+ @queue = Queue.new
69
+ end
70
+
71
+ # Continuation lifecycle (Loop#continue_loop): an explicit,
72
+ # contract-carrying no-op. Continuation resumes the same run, so
73
+ # undelivered notices must survive into the next execute_steps pass.
74
+ # Retention is the decision — encoding it as a named call at the
75
+ # continuation seam keeps the run/continue asymmetry greppable in
76
+ # code instead of living only in a comment inside #run.
77
+ def keep_for_continuation; end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ # THE single vocabulary home for the orchestrator family's tool-call
6
+ # id spellings (previously three independent interpolations: Loop's
7
+ # response normalizer, Subagent::StepRunner, StepExecutor's defensive
8
+ # fallback). One prefix and joining convention; TWO deliberate
9
+ # uniqueness policies — the sites' collision realities differ:
10
+ #
11
+ # - Loop (unique: true): production builds a fresh Loop per turn while
12
+ # the session history persists, so step_count restarts every turn
13
+ # and a deterministic "call_3_1" WILL recur inside one history —
14
+ # duplicate tool_use ids break strict providers' tool_result
15
+ # pairing. The 4-digit random suffix is load-bearing, not decor.
16
+ # - Subagent StepRunner (deterministic): a subagent's history lives
17
+ # and dies with its own run — never restored, never continued — so
18
+ # (subagent id, step, index) is unique by construction, and
19
+ # determinism keeps ids greppable across subagents in transcripts.
20
+ # - StepExecutor (unique: true): dead on the production path (the
21
+ # TurnExecutor normalizes ids before dispatch) but live on the
22
+ # executor's own seam — test doubles call it with bare, id-less tool
23
+ # calls, and no turn-local index exists there.
24
+ #
25
+ # mint is spelling-only by design: the id PRECEDENCE chain
26
+ # (tc["id"] || tc[:id] || mint) lives in one place now — the shared
27
+ # TurnExecutor normalizer — while the mint policy per channel is
28
+ # supplied through Channel#mint_call_id. Both channels receive fresh
29
+ # normalized {"id","tool","args"} hashes; the llm/ layer's dumb
30
+ # call_#{rand} fallbacks stay there: llm must never require
31
+ # orchestrator (layering).
32
+ module ToolCallId
33
+ module_function
34
+
35
+ # mint(3, 2) -> "call_3_2"
36
+ # mint("subagent-2", 5, 3) -> "call_subagent-2_5_3"
37
+ # mint(4, unique: true) -> "call_4_4821"
38
+ # mint(3, 2, unique: true) -> "call_3_2_4821"
39
+ # Byte-compatible with every pre-extraction spelling.
40
+ def mint(*parts, unique: false)
41
+ id = "call_#{parts.join('_')}"
42
+ id = "#{id}_#{rand(1000..9999)}" if unique
43
+ id
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ # Opt-in failure isolation for tool execution (G19): after `threshold`
6
+ # consecutive failures of the same tool, short-circuit further calls for a
7
+ # cooldown instead of burning agent steps on a wedged backend. Disabled by
8
+ # default — synchronous execution is the agent loop's design; this only
9
+ # stops repeated futile calls once the operator turns it on.
10
+ module Rubik
11
+ module Orchestrator
12
+ class ToolCircuitBreaker
13
+ # Raised by the StepExecutor when a call is refused; the executor's
14
+ # normal rescue converts it into a failed tool result so the loop
15
+ # plumbing (events, history) is reused untouched.
16
+ class CircuitOpenError < StandardError; end
17
+
18
+ STATES = [:closed, :open, :half_open].freeze
19
+
20
+ def initialize(threshold: 5, cooldown_seconds: 60.0)
21
+ @threshold = threshold
22
+ @cooldown_seconds = cooldown_seconds.to_f
23
+ @monitor = Monitor.new
24
+ @tools = {} # name -> { failures:, opened_at:, probing: }
25
+ end
26
+
27
+ def state(tool_name)
28
+ @monitor.synchronize { circuit(tool_name)[:state] }
29
+ end
30
+
31
+ def allow?(tool_name)
32
+ @monitor.synchronize do
33
+ circuit = circuit(tool_name)
34
+ case circuit[:state]
35
+ when :open
36
+ return false unless cooldown_elapsed?(circuit)
37
+
38
+ # one probe through the half-open gate
39
+ circuit[:state] = :half_open
40
+ circuit[:probing] = true
41
+ true
42
+ when :half_open
43
+ if circuit[:probing]
44
+ true
45
+ else
46
+ cooldown_elapsed?
47
+ end
48
+ else
49
+ true
50
+ end
51
+ end
52
+ end
53
+
54
+ def record_success(tool_name)
55
+ @monitor.synchronize do
56
+ @tools.delete(tool_name)
57
+ end
58
+ end
59
+
60
+ def record_failure(tool_name)
61
+ @monitor.synchronize do
62
+ circuit = circuit(tool_name)
63
+ if circuit[:state] == :half_open
64
+ # probe failed: reopen for another full cooldown
65
+ circuit[:opened_at] = now
66
+ circuit[:probing] = false
67
+ circuit[:state] = :open
68
+ else
69
+ circuit[:failures] += 1
70
+ circuit[:state] = :open if circuit[:failures] >= @threshold
71
+ circuit[:opened_at] = now if circuit[:state] == :open && circuit[:opened_at].nil?
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def circuit(tool_name)
79
+ @tools[tool_name] ||= { failures: 0, opened_at: nil, probing: false, state: :closed }
80
+ end
81
+
82
+ def cooldown_elapsed?(circuit)
83
+ circuit[:opened_at] && (now - circuit[:opened_at]) >= @cooldown_seconds
84
+ end
85
+
86
+ def now
87
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Orchestrator
5
+ # THE canonical tool-result history row — the dictionary single source
6
+ # of truth (agent-turn-executor plan, Phase 1). Every channel's writer
7
+ # (StepExecutor registered/invalid, MaestroGate refusal, subagent
8
+ # dispatch) constructs its row through ToolHistory.write, so key drift
9
+ # between channels is structurally impossible.
10
+ #
11
+ # Provider-consumption evidence (formatter.rb et al.):
12
+ # - tool_call_id — required by OpenAI native function calling
13
+ # (formatter.rb, extract_call_id) and Anthropic tool_use pairing;
14
+ # also read by TokenEstimator.
15
+ # - tool_name — OpenAI non-native and Ollama prefixed-user-message
16
+ # paths, TokenEstimator, ContextCompactor skill-tool filter,
17
+ # TranscriptFormatter.
18
+ # - status / duration_ms — read by NO wire-side consumer; observability
19
+ # only (tests, REPL events, stored transcripts). Formatters build
20
+ # fresh hashes and ignore unknown keys, so carrying them everywhere is
21
+ # wire-safe for every provider path.
22
+ # - content — an opaque String per channel (main path: Result#to_xml;
23
+ # subagent: truncated plain output). The dictionary unifies the row
24
+ # SHAPE, not the content encoding.
25
+ module ToolHistory
26
+ ROW_KEYS = [:role, :tool_call_id, :tool_name, :status, :duration_ms, :content].freeze
27
+
28
+ module_function
29
+
30
+ # Appends exactly one canonical row to +history+ and returns it.
31
+ def write(history, tool_call_id:, tool_name:, status:, duration_ms:, content:)
32
+ row = { role: "tool", tool_call_id: tool_call_id, tool_name: tool_name,
33
+ status: status, duration_ms: duration_ms, content: content }
34
+ history << row
35
+ row
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../tool_call_id"
4
+
5
+ module Rubik
6
+ module Orchestrator
7
+ class TurnExecutor
8
+ # The subagent-channel adapter: wraps the Subagent::Instance. Its
9
+ # methods run on the worker thread only, between monitor sections —
10
+ # the channel never holds @monitor and never emits on the bus under
11
+ # it (the same discipline as the StepRunner methods it replaces).
12
+ class SubagentChannel < Channel
13
+ def initialize(instance)
14
+ super()
15
+ @inst = instance
16
+ end
17
+
18
+ attr_reader :inst
19
+
20
+ # ── required state ──
21
+ def history = @inst.history
22
+
23
+ def tool_registry = @inst.tool_registry
24
+
25
+ def limits
26
+ { context_limit: @inst.context_limit, max_output_tokens: @inst.max_output_tokens, config: @inst.config }
27
+ end
28
+
29
+ # Subagent policy (ToolCallId doctrine): deterministic
30
+ # (subagent id, step, index) — the history lives and dies with the
31
+ # run, and greppable ids beat collision-proof ones here.
32
+ def mint_call_id(idx)
33
+ ToolCallId.mint(@inst.id, @inst.current_step, idx + 1)
34
+ end
35
+
36
+ # ── response shaping ──
37
+ # Moved verbatim from StepRunner#extract_response_content: Struct
38
+ # mock providers expose #content, real responses expose #text.
39
+ def response_text(res)
40
+ return res.text.to_s if res.respond_to?(:text)
41
+ return res.content.to_s if res.respond_to?(:content)
42
+
43
+ res.to_s
44
+ end
45
+
46
+ # Struct/duck mock providers expose the array attribute without the
47
+ # Response predicate — read the attribute directly.
48
+ def raw_tool_calls(res)
49
+ res.respond_to?(:tool_calls) ? res.tool_calls : []
50
+ end
51
+
52
+ # Deferred: Instance carries no total_tokens state.
53
+ def track_usage(_res) = nil
54
+
55
+ # Deferred: subagent continuation is future work.
56
+ def handle_truncation(_res, _text, _tool_calls, _finish_reason) = nil
57
+
58
+ # ── announce ──
59
+ def announce_compaction(reason, layer)
60
+ @inst.emit_compacted_event(reason, layer)
61
+ end
62
+
63
+ def announce_llm_thought(_text) = nil
64
+
65
+ # ── terminals & dispatch ──
66
+ # The shared two-strike nudge policy (Channel#empty_response_turn)
67
+ # now governs empty, tool-less responses: one standard nudge row,
68
+ # then the terminal below on the second consecutive strike. The
69
+ # subagent can no longer "complete" with an empty result — the
70
+ # declared Phase 4 delta.
71
+ def empty_response_failure
72
+ @inst.fail_turn!("Empty response from model (2 consecutive)")
73
+ { ok: false, completed: false }
74
+ end
75
+
76
+ def final_text_result(final_text)
77
+ @inst.history << { role: "assistant", content: final_text.empty? ? nil : final_text, tool_calls: nil }
78
+ @inst.complete_turn!(final_text)
79
+ { ok: true, completed: true }
80
+ end
81
+
82
+ def append_tool_call_row(final_text, tool_calls)
83
+ @inst.history << { role: "assistant", content: final_text.empty? ? nil : final_text, tool_calls: tool_calls }
84
+ end
85
+
86
+ def on_finish(args, fallback_text)
87
+ message = args["message"] || args[:message] || fallback_text || "Completed"
88
+ @inst.auto_apply_buffer!(:finish) unless @inst.workspace == "branch"
89
+ @inst.complete_turn!(message)
90
+ { ok: true, completed: true }
91
+ end
92
+
93
+ def execute_tool_call(tool_call, text)
94
+ return on_finish(tool_call["args"] || {}, text) if TurnExecutor.finish_call?(@inst.tool_registry, tool_call)
95
+
96
+ @inst.execute_subagent_tool_call(tool_call)
97
+ nil
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../shell_deps"
4
+ require_relative "../tool_call_id"
5
+ require_relative "../tool_history"
6
+
7
+ module Rubik
8
+ module Orchestrator
9
+ class TurnExecutor
10
+ # The user-channel adapter: wraps the Loop through the ShellDeps
11
+ # façade, so bare Class#allocate-built test loops keep working
12
+ # (the StepExecutor/BudgetManager/StreamManager precedent). Owns the
13
+ # user channel's response shaping: continuation stitching, the
14
+ # truncation/auto-continue flow, thought-aware nudges, step events,
15
+ # and the turn_result contract.
16
+ class UserChannel < Channel
17
+ def initialize(loop_or_deps)
18
+ super()
19
+ @deps = ShellDeps.wrap(loop_or_deps)
20
+ end
21
+
22
+ attr_reader :deps
23
+
24
+ # ── required state ──
25
+ def history = @deps.history
26
+
27
+ def tool_registry = @deps.tool_registry
28
+
29
+ # Observation projection directive (plan D): yalnız goal-mode user
30
+ # kanalında çözülür; directive_for önkoşul matrisi (knob'lar,
31
+ # coeffect, rev ≥ 1) sağlanmazsa nil döner → compaction byte-özdeş.
32
+ def absorb_directive
33
+ return nil unless @deps.goal_mode?
34
+
35
+ Rubik::Orchestrator::GoalLedger::ObservationProjection.directive_for(deps: @deps)
36
+ end
37
+
38
+ def limits
39
+ { context_limit: @deps.context_limit, max_output_tokens: @deps.max_output_tokens, config: @deps.config }
40
+ end
41
+
42
+ # Loop policy (ToolCallId doctrine): unique-by-suffix ids — the
43
+ # session history survives across turns while step_count restarts.
44
+ def mint_call_id(idx)
45
+ ToolCallId.mint(@deps.step_count, idx + 1, unique: true)
46
+ end
47
+
48
+ # ── response shaping ──
49
+ # Moved stitch_response: a buffered continuation turn prepends the
50
+ # accumulated partial text and resets the buffer.
51
+ def final_text(res, text)
52
+ cm = @deps.loop_instance.continuation_manager
53
+ return text if cm.continuation_buffer.empty?
54
+
55
+ cm.stitch_and_reset(res.text.to_s)
56
+ end
57
+
58
+ def track_usage(res) = @deps.budget_manager.track_token_usage!(res)
59
+
60
+ # Moved truncation/auto-continue flow (Plan 90): classify the
61
+ # truncation, append the partial thought/text rows plus the
62
+ # targeted nudge, or return the diagnostic terminal when
63
+ # auto-continue is disabled or the ceiling is exhausted.
64
+ def handle_truncation(res, _text_resp, tool_calls, finish_reason)
65
+ cm = @deps.loop_instance.continuation_manager
66
+ return nil unless cm.truncation?(finish_reason)
67
+
68
+ trunc_action = cm.handle_truncation(
69
+ finish_reason: finish_reason,
70
+ text_resp: res.text.to_s,
71
+ tool_calls: tool_calls,
72
+ res: res,
73
+ max_tokens: @deps.max_output_tokens || @deps.safe_config("llm.default_max_output_tokens", 4096)
74
+ )
75
+
76
+ if trunc_action[:continue]
77
+ nudge_policy.reset!
78
+ if trunc_action[:thought]
79
+ @deps.history << { role: "assistant", content: nil, thought: trunc_action[:thought] }
80
+ elsif trunc_action[:partial_text]
81
+ @deps.history << { role: "assistant", content: trunc_action[:partial_text] }
82
+ end
83
+ @deps.history << { role: "user", content: trunc_action[:nudge] }
84
+ @deps.on_step&.call(trunc_action[:step_event].merge(step: @deps.step_count, max_steps: @deps.max_steps)) if trunc_action[:step_event]
85
+ return :consumed
86
+ end
87
+ return nil unless trunc_action[:error]
88
+
89
+ @deps.on_step&.call(type: :error, content: trunc_action[:error], step: @deps.step_count, max_steps: @deps.max_steps)
90
+ @deps.loop_instance.turn_result(success: false, error: trunc_action[:error])
91
+ end
92
+
93
+ # ── nudge texts ──
94
+ # Plan 61 D-11 thought-aware variant: reasoning spent the budget
95
+ # without an action deserves a different sentence than a blank reply.
96
+ def empty_nudge_message(res)
97
+ if res.respond_to?(:thought) && res.thought && !res.thought.to_s.strip.empty?
98
+ "Your reasoning was completed, but no action or final response was provided. Please call a tool or provide your final output."
99
+ else
100
+ super
101
+ end
102
+ end
103
+
104
+ # ── announce ──
105
+ def announce_compaction(reason, layer)
106
+ @deps.on_step&.call(type: :emergency_compact, reason: reason, step: @deps.step_count,
107
+ max_steps: @deps.max_steps, layer: layer)
108
+ end
109
+
110
+ def announce_llm_thought(text)
111
+ @deps.on_step&.call(type: :llm_thought, content: text, step: @deps.step_count, max_steps: @deps.max_steps)
112
+ end
113
+
114
+ # ── terminals & dispatch ──
115
+ def empty_response_failure
116
+ @deps.loop_instance.turn_result(success: false, error: "Empty response from model")
117
+ end
118
+
119
+ def final_text_result(final_text)
120
+ @deps.history << { role: "assistant", content: final_text }
121
+ @deps.on_step&.call(type: :message, content: final_text, step: @deps.step_count, max_steps: @deps.max_steps)
122
+ return goal_wait_yield(via: "text") if goal_guard_pending?
123
+
124
+ @deps.loop_instance.turn_result(success: true, output: final_text)
125
+ end
126
+
127
+ def append_tool_call_row(final_text, tool_calls)
128
+ @deps.history << { role: "assistant", content: final_text.empty? ? nil : final_text, tool_calls: tool_calls }
129
+ end
130
+
131
+ def on_finish(args, fallback_text)
132
+ @deps.loop_instance.step_executor.handle_finish_tool(args, fallback_text)
133
+ end
134
+
135
+ def execute_tool_call(tool_call, text)
136
+ # Guard BEFORE dispatch: a finish over live subagents must not
137
+ # reach handle_finish_tool (its auto-apply and :finish event
138
+ # would celebrate a completion the pipeline has not reached).
139
+ # The deferred call still gets its canonical result row — the
140
+ # loop continues, so the next provider request must see the
141
+ # tool_use paired (strict providers 400 on orphaned ids).
142
+ if TurnExecutor.finish_call?(@deps.tool_registry, tool_call) && goal_guard_pending?
143
+ ToolHistory.write(@deps.history,
144
+ tool_call_id: tool_call["id"],
145
+ tool_name: "finish",
146
+ status: :ok,
147
+ duration_ms: 0.0,
148
+ content: "finish deferred: dispatched subagents still active — the goal stays open")
149
+ return goal_wait_yield(via: "finish")
150
+ end
151
+
152
+ @deps.loop_instance.step_executor.execute_single_tool_call(tool_call, text)
153
+ end
154
+
155
+ private
156
+
157
+ # Goal-mode premature-completion guard (GoalWait): in goal mode a
158
+ # success terminal while dispatched subagents are still live is a
159
+ # false "Goal Achieved" (field incident 2026-10-05) — route it
160
+ # through the implicit yield-and-wait, which returns nil so the
161
+ # step loop continues. Interactive/default mode never reaches it.
162
+ def goal_guard_pending?
163
+ @deps.goal_mode? && @deps.loop_instance.goal_wait.pending?
164
+ end
165
+
166
+ def goal_wait_yield(via:)
167
+ @deps.loop_instance.goal_wait.premature_yield!(via: via)
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end