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,410 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../log"
4
+ require_relative "../orchestrator/subagent/notices"
5
+ require_relative "../prompt/conversation_rows"
6
+ require_relative "pending_notice_queue"
7
+
8
+ module Rubik
9
+ module REPL
10
+ # Plan 63 Faz C: phased initialization collaborators extracted from Shell
11
+ # (keeps shell.rb under the decoupling line-count gate). The Shell remains
12
+ # the coordinator: these methods depend on shell identity, so they live in
13
+ # an included module rather than in Composition (the pure builder).
14
+ module Lifecycle
15
+ # Plan 109 Faz B: notice kinds worth interrupting the idle prompt
16
+ # with an autonomous agent-0 turn. Excludes :context_compacted
17
+ # (self-healed, guidance says no action needed) and
18
+ # :auto_apply_failed (informational; the subagent's own next step
19
+ # reports it).
20
+ WAKE_KINDS = [:completed, :budget_exhausted, :error_paused, :message, :question, :task].freeze
21
+ # Phase 1: assign built subsystems. Entries derive from the
22
+ # ServiceCatalog (the single declaration point): one loop assigns every
23
+ # injectable entry's ivar, then a second pass runs the per-entry
24
+ # after_inject hooks (injected-buffer post-wiring — which runs only on
25
+ # the buffer_manager kwarg route, where Composition did not own the
26
+ # manager's config — and the ast_graph sync cache load). Optional
27
+ # entries keep their respond_to? nil-guard for older parts fixtures.
28
+ def inject_parts(parts, injected_buffer: false)
29
+ values = {}
30
+ ServiceCatalog.injectable.each do |entry|
31
+ values[entry.name] =
32
+ if entry.optional
33
+ parts.respond_to?(entry.name) ? parts[entry.name] : nil
34
+ else
35
+ parts[entry.name]
36
+ end
37
+ instance_variable_set(:"@#{entry.name}", values[entry.name])
38
+ end
39
+ # Two passes by design: after_inject hooks may reference ANY entry's
40
+ # value (the ast_graph cache load reads cache_store), so they run
41
+ # only after every ivar is assigned — combining the loops would tie
42
+ # hooks to catalog order.
43
+ ServiceCatalog.wire_after_inject(values, parts, injected_buffer: injected_buffer)
44
+ end
45
+
46
+ # Phase 2: provide the core subsystems into the Runtime Coeffect
47
+ # Context. Rows derive from the ServiceCatalog; canonical (long-form)
48
+ # keys only — short forms ("buffer", "lsp", ...) remain readable via
49
+ # Rubik::Coeffects LEGACY_ALIASES for third parties.
50
+ def wire_runtime_context(parts)
51
+ ServiceCatalog.wire_rows(self, parts).each { |key, value| @runtime_manager.provide(key, value) }
52
+ end
53
+
54
+ # Phase 3: plugins declared in config (gems, drop-ins, aliases).
55
+ def load_plugins(parts)
56
+ return unless @plugin_manager
57
+
58
+ Array(@config.fetch("plugins.load", [])).each { |g| @plugin_manager.load_plugin_gem(g, runtime_manager: @runtime_manager) }
59
+ if @config.fetch("plugins.autoload_dropin", true) && parts.respond_to?(:plugin_storage) && parts.plugin_storage
60
+ @plugin_manager.load_dropin_plugins(runtime_manager: @runtime_manager, storage: parts.plugin_storage)
61
+ end
62
+ aliases_cfg = @config.fetch("plugins.aliases", {})
63
+ aliases_cfg.each { |s, f| @plugin_manager.register_alias(s, f) } if aliases_cfg.is_a?(Hash)
64
+ end
65
+
66
+ # Phase 4: AST, context-index and agent-loop runtime services.
67
+ def register_runtime_services
68
+ Rubik::Runtime::ASTService.register(@runtime_manager, @ast_graph, cache_store: @cache_store)
69
+ Rubik::Runtime::IndexService.register(@runtime_manager, @context_index)
70
+ Rubik::Runtime::LoopService.register(@runtime_manager, shell: self)
71
+ Rubik::Storage::Service.register(@runtime_manager, Rubik::Storage::Service.build(config: @config, root_dir: @root_dir))
72
+ # Vakanüvis kronikçisi storage'dan HEMEN SONRA bağlanır (STORE
73
+ # coeffect'i çözülebilir olmalı); kendi child ctx'inde yaşar,
74
+ # subagents.vakanuvis.enabled=false ile tümüyle kurulmaz.
75
+ Rubik::Storage::Vakanuvis::Service.bind(@runtime_manager, config: @config, event_bus: @event_bus)
76
+ # Pruner (KÖK pencere budayıcısı, DEFAULT ON —
77
+ # subagents.pruner.enabled; opt-out: false): SHELL/SUPERVISOR/DMESG
78
+ # coeffect'leri Faz 2'den gelir; kendi child ctx'inde yaşar,
79
+ # prune_context aracı REQUIRES üzerinden reaktif açılır.
80
+ Rubik::REPL::Pruner::Service.bind(@runtime_manager, config: @config, event_bus: @event_bus)
81
+ end
82
+
83
+ # Phase 5: build the tool registry and publish it to the runtime.
84
+ def build_and_provide_tool_registry
85
+ @tool_registry = build_tool_registry
86
+ @mcp_manager.tools_registry = @tool_registry if @mcp_manager
87
+ @subagent_supervisor.tool_registry = @tool_registry if @subagent_supervisor.respond_to?(:tool_registry=)
88
+ @runtime_manager.provide(:tool_registry, @tool_registry)
89
+ end
90
+
91
+ # Phase 6: UI theme/renderer wiring into the runtime.
92
+ def sync_ui
93
+ @ui_theme = Rubik::UI::Theme.for(@output, @config)
94
+ @ui_renderer = Rubik::UI::RenderService.register(@runtime_manager, shell: self)
95
+ end
96
+
97
+ # Phase 7: route :default; on resolve, satisfy the `llm` coeffect.
98
+ # Audit 1: rescue is intentional (credentials may be unconfigured).
99
+ def sync_default_llm_route
100
+ default_routed = @router.route(task_type: :default)
101
+ @runtime_manager.provide(:llm_client, default_routed[:provider]) if default_routed && default_routed[:provider]
102
+ @subagent_supervisor.router = @router if @subagent_supervisor.respond_to?(:router=)
103
+ rescue StandardError => e
104
+ debug_warn("Default LLM route unavailable at startup (coeffect stays pending): #{e.class}: #{e.message}")
105
+ end
106
+
107
+ # Reload the provider catalog from config and re-route the live client —
108
+ # the mandatory pair after any providers.* mutation. Reactive wiring
109
+ # (wire_llm_change) runs it for listener-driven changes; commands run it
110
+ # explicitly because the Config verbs mutate the live spec hash in
111
+ # place, so set sees old == new and never fires the providers.*
112
+ # notification — dropping this "redundant-looking" refresh would
113
+ # silently desync the registry.
114
+ def refresh_llm_registry!
115
+ llm_registry&.load_from_config(config)
116
+ resync_llm_route!
117
+ end
118
+
119
+ # Live LLM re-route (reactive config wiring, !model/!provider switches):
120
+ # re-open sticky routing so the next free-text turn re-resolves with the
121
+ # new model (Plan 61 D-7), then re-provide the default llm_client
122
+ # coeffect. A failing route keeps the previous client — the same silent
123
+ # contract as the extracted Commands::AgentModels.sync_default_provider.
124
+ def resync_llm_route!
125
+ @conversation_route_stamp = nil
126
+ default_routed = begin
127
+ @router&.route(task_type: :default)
128
+ rescue StandardError => e
129
+ debug_warn("Live LLM re-route failed (keeping previous client): #{e.class}: #{e.message}")
130
+ nil
131
+ end
132
+ return unless default_routed && default_routed[:provider]
133
+
134
+ @runtime_manager.provide(:llm_client, default_routed[:provider])
135
+ end
136
+
137
+ # Phase 8: warm-up — AST indexing, built-in commands, ambient globals,
138
+ # overlay loading, reactive config wiring.
139
+ def warm_up!
140
+ # nil = unset: Platform.async_knob falls back to the env-aware
141
+ # default (RUBIK_TEST=1 warms synchronously-off) so the schema entry
142
+ # never forces async warm in tests.
143
+ warm_async = Platform.async_knob(@config, "ast.async_warm")
144
+ warm_ast_graph(async: warm_async)
145
+ register_default_commands
146
+ Rubik.current_shell = self
147
+ Rubik.runtime_manager = @runtime_manager
148
+ @overlay_storage.load_all(self, @runtime_manager)
149
+ @config_cleanups = ConfigWiring.wire(self)
150
+ # Phase 5 built the tool registry; the skill tool's epoch (its
151
+ # revertible registration + inverse) opens on the manager's own
152
+ # sub-context only after config wiring so enable/disable knobs react.
153
+ @skill_manager&.open_epoch!(@tool_registry)
154
+ # mcp.async_load nil = env-aware (RUBIK_TEST=1 boots synchronously),
155
+ # resolved by the same Platform.async_knob home as ast.async_warm:
156
+ mcp_async = Platform.async_knob(@config, "mcp.async_load")
157
+ wire_mcp_boot_notices!(async: mcp_async)
158
+ @mcp_manager&.load_from_config(@config, async: mcp_async)
159
+ wire_subagent_events!
160
+ end
161
+
162
+ # Async MCP boot progress notices (mcp-async-boot D8): subscribe BEFORE
163
+ # the boot triggers so no event is missed, with the unsubscribe inverse
164
+ # registered in the same breath (wire_subagent_events! pattern). Async
165
+ # mode only — a synchronous boot finishes before the first prompt, so
166
+ # its failures already surfaced on stderr.
167
+ def wire_mcp_boot_notices!(async:)
168
+ return unless async && @event_bus
169
+ return if @mcp_notice_tokens # idempotent re-wire
170
+
171
+ ctx = @runtime_manager&.context
172
+ return unless ctx.respond_to?(:on_dispose)
173
+
174
+ @mcp_notice_tokens = [
175
+ [:mcp_server_loaded, @event_bus.subscribe(:mcp_server_loaded) { |payload| print_mcp_boot_notice(:loaded, payload) }],
176
+ [:mcp_server_failed, @event_bus.subscribe(:mcp_server_failed) { |payload| print_mcp_boot_notice(:failed, payload) }]
177
+ ]
178
+ ctx.on_dispose do
179
+ @mcp_notice_tokens&.each { |(event, token)| @event_bus.unsubscribe(event, token) }
180
+ @mcp_notice_tokens = nil
181
+ end
182
+ end
183
+
184
+ # Same kworker-thread contract as handle_subagent_event: no shell locks,
185
+ # drop silently when the UI is not up yet (an instant boot failure is
186
+ # still on stderr and in dmesg). Pure display line — never enters the
187
+ # conversation history and never wakes an idle turn.
188
+ def print_mcp_boot_notice(state, payload)
189
+ return unless @running && @output
190
+ return unless payload.is_a?(Hash)
191
+
192
+ name = payload[:server]
193
+ line = if state == :loaded
194
+ "mcp: server '#{name}' ready (#{payload[:tools] || 0} tools)"
195
+ else
196
+ "mcp: server '#{name}' failed (#{payload[:error]})"
197
+ end
198
+ @terminal_guard&.print_safe_notice(line)
199
+ end
200
+
201
+ def wire_subagent_events!
202
+ return unless @event_bus
203
+ return if @subagent_event_tokens # idempotent re-wire (RenderService precedent)
204
+
205
+ # Inverse-by-construction: subscribe only where the unsubscribe
206
+ # inverse can be registered in the same breath (AGENTS.md §1A).
207
+ ctx = @runtime_manager&.context
208
+ return unless ctx.respond_to?(:on_dispose)
209
+
210
+ @subagent_notices_mutex = Mutex.new
211
+ @subagent_notices = []
212
+ # Plan 109: the idle-turn wake flag, eagerly initialized BEFORE any
213
+ # subscription exists so handlers never race its construction.
214
+ @pending_notice_queue = PendingNoticeQueue.new
215
+ tokens = Rubik::Orchestrator::Subagent::Notices::EVENTS.keys.map do |event|
216
+ [event, @event_bus.subscribe(event) { |payload| handle_subagent_event(event, payload) }]
217
+ end
218
+ tokens << [:unit_state_changed, @event_bus.subscribe(:unit_state_changed) { |payload| handle_unit_event(payload) }]
219
+ tokens << [:subagent_message_to_parent, @event_bus.subscribe(:subagent_message_to_parent) { |payload| handle_parent_message(payload) }]
220
+ @subagent_event_tokens = tokens
221
+ ctx.on_dispose do
222
+ @subagent_event_tokens&.each { |(event, token)| @event_bus.unsubscribe(event, token) }
223
+ @subagent_event_tokens = nil
224
+ end
225
+ end
226
+
227
+ # ONE routing seam for subagent notices (Candidate 02, unified for
228
+ # Plan 109): bus event -> normalized notice -> channel-split
229
+ # delivery. Called on the kworker thread (bus dispatch is
230
+ # synchronous): Queue pushes, the standby Mutex, and the guard
231
+ # monitor only — no shell locks. Model channel (structured hash)
232
+ # enters the active loop queue and is rendered to XML at drain
233
+ # time; user channel (terminal line) is buffered to stdout while a
234
+ # loop is active and flushed at turn end. Between turns the notice
235
+ # is injected into @conversation_history, printed, and — when
236
+ # wake-worthy — queued to owe agent-0 an autonomous notice turn.
237
+ def handle_subagent_event(event_name, payload)
238
+ return unless @running && @output && !@replaying
239
+
240
+ notice = Rubik::Orchestrator::Subagent::Notices.normalize(event_name, payload)
241
+ route_subagent_notice(notice) if notice
242
+ end
243
+
244
+ def handle_unit_event(payload)
245
+ return unless @running && @output && !@replaying
246
+ return unless payload.is_a?(Hash)
247
+
248
+ type = payload[:type]&.to_sym
249
+ return unless [:process, :task].include?(type)
250
+
251
+ to_state = payload[:to_state]&.to_sym
252
+ return unless [:completed, :failed].include?(to_state)
253
+
254
+ route_subagent_notice({ kind: :task, id: payload[:unit_id], role: "Background Task", state: to_state.to_s, reason: payload[:reason] })
255
+ end
256
+
257
+ def handle_parent_message(payload)
258
+ return unless @running && @output && !@replaying
259
+ return unless payload.is_a?(Hash)
260
+
261
+ route_subagent_notice({ kind: :message, id: payload[:sender_id] || "subagent",
262
+ role: "Subagent", state: "message", reason: payload[:message].to_s })
263
+ end
264
+
265
+ def route_subagent_notice(notice)
266
+ line = Rubik::Orchestrator::Subagent::Notices.user_line(notice)
267
+ if @active_loop
268
+ @active_loop.enqueue_subagent_notice(notice) if @active_loop.respond_to?(:enqueue_subagent_notice)
269
+ @subagent_notices_mutex&.synchronize { @subagent_notices << line }
270
+ else
271
+ inject_subagent_notice_into_conversation_history!(notice)
272
+ if @terminal_guard
273
+ @terminal_guard.print_safe_notice(line)
274
+ else
275
+ @output.puts "\n#{line}"
276
+ end
277
+ # Wakeloop fix 2026-10-09: an event landing inside an AUTONOMOUS
278
+ # turn's idle-path windows (before @active_loop is set, or in the
279
+ # ensure tail after it is cleared) must not arm a follow-up wake —
280
+ # that is the probabilistic residue of the chained-auto-turn bug.
281
+ # History injection and the printed line above still happen (no
282
+ # loss); only the synthetic Enter is withheld. A fresh event at a
283
+ # genuinely idle prompt (@autonomous_turn false) wakes as designed.
284
+ request_reactive_wakeup!(notice) unless @autonomous_turn
285
+ end
286
+ end
287
+
288
+ # Plan 109 Faz B: the reactive wake composition — vocabulary policy
289
+ # (WAKE_KINDS), kill-switch (subagents.reactive_wakeup config),
290
+ # state (PendingNoticeQueue), mechanism (TerminalGuard#safe_wakeup!).
291
+ # Every wake-worthy enqueue attempts the kick (no empty->non-empty
292
+ # coalescing): a kick refused earlier (user typing / paste
293
+ # collection) must retry on the next event, or the wake would wait
294
+ # for a manual Enter. TerminalGuard re-verifies emptiness under its
295
+ # monitor, so a typing user is never disturbed, and a surplus
296
+ # injected Enter degrades to the :empty noop. Runs on the kworker
297
+ # thread: queue push and the guard monitor only.
298
+ def request_reactive_wakeup!(notice)
299
+ return unless reactive_wakeup_enabled?
300
+ return unless notice.is_a?(Hash) && WAKE_KINDS.include?(notice[:kind])
301
+
302
+ pending_notice_queue.enqueue(notice)
303
+ @terminal_guard&.safe_wakeup!
304
+ end
305
+
306
+ def reactive_wakeup_enabled?
307
+ @config.respond_to?(:fetch) ? @config.fetch("subagents.reactive_wakeup", true) != false : true
308
+ end
309
+
310
+ # Eager init happens in wire_subagent_events! (before any
311
+ # subscription exists); the lazy fallback keeps bare Lifecycle test
312
+ # doubles working (SubagentNoticeInbox mold).
313
+ def pending_notice_queue
314
+ @pending_notice_queue ||= PendingNoticeQueue.new
315
+ end
316
+
317
+ def pending_wake_notices?
318
+ pending_notice_queue.any?
319
+ end
320
+
321
+ def drain_pending_wake_notices!
322
+ pending_notice_queue.drain_all
323
+ end
324
+
325
+ # Staleness rule: any real turn's context already includes the
326
+ # idle-injected notices in @conversation_history — a surviving
327
+ # pending wake would turn the next stray Enter into a redundant
328
+ # notice turn.
329
+ def clear_pending_wake_notices!
330
+ pending_notice_queue.clear!
331
+ end
332
+
333
+ def inject_subagent_notice_into_conversation_history!(notice)
334
+ xml_payload = Rubik::Orchestrator::Subagent::Notices.history_block([notice])
335
+ @conversation_history ||= []
336
+ Rubik::Prompt::ConversationRows.append_notice!(@conversation_history, xml_payload)
337
+ end
338
+
339
+ def flush_subagent_notices!
340
+ return unless @output && @subagent_notices_mutex
341
+
342
+ notices = @subagent_notices_mutex.synchronize do
343
+ list = @subagent_notices.dup
344
+ @subagent_notices.clear
345
+ list
346
+ end
347
+ return if notices.empty?
348
+
349
+ if @terminal_guard
350
+ notices.each { |m| @terminal_guard.print_safe_notice(m) }
351
+ else
352
+ @output.puts ""
353
+ notices.each { |m| @output.puts m }
354
+ end
355
+ end
356
+
357
+ # Session review B3 (turn-loss fix): Loop's drain runs at the TOP of each
358
+ # step, so notices landing during the final step stranded in the loop
359
+ # inbox at turn end — the user channel flushed a line, but the model
360
+ # channel never entered history and no wake was owed (Plan 109's
361
+ # documented "late-notice asymmetry" accepted the loss; a live session
362
+ # showed it costs a whole turn). Called from execute_free_text's ensure
363
+ # AFTER @active_loop = nil: the notices are injected into
364
+ # @conversation_history (visible to the next turn) and wake-worthy kinds
365
+ # arm the reactive wake, exactly like the idle-path branch of
366
+ # route_subagent_notice. Runs on the turn thread. Per-notice rescue is
367
+ # the ensure block's own contract ("cleanup must never mask the
368
+ # in-flight exception") — and a raising notice must not strand its
369
+ # queue-mates that drain_all already popped.
370
+ #
371
+ # arm_wake: false keeps the model-channel delivery but does NOT fire a
372
+ # synthetic Enter (wakeloop fix 2026-10-09): an INTERRUPTED turn and an
373
+ # AUTONOMOUS notice turn settle instead of re-arming — otherwise every
374
+ # stranded notice chains another auto-turn (the prompt never returns)
375
+ # and the cycle survives Ctrl+C.
376
+ def deliver_stranded_loop_notices!(loop_instance, arm_wake: true)
377
+ return unless loop_instance.respond_to?(:subagent_notice_inbox)
378
+
379
+ loop_instance.subagent_notice_inbox.drain_all.each do |notice|
380
+ inject_subagent_notice_into_conversation_history!(notice)
381
+ request_reactive_wakeup!(notice) if arm_wake
382
+ rescue StandardError => e
383
+ # Load-bearing visibility (wakeloop fix 2026-10-09): this delivery
384
+ # is the fix's "no loss" guarantee, so a dropped notice may not
385
+ # vanish behind the ENV-gated debug_warn. Unconditional, non-raising.
386
+ Rubik::Log.warn(:repl, "stranded notice delivery failed (#{notice[:kind].inspect}): " \
387
+ "#{e.class}: #{e.message}")
388
+ end
389
+ end
390
+
391
+ def reap_dead_kernel_units!
392
+ init = @init || (@runtime_manager&.context&.get(:init) if @runtime_manager&.context.respond_to?(:get))
393
+ init&.reap_dead_units!
394
+ rescue StandardError => e
395
+ debug_warn("reap_dead_kernel_units failed: #{e.class}: #{e.message}")
396
+ end
397
+
398
+ # Audit 1: ENV["RUBIK_DEBUG"]-gated logging for intentionally swallowed
399
+ # exceptions (convention from config/store.rb). Never raises.
400
+ #
401
+ # $stderr.puts, not Kernel#warn: warn is silenced when $VERBOSE is nil
402
+ # (bundler/rake -W0). Same ruling as Log.emit and Platform.debug_warn.
403
+ def debug_warn(msg)
404
+ $stderr.puts "[WARN] #{msg}" if ENV["RUBIK_DEBUG"] # rubocop:disable Style/StderrPuts
405
+ rescue StandardError
406
+ nil
407
+ end
408
+ end
409
+ end
410
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module REPL
5
+ # Multi-line paste desteği (Plan 91): terminaller yapıştırılan bloğu
6
+ # satır-satır ENTER'larla gönderir; Windows konsolunda Reline'in
7
+ # bracketed-paste'i bulunmadığından her satır ayrı bir turn olarak
8
+ # yürütülür. Bu mixin burst algılama (zamanlama + IOGate#in_pasting?)
9
+ # ve explicit `!paste` modunu sağlar. Durum Shell'de yaşar
10
+ # (@config, @reline, @output); burada yalnızca politika kodlanır.
11
+ module PasteSupport
12
+ # Plan 91 — set by the !paste command, consumed once by
13
+ # read_and_process_line to enter explicit paste mode. The named API
14
+ # (not an ivar poke) is what keeps the commands layer out of Shell
15
+ # internals.
16
+ def request_paste!
17
+ @paste_requested = true
18
+ end
19
+
20
+ # Bir yapıştırma bloğunun satırlarının "aynı burst" sayılması için
21
+ # iki readline dönüşü arasındaki maksimum süre (ms).
22
+ def paste_join_window_ms
23
+ @config ? @config.fetch("ui.paste_join_window_ms", 120).to_f : 120.0
24
+ end
25
+
26
+ # Monotonic saniye sayacı; testler singleton ile ezebilir.
27
+ def paste_clock
28
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+ end
30
+
31
+ # Yapıştırma sürerken giriş tamponunda bekleyen bayt var mı?
32
+ # SADECE gerçek interaktif konsolda güvenilirdir: TTY olmayan
33
+ # ortamlarda (test harness, pipe) Windows/ANSI gate'in in_pasting?
34
+ # yanıltıcı true döndürebildiğinden interactive_input? şarttır.
35
+ def paste_pending?
36
+ return false unless interactive_input?
37
+
38
+ gate = @reline.const_defined?(:IOGate) ? @reline::IOGate : nil
39
+ return false unless gate.respond_to?(:in_pasting?)
40
+
41
+ gate.in_pasting? == true
42
+ rescue StandardError
43
+ false
44
+ end
45
+
46
+ def paste_line_fast?(last_line_at, now)
47
+ return false if last_line_at.nil?
48
+
49
+ ((now - last_line_at) * 1000.0) <= paste_join_window_ms
50
+ end
51
+
52
+ # Burst collection only makes sense on a real interactive console:
53
+ # with stubbed/piped readline (tests, RUBIK_TEST=1, non-TTY stdin)
54
+ # every stubbed line arrives "instantly", so an ungated collector
55
+ # swallows consecutive scripted lines into one turn. Test harnesses
56
+ # stub this method to exercise the collector deterministically.
57
+ def paste_burst_eligible?
58
+ interactive_input?
59
+ end
60
+
61
+ # Explicit paste modu: satırları '.' satırına (veya EOF'a) kadar okur,
62
+ # birleşik metni döndürür. ^C Interrupt olarak yukarı sızar
63
+ # (read_and_process_line'in mevcut rescue'su iptali zaten ele alır).
64
+ # Plan 109: koleksiyon with_collecting içinde — reaktif bir uyandırma
65
+ # Enter'ı yapıştırılan metne boş satır eklemez.
66
+ def read_paste_block
67
+ @output.puts "[Rubik] Paste modu: metni yapıştırın, bitirmek için tek başına '.' yazın (Ctrl+C iptal)."
68
+ lines = []
69
+ collector = lambda do
70
+ loop do
71
+ line = respond_to?(:safe_readline) ? safe_readline("paste > ", add_history: false) : @reline.readline("paste > ", false)
72
+ break if line.nil? # EOF -> elimizdekiyle bitir
73
+ break if ["\x04", "\x1a"].include?(line)
74
+ break if line.strip == "."
75
+
76
+ lines << line
77
+ end
78
+ end
79
+ if @terminal_guard.respond_to?(:with_collecting)
80
+ @terminal_guard.with_collecting(&collector)
81
+ else
82
+ collector.call
83
+ end
84
+ lines.join("\n")
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module REPL
5
+ # The idle-path wake queue (Plan 109 Faz B): a queue-only boundary in
6
+ # the Orchestrator::SubagentNoticeInbox mold — validate at entry,
7
+ # Queue-backed, FIFO drain with a ThreadError terminator. Deliberately
8
+ # distinct from the orchestrator inbox: that one is the ACTIVE-turn
9
+ # model-channel mailbox with run/continuation lifecycle; this one is
10
+ # the IDLE-turn wake flag. Its contents are advisory payload only —
11
+ # the authoritative copy of every notice rides @conversation_history
12
+ # via the idle injection; the queue answers ONE question: does the
13
+ # REPL owe agent-0 an autonomous notice turn?
14
+ #
15
+ # Threading contract: enqueue runs on the kworker thread (synchronous
16
+ # EventBus dispatch) — validation plus a bare Queue#push, never a
17
+ # lock a REPL thread might hold. any?/drain_all/clear! run on the
18
+ # REPL thread between prompts. The check-then-act window between
19
+ # any? and readline is benign by construction: the handler that
20
+ # pushes also fires TerminalGuard#safe_wakeup!, whose ungetc wakes
21
+ # the NEXT readline even if the current check already passed.
22
+ class PendingNoticeQueue
23
+ def initialize
24
+ @queue = Queue.new
25
+ end
26
+
27
+ # Entry point and the ONLY mutation path: validate, then push.
28
+ # Pinned message, mold style.
29
+ def enqueue(notice)
30
+ raise ArgumentError, "pending wake notice must be a Hash with :kind, got #{notice.inspect}" unless notice.is_a?(Hash) && notice[:kind]
31
+
32
+ @queue.push(notice)
33
+ end
34
+
35
+ def any?
36
+ !empty?
37
+ end
38
+
39
+ def empty?
40
+ @queue.empty?
41
+ end
42
+
43
+ # Pops everything pending (FIFO); a second call returns [].
44
+ def drain_all
45
+ notices = []
46
+ loop { notices << @queue.pop(true) }
47
+ rescue ThreadError
48
+ notices
49
+ end
50
+
51
+ # Staleness rule (Plan 109): any user-driven dispatch supersedes a
52
+ # pending wake — the notices already ride the conversation history
53
+ # into whatever turn runs next.
54
+ def clear!
55
+ drain_all
56
+ nil
57
+ end
58
+ end
59
+ end
60
+ end