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,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Pure fiber ordering for Runtime::Manager (G4 extraction): Kahn's algorithm
4
+ # over provides/injects/requires with legacy-alias canonicalization. Returns
5
+ # [sorted, cycled] — cycle failure REPORTING stays in the Manager (it touches
6
+ # the context's dmesg service); this module only computes the order.
7
+ module Rubik
8
+ module Runtime
9
+ module FiberOrdering
10
+ module_function
11
+
12
+ def sort(fibers)
13
+ return [fibers, []] if fibers.size <= 1
14
+
15
+ provider_map = {}
16
+ fibers.each do |f|
17
+ (f.respond_to?(:provides) ? f.provides : [f.name.downcase]).each do |k|
18
+ (provider_map[k] ||= []) << f
19
+ canon = Rubik::Coeffects.canonical(k)
20
+ (provider_map[canon] ||= []) << f if canon && canon != k
21
+ end
22
+ end
23
+
24
+ dependents = Hash.new { |h, k| h[k] = Set.new }
25
+ in_degree = Hash.new(0)
26
+ fibers.each do |consumer|
27
+ needed = (Array(consumer.inject) + Array(consumer.requires)).map { |d| d.to_s.downcase }
28
+ prereqs = Set.new
29
+ needed.each do |k|
30
+ providers = (provider_map[k] || []) + (provider_map[Rubik::Coeffects.canonical(k)] || [])
31
+ providers.each { |p| prereqs << p if p != consumer && fibers.include?(p) }
32
+ end
33
+ in_degree[consumer] = prereqs.size
34
+ prereqs.each { |p| dependents[p] << consumer }
35
+ end
36
+
37
+ queue = fibers.select { |f| in_degree[f].zero? }
38
+ sorted = []
39
+ until queue.empty?
40
+ curr = queue.shift
41
+ sorted << curr
42
+ dependents[curr].each do |dep|
43
+ in_degree[dep] -= 1
44
+ queue << dep if in_degree[dep].zero?
45
+ end
46
+ end
47
+
48
+ [sorted, fibers - sorted]
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Failed-fiber retry gate (G4 extraction from Runtime::Manager; review
4
+ # 2026-09-17, Finding 3): a permanently-broken factory used to re-run on EVERY
5
+ # reconcile_all (each provide/register). Failures back off exponentially and
6
+ # stop after MAX_RETRIES attempts; a successful activation or a fresh
7
+ # register_fiber clears the bookkeeping.
8
+ #
9
+ # The backoff sleep must NOT run while holding the Manager Monitor: the sleep
10
+ # happens with the monitor fully released (depth 0) so it never serializes
11
+ # other threads; bookkeeping is committed atomically under it first, and the
12
+ # factory activation re-acquires the lock (depth 1) afterwards.
13
+ module Rubik
14
+ module Runtime
15
+ class FiberRetryGate
16
+ MAX_RETRIES = 5
17
+ BACKOFF_BASE_S = 0.05
18
+
19
+ def initialize(monitor, context)
20
+ @monitor = monitor
21
+ @context = context
22
+ @bookkeeping = {} # name -> { attempts:, last_attempt_at:, warned: }
23
+ end
24
+
25
+ # Explicit re-registration / unregistration resets the retry budget.
26
+ def clear(name)
27
+ @monitor.synchronize { @bookkeeping.delete(name) }
28
+ end
29
+
30
+ # No bookkeeping outlives the manager (fresh-eye R2).
31
+ def clear_all
32
+ @bookkeeping.clear
33
+ end
34
+
35
+ def attempt(fiber)
36
+ delay = 0.0
37
+ elapsed = 0.0
38
+ @monitor.synchronize do
39
+ book = @bookkeeping[fiber.name] ||= { attempts: 0, last_attempt_at: nil }
40
+ if book[:attempts] >= MAX_RETRIES
41
+ warn_budget_exhausted(fiber, book) # atomic, one-shot, stays under the lock
42
+ return
43
+ end
44
+
45
+ prev_attempts = book[:attempts]
46
+ ts_prev = book[:last_attempt_at]
47
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
48
+ delay = ts_prev ? BACKOFF_BASE_S * (2**prev_attempts) : 0.0
49
+ elapsed = ts_prev ? (now - ts_prev) : Float::INFINITY
50
+ book[:attempts] += 1
51
+ book[:last_attempt_at] = now
52
+ end
53
+
54
+ # Sleep happens with the monitor FULLY released (depth 0) so it never
55
+ # serializes other threads; bookkeeping is already committed above.
56
+ sleep(delay - elapsed) if delay > elapsed
57
+
58
+ # Re-acquire (depth 1) and run the factory: the failing fiber is not in
59
+ # the manager's activation marker, so the reentrancy guard lets this
60
+ # re-activate it. A successful activation clears its bookkeeping.
61
+ @monitor.synchronize do
62
+ yield fiber
63
+ @bookkeeping.delete(fiber.name) if fiber.state == :active
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # One-shot visibility when a fiber's retry budget is exhausted (fresh-eye
70
+ # R3): the old always-retry behavior was noisy but visible; without this
71
+ # warn a permanently-broken factory would sink silently into :failed.
72
+ # Warns exactly once per budget cycle — re-registration resets the flag.
73
+ def warn_budget_exhausted(fiber, book)
74
+ return if book[:warned]
75
+
76
+ book[:warned] = true
77
+ last = fiber.last_error
78
+ Rubik::Log.warn(:runtime, "fiber '#{fiber.name}' retry budget exhausted " \
79
+ "(#{book[:attempts]} attempts, MAX_FIBER_RETRIES=#{MAX_RETRIES}) — " \
80
+ "staying :failed; re-register to retry. Last error: #{last&.class}: #{last&.message}")
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require_relative "../coeffects"
5
+ require_relative "service_registration"
6
+
7
+ module Rubik
8
+ module Runtime
9
+ module IndexService
10
+ module_function
11
+
12
+ def register(context_or_manager, index)
13
+ ServiceRegistration.call(context_or_manager) do |ctx|
14
+ next index unless ctx
15
+
16
+ ctx.provide(Rubik::Coeffects::CONTEXT_INDEX, index)
17
+ index
18
+ end
19
+ end
20
+
21
+ def sync_roots(context_or_manager, index, additional_roots:, ast_graph: nil, cache_store: nil)
22
+ return unless index
23
+
24
+ index.additional_roots = Array(additional_roots)
25
+ index.full_scan
26
+
27
+ context_or_manager.reconcile_all if context_or_manager.respond_to?(:reconcile_all)
28
+
29
+ if ast_graph
30
+ root_d = index.root_dir
31
+
32
+ # Re-index all supported files known to the context index.
33
+ # Rely on File.exist?(full_path) as the sole guard: a file
34
+ # already in the graph simply re-indexes harmlessly.
35
+ index.files.each_key do |rel_file|
36
+ full_path = File.expand_path(rel_file, root_d)
37
+ ext = File.extname(rel_file).downcase
38
+ if defined?(Rubik::AST::Extractor) && Rubik::AST::Extractor.supported_extensions.include?(ext) &&
39
+ File.exist?(full_path)
40
+ ast_graph.index_file(full_path)
41
+ end
42
+ end
43
+
44
+ if cache_store && File.directory?(root_d)
45
+ begin
46
+ ast_graph.save_to_cache(cache_store)
47
+ rescue => e
48
+ Rubik::Log.warn(:ast, "AST graph cache save failed (#{e.class}: #{e.message}); stale cache retained")
49
+ nil
50
+ end
51
+ end
52
+ end
53
+
54
+ index
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "fiber"
4
+ require_relative "../coeffects"
5
+ require_relative "service_registration"
6
+
7
+ module Rubik
8
+ module Runtime
9
+ class AgentLoopFiber < ComponentFiber
10
+ attr_reader :shell
11
+
12
+ def initialize(name: "orchestrator_loop", inject: ["llm_client", "tool_registry", "buffer_manager"], config: {}, shell: nil, &factory)
13
+ @shell = shell
14
+ super(name: name, inject: inject, config: config, &factory)
15
+ end
16
+
17
+ def build_instance(sub_context, config)
18
+ LoopService.build_loop(sub_context, shell: @shell, config: config)
19
+ end
20
+ end
21
+
22
+ module LoopService
23
+ module_function
24
+
25
+ def register(runtime_manager_or_context, shell: nil, name: "orchestrator_loop", inject: ["llm_client", "tool_registry", "buffer_manager"], config: {})
26
+ fiber = AgentLoopFiber.new(name: name, inject: inject, config: config, shell: shell)
27
+ # Fiber registration is not a plain provide: Manager#register_fiber
28
+ # reconciles itself, so the spine's trailing reconcile is suppressed.
29
+ ServiceRegistration.call(runtime_manager_or_context, reconcile: false) do |ctx|
30
+ if runtime_manager_or_context.respond_to?(:register_fiber)
31
+ runtime_manager_or_context.register_fiber(fiber)
32
+ elsif ctx && fiber.satisfiable?(ctx)
33
+ fiber.activate!(ctx)
34
+ end
35
+ fiber
36
+ end
37
+ end
38
+
39
+ def build_loop(context, shell: nil, config: {})
40
+ ctx = context.respond_to?(:context) ? context.context : context
41
+ provider = config[:provider] || (ctx.respond_to?(:get) ? Rubik::Coeffects.get(ctx, Rubik::Coeffects::LLM_CLIENT) : nil)
42
+ tool_registry = config[:tool_registry] || (if ctx.respond_to?(:get)
43
+ Rubik::Coeffects.get(ctx,
44
+ Rubik::Coeffects::TOOL_REGISTRY)
45
+ end) || shell&.build_tool_registry
46
+ app_config = config[:config] || shell&.config ||
47
+ (ctx.respond_to?(:get) ? Rubik::Coeffects.get(ctx, Rubik::Coeffects::CONFIG) : nil)
48
+
49
+ max_steps = config[:max_steps] || (app_config.respond_to?(:fetch) ? app_config.fetch("agent.max_steps", 40).to_i : 40)
50
+ warn_ratio = config[:warn_at_ratio] || (app_config.respond_to?(:fetch) ? app_config.fetch("agent.warn_at_ratio", 0.75).to_f : 0.75)
51
+ model = config[:model]
52
+ on_step = config[:on_step]
53
+ context_limit = config[:context_limit]
54
+ max_output_tokens = config[:max_output_tokens] ||
55
+ (app_config.respond_to?(:fetch) && app_config.fetch("llm.default_max_output_tokens", 4_096)&.to_i) || 4_096
56
+
57
+ require_relative "../orchestrator"
58
+
59
+ Rubik::Orchestrator::Loop.new(
60
+ provider: provider,
61
+ tool_registry: tool_registry,
62
+ max_steps: max_steps,
63
+ warn_at_ratio: warn_ratio,
64
+ model: model,
65
+ shell: shell,
66
+ context_limit: context_limit,
67
+ max_output_tokens: max_output_tokens,
68
+ on_step: on_step
69
+ )
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+ require_relative "context"
5
+ require_relative "fiber"
6
+ require_relative "fiber_ordering"
7
+ require_relative "fiber_retry_gate"
8
+
9
+ module Rubik
10
+ module Runtime
11
+ class Manager
12
+ attr_reader :context, :retry_gate
13
+
14
+ def initialize(context = nil)
15
+ @context = context || Context.new
16
+ @fibers = {}
17
+ @monitor = Monitor.new
18
+ # Fiber names with a factory currently in flight (reentrant reconcile
19
+ # guard): a factory that re-provides its own dependency re-enters
20
+ # reconcile_all on the same thread; without this marker the pending/
21
+ # active branches would re-activate the fiber and recurse unbounded
22
+ # (Monitor is reentrant, so the spiral ends in SystemStackError, not
23
+ # a deadlock).
24
+ @activating = {}
25
+ # Failed-fiber retry budget/backoff (G4): the gate owns the
26
+ # bookkeeping; the manager routes clear/attempt through it.
27
+ @retry_gate = FiberRetryGate.new(@monitor, @context)
28
+ # name -> Set of canonical coeffect keys this fiber's factory injects.
29
+ # The reactive surface (provide/unprovide) consults this to decide which
30
+ # fibers are gated on a coeffect that changed, so retracting a service
31
+ # through the reactive path retracts (deactivates) every fiber gated on
32
+ # it — matching provide's on-demand reactivation (AGENTS.md §5, Finding B).
33
+ @fiber_deps = {}
34
+ @disposed = false
35
+ end
36
+
37
+ # Retry-policy constants moved to FiberRetryGate (G4); aliases keep the
38
+ # Manager-facing names (tests, warn-message text) stable.
39
+ MAX_FIBER_RETRIES = FiberRetryGate::MAX_RETRIES
40
+ RETRY_BACKOFF_BASE_S = FiberRetryGate::BACKOFF_BASE_S
41
+
42
+ def fibers
43
+ @monitor.synchronize { @fibers.dup }
44
+ end
45
+
46
+ def disposed?
47
+ @monitor.synchronize { @disposed }
48
+ end
49
+
50
+ def register_component(name, inject: [], requires: [], provides: [], config: {}, &factory)
51
+ name_str = name.to_s
52
+ fiber = ComponentFiber.new(name: name_str, inject: inject, requires: requires, provides: provides, config: config, &factory)
53
+ register_fiber(fiber)
54
+ end
55
+
56
+ # Reentrant lock safety: Monitor#synchronize is explicitly reentrant (recursive),
57
+ # allowing register_fiber to store the fiber and activate it. The activation
58
+ # is performed OUTSIDE the lock (via reconcile_fiber) so a failing fiber's
59
+ # backoff sleep never serializes other threads (Finding 3).
60
+ def register_fiber(fiber)
61
+ deps = Array(fiber.inject).map { |n| n.to_s.downcase }
62
+ @monitor.synchronize do
63
+ raise ContextDisposedError, "Cannot register fiber on a disposed Manager" if @disposed
64
+
65
+ @fiber_deps.delete(fiber.name) unless deps.empty?
66
+ @fiber_deps[fiber.name] = deps.to_set unless deps.empty?
67
+ @fibers[fiber.name] = fiber
68
+ @retry_gate.clear(fiber.name) # explicit re-registration resets the retry budget
69
+ end
70
+ reconcile_fiber(fiber)
71
+ fiber
72
+ end
73
+
74
+ def unregister_component(name)
75
+ @monitor.synchronize do
76
+ name_str = name.to_s
77
+ fiber = @fibers.delete(name_str)
78
+ @fiber_deps.delete(name_str)
79
+ @retry_gate.clear(name_str) # fresh-eye R2: disposed fibers must not leak their retry budget
80
+ fiber&.dispose!
81
+ end
82
+ end
83
+
84
+ # Reactive retraction (AGENTS.md §5, Finding B): unlike Context#unprovide
85
+ # (which removes a service from the coeffect registry without touching
86
+ # dependents), retraction here routes through reconcile_all(changed_service:).
87
+ # Any fiber whose factory injects this coeffect therefore transitions
88
+ # Active->Pending and its partial inverse runs (LIFO via its sub-context),
89
+ # instead of orphaning against a now-stale/coeffect-less context. Re-
90
+ # providing the key afterwards is symmetric to provide: the fiber is
91
+ # reactivated on-demand.
92
+ def unprovide(service_name)
93
+ key = service_name.to_s.downcase
94
+ @monitor.synchronize { @context.unprovide(key) }
95
+ reconcile_all(changed_service: key)
96
+ nil # idempotent no-op returns nil (unprovide has no meaningful return)
97
+ end
98
+
99
+ # Reactive provide (AGENTS.md §5): adding a coeffect activates any pending
100
+ # fiber gated on it, and reactivates an active fiber when the SAME service
101
+ # object is re-provided (identity change), so its new instance is injected.
102
+ # Both transitions run reconcile_all(changed_service:) OUTSIDE the monitor.
103
+ def provide(service_name, service)
104
+ @monitor.synchronize { @context.provide(service_name, service) }
105
+ # reconcile_all runs OUTSIDE this lock so a failing fiber's backoff sleep
106
+ # never serializes other threads (Finding 3).
107
+ reconcile_all(changed_service: service_name)
108
+ end
109
+
110
+ # Reentrant lock safety: Monitor#synchronize is explicitly reentrant (recursive),
111
+ # allowing swap_component to safely invoke unregister_component and register_component
112
+ # from within the same thread without deadlocking.
113
+ def swap_component(name, inject: [], requires: [], provides: [], config: {}, &new_factory)
114
+ @monitor.synchronize do
115
+ name_str = name.to_s
116
+ unregister_component(name_str)
117
+ register_component(name_str, inject: inject, requires: requires, provides: provides, config: config, &new_factory)
118
+ end
119
+ end
120
+
121
+ def reconcile_all(changed_service: nil)
122
+ fibers = @monitor.synchronize { @fibers.values.dup }
123
+ sorted_fibers = topological_sort_fibers(fibers)
124
+ sorted_fibers.each do |fiber|
125
+ reconcile_fiber(fiber, changed_service: changed_service)
126
+ end
127
+ end
128
+
129
+ def topological_sort_fibers(fibers)
130
+ sorted, cycled = FiberOrdering.sort(fibers)
131
+ handle_fiber_cycle(cycled) unless cycled.empty?
132
+ sorted
133
+ end
134
+
135
+ def handle_fiber_cycle(cycle_fibers)
136
+ names = cycle_fibers.map(&:name).join(", ")
137
+ msg = "Dependency cycle detected among fibers: #{names}"
138
+ Rubik::Log.warn(:runtime, msg)
139
+ dmesg = begin
140
+ @context.resolve(Rubik::Coeffects::DMESG)
141
+ rescue StandardError
142
+ nil
143
+ end
144
+ dmesg&.log(:runtime, :err, msg) if dmesg.respond_to?(:log)
145
+
146
+ err = StandardError.new(msg)
147
+ @monitor.synchronize do
148
+ cycle_fibers.each { |cf| cf.mark_failed!(err) if cf.respond_to?(:mark_failed!) }
149
+ end
150
+ end
151
+
152
+ def reconcile_fiber(fiber, changed_service: nil)
153
+ # pending / active branches activate with the monitor held at depth 1
154
+ # (the factory returns while we still hold it, so nested reconciles see
155
+ # the newest value). The :failed branch defers activation until AFTER
156
+ # the monitor is released so the backoff sleep serializes no one.
157
+ deferred = nil
158
+ @monitor.synchronize do
159
+ # Reentrancy guard: skip fibers whose factory is currently running
160
+ # (nested reconcile from a factory-side provide). The in-flight
161
+ # factory already observes the newest value it provided, so the
162
+ # outer activation converges; a skipped re-run would only recurse.
163
+ return if @activating.key?(fiber.name)
164
+
165
+ case fiber.state
166
+ when :pending
167
+ with_activation_marker(fiber) { fiber.activate!(@context) } if fiber.ready_to_activate?(@context)
168
+ when :active
169
+ if !fiber.ready_to_activate?(@context)
170
+ fiber.deactivate!
171
+ elsif changed_service && fiber_depends_on_service?(fiber, changed_service.to_s.downcase)
172
+ with_activation_marker(fiber) { fiber.reactivate!(@context) }
173
+ end
174
+ when :failed
175
+ deferred = fiber if fiber.ready_to_activate?(@context)
176
+ end
177
+ end
178
+
179
+ # Monitor is now fully released (depth 0); the deferred retry's backoff
180
+ # sleep therefore runs with no lock held.
181
+ return unless deferred
182
+
183
+ @retry_gate.attempt(deferred) do |fiber|
184
+ with_activation_marker(fiber) { fiber.activate!(@context) }
185
+ end
186
+ end
187
+
188
+ def active_fibers
189
+ @monitor.synchronize { @fibers.values.select { |f| f.state == :active } }
190
+ end
191
+
192
+ def pending_fibers
193
+ @monitor.synchronize { @fibers.values.select { |f| f.state == :pending } }
194
+ end
195
+
196
+ def failed_fibers
197
+ @monitor.synchronize { @fibers.values.select { |f| f.state == :failed } }
198
+ end
199
+
200
+ def dispose!
201
+ @monitor.synchronize do
202
+ return if @disposed
203
+
204
+ @disposed = true
205
+ begin
206
+ @fibers.values.reverse_each do |fiber|
207
+ fiber.dispose!
208
+ rescue StandardError => e
209
+ Rubik::Log.warn(:runtime, "fiber '#{fiber.name}' dispose failed: #{e.class}: #{e.message}")
210
+ end
211
+ ensure
212
+ @fibers.clear
213
+ @retry_gate.clear_all # fresh-eye R2: no bookkeeping outlives the manager
214
+ @context.dispose
215
+ end
216
+ end
217
+ end
218
+
219
+ private
220
+
221
+ # Marks the fiber while its factory runs so nested reconciles skip it;
222
+ # ensure-based so a raising factory (→ :failed) never wedges the marker.
223
+ def with_activation_marker(fiber)
224
+ @activating[fiber.name] = true
225
+ begin
226
+ yield
227
+ ensure
228
+ @activating.delete(fiber.name)
229
+ end
230
+ end
231
+
232
+ # A fiber depends on a changed service when it injects the literal key
233
+ # or a legacy alias of it (Rubik::Coeffects SSOT), so alias-injected
234
+ # fibers react to canonical re-provides instead of holding stale
235
+ # service instances.
236
+ def fiber_depends_on_service?(fiber, changed_key)
237
+ fiber.inject.any? { |dep| dep == changed_key || Rubik::Coeffects.canonical(dep) == changed_key }
238
+ end
239
+ end
240
+ end
241
+ end