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,441 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "timers"
5
+ require_relative "dns_pool"
6
+
7
+ module Rubik
8
+ module Concurrency
9
+ # The reactor backing a Scheduler: one OS thread, N non-blocking fibers.
10
+ #
11
+ # Thread discipline (load-bearing — verified against Ruby 4.0.6 probes,
12
+ # see _plan/fiber-scheduler-silent-concurrency-2026-10-05.md):
13
+ # * VM hooks (io_wait, kernel_sleep, block, fiber creation) always run
14
+ # on the loop thread, inside a resume. Timers, the io-interest
15
+ # registry, park generations and spawn bookkeeping are therefore
16
+ # touched only by the loop thread — no locks.
17
+ # * Foreign threads (dying join targets, process waiters, DNS workers)
18
+ # wake fibers through #push / #unpark, which guard @runnable/@parked
19
+ # with @mutex and write a self-pipe byte so a blocked IO.select on
20
+ # the loop thread wakes immediately.
21
+ # * Wake staleness is handled by park generations: every registration
22
+ # stamps @gen[fiber]; timer fires and io dispatch only push when the
23
+ # generation still matches, so a fiber that timed out and re-parked
24
+ # elsewhere can never be resumed by a stale source.
25
+ #
26
+ # Drain model (waiters, not liveness — Fiber#alive? does not exist on
27
+ # Ruby 4.0): the loop runs while anything is runnable, parked (Mutex/CV/
28
+ # Thread#join/block), io-waiting, or timer-pending. Finished fibers leave
29
+ # no registration, so "is anything waiting?" answers "are we done?".
30
+ class EventLoop
31
+ READABLE = 1
32
+ WRITABLE = 2
33
+
34
+ attr_reader :spawn_errors
35
+
36
+ def initialize
37
+ @mutex = ::Mutex.new
38
+ @runnable = [] # [[fiber, resume_value]] — guarded by @mutex
39
+ @parked = {} # fiber => true (block-hook parks) — guarded
40
+ @waiter_threads = [] # process-wait threads — guarded
41
+ # Stale timers (fiber re-parked or resumed since registration) must
42
+ # not gate draining — @gen is bumped on every resume, so any of this
43
+ # fiber's outstanding registrations becomes inert at that moment.
44
+ @timers = Timers.new { |fiber, gen| @gen[fiber] != gen }
45
+ @io_waiters = {} # io => { read: {fiber=>gen}, write: {fiber=>gen} }
46
+ @gen = ::Hash.new(0) # fiber => park generation (loop thread only)
47
+ @live = {} # fiber => true while a spawned fiber runs
48
+ @next_ordinal = 0
49
+ @spawn_errors = [] # [[ordinal, exception]] — body/spawn lifecycle
50
+ @loop_thread = nil
51
+ @closed = false
52
+ @wake_read, @wake_write = ::IO.pipe
53
+ end
54
+
55
+ def closed?
56
+ @closed
57
+ end
58
+
59
+ # ---- Any-thread API ---------------------------------------------------
60
+
61
+ # Queues a fiber to be resumed with value. Safe from foreign threads:
62
+ # pushes write a self-pipe byte so the loop's IO.select wakes at once.
63
+ def push(fiber, value: true)
64
+ @mutex.synchronize do
65
+ return if @closed
66
+
67
+ @runnable << [fiber, value]
68
+ end
69
+ kick unless ::Thread.current.equal?(@loop_thread)
70
+ end
71
+
72
+ # unblock-hook counterpart for programmatically parked fibers (DNS
73
+ # jobs, fan-out latches): removes the park and delivers value. Park
74
+ # removal and resume append happen under ONE lock section — a split
75
+ # update could let the drain's idle? check observe both sets empty in
76
+ # the gap and exit, stranding the resume (and the fiber's ensures).
77
+ def unpark(_blocker, fiber, value: true)
78
+ @mutex.synchronize do
79
+ return if @closed
80
+
81
+ @parked.delete(fiber)
82
+ @runnable << [fiber, value]
83
+ end
84
+ kick unless ::Thread.current.equal?(@loop_thread)
85
+ end
86
+
87
+ def interrupt(fiber, exception = Interrupted)
88
+ exc =
89
+ if exception.is_a?(::Exception)
90
+ exception
91
+ elsif exception.is_a?(::Class)
92
+ exception.new("fiber interrupted")
93
+ else
94
+ Interrupted.new("fiber interrupted")
95
+ end
96
+ push(fiber, value: exc)
97
+ end
98
+
99
+ def track_waiter(thread)
100
+ @mutex.synchronize { @waiter_threads << thread unless @closed }
101
+ end
102
+
103
+ # ---- Loop-thread API (VM hooks & body) ---------------------------------
104
+
105
+ # Scheduler.fiber hook (Ruby 4.0): we create the fiber. Errors from the
106
+ # block never escape into the reactor — they are captured per spawn
107
+ # ordinal and raised by the boundary after the drain (K7), including
108
+ # Interrupt-level deaths so teardown can finish cleanly first.
109
+ def spawn_fiber(&block)
110
+ @next_ordinal += 1
111
+ ordinal = @next_ordinal
112
+ fiber = nil
113
+ fiber = ::Fiber.new(blocking: false) do
114
+ block.call
115
+ rescue ::Exception => e # rubocop:disable Lint/RescueException -- boundary re-raises after drain
116
+ @spawn_errors << [ordinal, e]
117
+ ensure
118
+ @live.delete(fiber)
119
+ end
120
+ @live[fiber] = true
121
+ push(fiber, value: nil)
122
+ fiber
123
+ end
124
+
125
+ def nap(duration)
126
+ park_fiber { |fiber, gen| @timers.add(monotonic + duration, fiber, true, gen) }
127
+ true
128
+ end
129
+
130
+ # block-hook: park the current fiber on a blocker (Mutex, CV, Thread).
131
+ # Returns truthy when unparked, false when the timeout elapsed. A
132
+ # timeout of 0 still registers a timer (fires immediately) — truthiness
133
+ # guards would park join(0)/wait(0) polls forever.
134
+ def park(_blocker, timeout = nil)
135
+ @mutex.synchronize { @parked[::Fiber.current] = true }
136
+ value = park_fiber do |fiber, gen|
137
+ @timers.add(monotonic + timeout, fiber, :timeout, gen) unless timeout.nil?
138
+ end
139
+ value == :timeout ? false : value
140
+ end
141
+
142
+ # io_wait hook: interests is a READABLE/WRITABLE mask. The resume value
143
+ # is the ready-event mask (0 when the timeout elapsed — falsy, which is
144
+ # what IO#wait_readable reports to callers like Net::HTTP).
145
+ def wait_io(io, interests, timeout)
146
+ fiber = ::Fiber.current
147
+ gen = (@gen[fiber] += 1)
148
+ slot = (@io_waiters[io] ||= { read: {}, write: {} })
149
+ slot[:read][fiber] = gen if interests.anybits?(READABLE)
150
+ slot[:write][fiber] = gen if interests.anybits?(WRITABLE)
151
+ @timers.add(monotonic + timeout, fiber, 0, gen) unless timeout.nil?
152
+ value = ::Fiber.yield
153
+ raise value if value.is_a?(::Exception)
154
+
155
+ value
156
+ end
157
+
158
+ # io_read body (one nonblocking attempt per call — the VM re-invokes
159
+ # the hook in a loop, mirroring IO::Buffer#read's one-syscall shape).
160
+ # Fills the caller's buffer and returns bytes read (0 = EOF).
161
+ #
162
+ # Two 4.0.6 facts shape this (verified by probe): the integer hook
163
+ # args are protocol noise (3.3 passes length; 4.0 passes 0,0 — both
164
+ # size the buffer to the requested read, hence buffer.size), and
165
+ # EVERY blocking-family read primitive — read_nonblock included — is
166
+ # itself routed back through scheduler.io_read once the hook exists.
167
+ # The Fiber.blocking wrap is the VM-sanctioned escape (ruby's own test
168
+ # scheduler wraps its primitives the same way): it runs the primitive
169
+ # immediately, bypassing interception, and read_nonblock(exception:
170
+ # false) never blocks the OS thread.
171
+ def read_io(io, buffer)
172
+ length = buffer.size
173
+ return 0 if length.zero?
174
+
175
+ loop do
176
+ chunk = ::Fiber.blocking { io.read_nonblock(length, exception: false) }
177
+ case chunk
178
+ when :wait_readable then wait_io(io, READABLE, nil)
179
+ when :wait_writable then wait_io(io, WRITABLE, nil)
180
+ when nil then return 0
181
+ else
182
+ buffer.copy(::IO::Buffer.for(chunk), 0)
183
+ return chunk.bytesize
184
+ end
185
+ end
186
+ end
187
+
188
+ # io_write body: writes the whole caller-supplied buffer with one
189
+ # nonblocking attempt per call, parking on WRITABLE between attempts.
190
+ # (4.0.6 passes (0,0) as integers and expects the full buffer out.)
191
+ def write_io(io, buffer)
192
+ data = buffer.get_string(0, buffer.size)
193
+ return 0 if data.empty?
194
+
195
+ loop do
196
+ written = ::Fiber.blocking { io.write_nonblock(data, exception: false) }
197
+ return written if written.is_a?(::Integer)
198
+
199
+ wait_io(io, WRITABLE, nil)
200
+ end
201
+ end
202
+
203
+ # DNS resolution parks the fiber; the DnsPool worker delivers the list
204
+ # of IP strings (or an exception instance to raise) via unpark.
205
+ def resolve_address(hostname)
206
+ pool = (@dns_pool ||= DnsPool.new(self))
207
+ pool.resolve(hostname)
208
+ rescue StandardError => e
209
+ # Pool unusable (thread creation blocked, shutdown racing): resolve
210
+ # inline — correct, just not interleaved.
211
+ Rubik::Log.warn(:concurrency, "dns pool unavailable, resolving inline: #{e.class}: #{e.message}")
212
+ ::Addrinfo.getaddrinfo(hostname, nil).map(&:ip_address).uniq
213
+ end
214
+
215
+ # ---- Reactor -------------------------------------------------------------
216
+
217
+ # Runs the boundary body as a fiber and drains everything it spawned.
218
+ # Returns [body_result, body_error]; spawned-fiber errors live in
219
+ # #spawn_errors for the boundary to raise after the fact.
220
+ def run(&body)
221
+ @loop_thread = ::Thread.current
222
+ result = nil
223
+ body_error = nil
224
+ runner = nil
225
+ runner = ::Fiber.new(blocking: false) do
226
+ result = body.call
227
+ rescue ::Exception => e # rubocop:disable Lint/RescueException -- re-raised by Boundary after drain
228
+ body_error = e
229
+ end
230
+ @live[runner] = true
231
+ push(runner, value: nil)
232
+ run_until_drained
233
+ [result, body_error]
234
+ ensure
235
+ @live.delete(runner) if runner
236
+ end
237
+
238
+ def run_until_drained
239
+ @loop_thread = ::Thread.current
240
+ until idle?
241
+ run_batch
242
+ break if idle?
243
+
244
+ select_or_wait
245
+ end
246
+ end
247
+
248
+ def idle?
249
+ @mutex.synchronize { @runnable.empty? && @parked.empty? } &&
250
+ @timers.empty? && @io_waiters.empty?
251
+ end
252
+
253
+ # Idempotent teardown. Never raises (mirrors Platform::IoDrain):
254
+ # stops the DNS pool, joins process-wait threads with a bounded
255
+ # timeout, kills stragglers, closes the self-pipe, and reports any
256
+ # pending work left behind by an abandoned drain instead of hiding it.
257
+ def shutdown
258
+ waiters = nil
259
+ @mutex.synchronize do
260
+ return if @closed
261
+
262
+ @closed = true
263
+ waiters = @waiter_threads.dup
264
+ @waiter_threads.clear
265
+ end
266
+ @dns_pool&.shutdown
267
+ waiters.each do |thread|
268
+ thread.join(0.1)
269
+ thread.kill if thread.alive?
270
+ rescue StandardError
271
+ nil
272
+ end
273
+ leak = nil
274
+ @mutex.synchronize do
275
+ leak = { runnable: @runnable.size, parked: @parked.size, timers: @timers.size,
276
+ io_waiters: @io_waiters.size, live: @live.size }
277
+ end
278
+ Rubik::Log.warn(:concurrency, "event loop closed with pending work: #{leak.inspect}") if leak.values.sum.positive?
279
+ [@wake_read, @wake_write].each do |io|
280
+ io.close unless io.closed?
281
+ rescue StandardError
282
+ nil
283
+ end
284
+ end
285
+
286
+ private
287
+
288
+ # Shared park sequence: bump the generation (invalidating older stale
289
+ # wakeups), register the caller's timers, yield, and either re-raise a
290
+ # delivered exception or return the resume value.
291
+ def park_fiber
292
+ fiber = ::Fiber.current
293
+ gen = (@gen[fiber] += 1)
294
+ yield fiber, gen
295
+ value = ::Fiber.yield
296
+ raise value if value.is_a?(::Exception)
297
+
298
+ value
299
+ end
300
+
301
+ def run_batch
302
+ while (item = @mutex.synchronize { @runnable.shift })
303
+ fiber, value = item
304
+ # A resumed fiber is definitionally no longer parked; dropping the
305
+ # registration here keeps interrupt-style wakes from leaking parks.
306
+ # Bumping the park generation invalidates every OTHER outstanding
307
+ # registration the fiber still holds (io_wait timeout timer, io
308
+ # interests) so dead entries cannot gate draining.
309
+ @mutex.synchronize { @parked.delete(fiber) }
310
+ @gen[fiber] += 1
311
+ drop_io_interest(fiber)
312
+ begin
313
+ fiber.resume(value)
314
+ rescue ::FiberError
315
+ nil # stale wakeup for a finished fiber
316
+ rescue StandardError => e
317
+ # A failure of the resume machinery itself (not the fiber's block
318
+ # — those are captured in spawn_fiber): record it so the
319
+ # boundary's raise policy surfaces it instead of only a log line.
320
+ Rubik::Log.warn(:concurrency, "fiber resume failed: #{e.class}: #{e.message} @ #{(e.backtrace || []).first}")
321
+ @spawn_errors << [@next_ordinal += 1, e]
322
+ end
323
+ end
324
+ end
325
+
326
+ def select_or_wait
327
+ read_set = @io_waiters.keys
328
+ write_set = @io_waiters.each_with_object([]) do |(io, slot), acc|
329
+ acc << io unless slot[:write].empty?
330
+ end
331
+ read_set << @wake_read
332
+ timeout = @timers.next_gap(monotonic)
333
+
334
+ ready = safe_select(read_set, write_set, timeout)
335
+ return if ready == :retry
336
+
337
+ drain_wake_pipe
338
+ if ready
339
+ ready_read, ready_write = ready
340
+ dispatch_ready(ready_read, ready_write)
341
+ end
342
+ fire_due_timers
343
+ end
344
+
345
+ # A closed io among the watched sets makes IO.select raise; prune the
346
+ # offender and let the caller loop again (sentinel :retry). When
347
+ # nothing is prunable the loop would spin hot forever — fail loud and
348
+ # let the boundary's raise policy surface it instead.
349
+ def safe_select(read_set, write_set, timeout)
350
+ ::IO.select(read_set, write_set, nil, timeout)
351
+ rescue IOError, Errno::EBADF, Errno::EINVAL => e
352
+ pruned = prune_closed_ios((read_set + write_set).uniq)
353
+ return :retry if pruned.positive?
354
+
355
+ Rubik::Log.warn(:concurrency, "io_select failed with nothing prunable (#{e.class}: #{e.message}; " \
356
+ "watching #{read_set.size + write_set.size} ios)")
357
+ raise Error, "event loop io_select failed: #{e.class}: #{e.message}"
358
+ end
359
+
360
+ def fire_due_timers
361
+ @timers.fire_due!(monotonic) do |fiber, value, gen|
362
+ next if @gen[fiber] != gen # stale: fiber moved on after registering
363
+
364
+ drop_io_interest(fiber)
365
+ push(fiber, value: value)
366
+ end
367
+ end
368
+
369
+ def dispatch_ready(ready_read, ready_write)
370
+ ready_read.each { |io| wake_io(io, READABLE) }
371
+ ready_write.each { |io| wake_io(io, WRITABLE) }
372
+ end
373
+
374
+ def wake_io(io, events)
375
+ slot = @io_waiters[io]
376
+ return unless slot
377
+
378
+ waiters = slot[:read].merge(slot[:write])
379
+ waiters.each do |fiber, gen|
380
+ if @gen[fiber] != gen
381
+ # Stale interest (the fiber timed out or moved on): remove it so
382
+ # a never-ready io cannot keep the loop from draining.
383
+ slot[:read].delete(fiber)
384
+ slot[:write].delete(fiber)
385
+ next
386
+ end
387
+
388
+ # Drop both interest directions for this fiber; if it waits again
389
+ # it re-registers with a fresh generation.
390
+ slot[:read].delete(fiber)
391
+ slot[:write].delete(fiber)
392
+ push(fiber, value: events)
393
+ end
394
+ @io_waiters.delete(io) if slot[:read].empty? && slot[:write].empty?
395
+ end
396
+
397
+ def drop_io_interest(fiber)
398
+ @io_waiters.each do |io, slot|
399
+ next unless slot[:read].key?(fiber) || slot[:write].key?(fiber)
400
+
401
+ slot[:read].delete(fiber)
402
+ slot[:write].delete(fiber)
403
+ @io_waiters.delete(io) if slot[:read].empty? && slot[:write].empty?
404
+ end
405
+ end
406
+
407
+ # Removes closed ios from the interest registry, waking their waiting
408
+ # fibers with a 0 mask (their next nonblocking attempt sees the truth).
409
+ # Returns the number of ios pruned.
410
+ def prune_closed_ios(ios)
411
+ pruned = 0
412
+ ios.each do |io|
413
+ next unless io.closed?
414
+
415
+ slot = @io_waiters.delete(io)
416
+ next unless slot
417
+
418
+ (slot[:read].keys + slot[:write].keys).each { |fiber| push(fiber, value: 0) }
419
+ pruned += 1
420
+ end
421
+ pruned
422
+ end
423
+
424
+ def drain_wake_pipe
425
+ nil while @wake_read.read_nonblock(4096, exception: false).is_a?(String)
426
+ rescue IOError, Errno::EBADF
427
+ nil
428
+ end
429
+
430
+ def kick
431
+ @wake_write.write_nonblock(".", exception: false)
432
+ rescue StandardError
433
+ nil # pipe full (a wake byte is already pending) or closing
434
+ end
435
+
436
+ def monotonic
437
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
438
+ end
439
+ end
440
+ end
441
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Concurrency
5
+ # Bounded thread-join mechanics (arch-deepening 2026-10-08 #12): the
6
+ # monotonic-deadline polling join previously hand-mirrored in
7
+ # mcp/manager.rb#join_boot_safely and repl/ast_warmup.rb#join_indexer_safely.
8
+ # Mechanics only — the caller passes the thread handle and keeps its own
9
+ # log channel and timeout-exhaustion policy; this module never logs and
10
+ # never raises (a Thread#join failure is RETURNED, the error-as-data
11
+ # precedent from ProcessWaiter).
12
+ module Join
13
+ # Poll cadence: short enough that teardown feels immediate, long
14
+ # enough to never spin the GVL.
15
+ POLL_INTERVAL = 0.05
16
+
17
+ module_function
18
+
19
+ # Joins +handle+ and reports the outcome:
20
+ # :no_thread nil handle (caller-level "nothing to join")
21
+ # :joined thread finished inside the bound
22
+ # :timeout Numeric timeout: deadline lapsed, thread still alive
23
+ # StandardError the thread died raising — RETURNED, never raised
24
+ # (caller owns the log). A crash BEFORE the first
25
+ # poll is surfaced too: the final non-blocking join
26
+ # observes a dead-with-exception thread that no
27
+ # loop iteration ever joined.
28
+ # timeout: nil joins unboundedly, polling at POLL_INTERVAL. The handle
29
+ # is captured ONCE at call time — callers that must not hold a lock
30
+ # while joining read the handle under their lock first (G5) and pass
31
+ # it in.
32
+ def thread(handle, timeout: nil)
33
+ return :no_thread unless handle
34
+
35
+ if timeout
36
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
37
+ while handle.alive?
38
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
39
+ break if remaining <= 0
40
+
41
+ handle.join([remaining, POLL_INTERVAL].min)
42
+ end
43
+ return :timeout if handle.alive?
44
+ else
45
+ handle.join(POLL_INTERVAL) while handle.alive?
46
+ end
47
+
48
+ handle.join(0)
49
+ :joined
50
+ rescue StandardError => e
51
+ e
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ require_relative "errors"
5
+
6
+ module Rubik
7
+ module Concurrency
8
+ # process_wait hook body. Fast path: a WNOHANG poll satisfies callers
9
+ # that only probe (Process.detach, waitpid(WNOHANG)) without a thread.
10
+ # Blocking path: one short-lived waiter thread per waited pid doing a
11
+ # blocking Process.wait — the fiber's Thread#join on it parks through the
12
+ # block/unblock hooks, so no fiber-visible machinery is needed. Never
13
+ # waits on -1 (that would race Open3's per-child wait_thr reapers).
14
+ #
15
+ # Errno::ECHILD means someone else (usually the Open3 wait thread that
16
+ # spawned the child) reaped the pid first; returning nil forever would
17
+ # make the VM spin on the dead pid, so a truthy StatusLike stands in.
18
+ module ProcessWaiter
19
+ module_function
20
+
21
+ def wait(pid, flags, loop)
22
+ # Process.wait returns the PID (Integer); the Process::Status the VM
23
+ # requires as the hook's return lives in $? after the call. ECHILD
24
+ # means another reaper (usually Open3's wait_thr) got the status
25
+ # first; a StatusLike with a NIL exitstatus stands in — truthy for
26
+ # the VM's done-check without fabricating a success/failure code.
27
+ if flags.anybits?(::Process::WNOHANG)
28
+ begin
29
+ reaped = ::Process.wait(pid, flags)
30
+ return reaped.nil? ? nil : $CHILD_STATUS
31
+ rescue Errno::ECHILD
32
+ return Errors::StatusLike.new(pid: pid, exitstatus: nil)
33
+ end
34
+ end
35
+
36
+ waiter = ::Thread.new do
37
+ ::Process.wait(pid)
38
+ $CHILD_STATUS
39
+ rescue Errno::ECHILD
40
+ Errors::StatusLike.new(pid: pid, exitstatus: nil)
41
+ rescue StandardError => e
42
+ e
43
+ end
44
+ loop.track_waiter(waiter)
45
+ value = waiter.value
46
+ # Waiter failure arrives as the thread value; the VM expects a
47
+ # Process::Status-shaped return, so RAISE the original error here —
48
+ # returning it as data would surface as an opaque TypeError.
49
+ raise value if value.is_a?(::Exception)
50
+
51
+ value
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "event_loop"
4
+ require_relative "process_waiter"
5
+
6
+ module Rubik
7
+ module Concurrency
8
+ # Adapter for the VM's Fiber::Scheduler hook protocol — stateless; all
9
+ # state lives on the EventLoop. Ruby 3.3 ↔ 4.0 protocol drift is
10
+ # concentrated here (probes against 4.0.6, plan §2):
11
+ #
12
+ # fiber 4.0: fiber(&blk) — scheduler creates and returns the fiber.
13
+ # 3.3: fiber(fiber, block) — VM pre-created it.
14
+ # io_read integer args differ (3.3 passes length; 4.0 passes 0, 0)
15
+ # io_write but the buffer is always sized to the operation, so both
16
+ # hooks drive off buffer.size — see EventLoop#read_io.
17
+ #
18
+ # io_read/io_write are required on 4.0: without them a plain IO#read
19
+ # inside a scheduled fiber falls back to a whole-thread BLOCKING read
20
+ # (measured: 20 × 0.2 s child pipes serialized into 13 s). The buffer is
21
+ # filled/drained with read_nonblock/write_nonblock + IO::Buffer views —
22
+ # IO#nonblock= and IO::Buffer#read(io,...) are NOT usable on Windows
23
+ # builds (NoMethodError / constant-zero reads, verified), while the
24
+ # nonblock(…, exception: false) family works for pipes and sockets alike.
25
+ class Scheduler
26
+ attr_reader :loop
27
+
28
+ def initialize(loop)
29
+ @loop = loop
30
+ end
31
+
32
+ def fiber(*args, &blk)
33
+ blk ? @loop.spawn_fiber(&blk) : args[0]
34
+ end
35
+
36
+ def fiber_interrupt(target, exception = nil)
37
+ @loop.interrupt(target, exception)
38
+ nil
39
+ end
40
+
41
+ def io_wait(io, events, timeout)
42
+ @loop.wait_io(io, events, timeout)
43
+ end
44
+
45
+ # The integer args differ per protocol (3.3: length; 4.0. offset/length
46
+ # both zero) but the buffer is always sized to the operation, so the
47
+ # EventLoop drives both hooks off buffer.size — see read_io/write_io.
48
+ def io_read(io, buffer, *_protocol_args)
49
+ @loop.read_io(io, buffer)
50
+ end
51
+
52
+ def io_write(io, buffer, *_protocol_args)
53
+ @loop.write_io(io, buffer)
54
+ end
55
+
56
+ def kernel_sleep(duration = nil)
57
+ duration ? @loop.nap(duration) : @loop.park(:sleep)
58
+ true
59
+ end
60
+
61
+ def block(blocker, timeout = nil)
62
+ @loop.park(blocker, timeout)
63
+ end
64
+
65
+ def unblock(blocker, fiber)
66
+ @loop.unpark(blocker, fiber)
67
+ nil
68
+ end
69
+
70
+ def process_wait(pid, flags)
71
+ ProcessWaiter.wait(pid, flags, @loop)
72
+ end
73
+
74
+ def address_resolve(hostname)
75
+ @loop.resolve_address(hostname)
76
+ end
77
+
78
+ def scheduler_close
79
+ @loop.shutdown
80
+ nil
81
+ end
82
+ end
83
+ end
84
+ end