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,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../platform"
4
+
5
+ module Rubik
6
+ module Runtime
7
+ class ExecutionFailure
8
+ attr_reader :cmd, :exit_code, :stdout, :stderr, :duration, :category, :suggestion, :metadata, :timestamp
9
+
10
+ def initialize(cmd:, exit_code:, stdout: "", stderr: "", duration: 0.0, category: :unknown_failure, suggestion: nil, metadata: {})
11
+ @cmd = cmd.to_s
12
+ @exit_code = exit_code.to_i
13
+ @stdout = stdout.to_s
14
+ @stderr = stderr.to_s
15
+ @duration = duration.to_f
16
+ @category = category.to_sym
17
+ @suggestion = suggestion.to_s
18
+ @metadata = metadata || {}
19
+ @timestamp = begin
20
+ Time.now.utc.iso8601
21
+ rescue NoMethodError
22
+ Time.now.to_s
23
+ end
24
+ end
25
+
26
+ def to_h
27
+ {
28
+ cmd: @cmd,
29
+ exit_code: @exit_code,
30
+ stdout: @stdout,
31
+ stderr: @stderr,
32
+ duration: @duration,
33
+ category: @category,
34
+ suggestion: @suggestion,
35
+ metadata: @metadata,
36
+ timestamp: @timestamp
37
+ }
38
+ end
39
+
40
+ def format_for_llm
41
+ lines = [
42
+ "=== Runtime Execution Diagnostic ===",
43
+ "• Command : #{@cmd}",
44
+ "• Exit Code : #{@exit_code}",
45
+ "• Duration : #{@duration.round(4)}s",
46
+ "• Category : #{@category.to_s.upcase}"
47
+ ]
48
+ lines << "• Stderr : #{@stderr.strip}" unless @stderr.strip.empty?
49
+ lines << "• Stdout : #{@stdout.strip}" if @stderr.strip.empty? && !@stdout.strip.empty?
50
+ lines << "• Suggestion : #{@suggestion}" unless @suggestion.strip.empty?
51
+ lines.join("\n")
52
+ end
53
+ end
54
+
55
+ module DiagnosticEngine
56
+ class << self
57
+ def diagnose(cmd:, exit_code:, stdout: "", stderr: "", duration: 0.0, metadata: {})
58
+ cmd_str = cmd.to_s
59
+ stderr_str = stderr.to_s
60
+ stdout_str = stdout.to_s
61
+ combined = "#{stderr_str}\n#{stdout_str}"
62
+
63
+ category = determine_category(cmd_str, exit_code, stderr_str, stdout_str, combined)
64
+ suggestion = generate_suggestion(category, cmd_str, combined)
65
+
66
+ ExecutionFailure.new(
67
+ cmd: cmd_str,
68
+ exit_code: exit_code,
69
+ stdout: stdout_str,
70
+ stderr: stderr_str,
71
+ duration: duration,
72
+ category: category,
73
+ suggestion: suggestion,
74
+ metadata: metadata
75
+ )
76
+ end
77
+
78
+ def analyze_process_result(process_result, cmd: nil, metadata: {})
79
+ return nil if process_result.nil?
80
+
81
+ res_cmd = cmd || process_result[:cmd] || "unknown_command"
82
+ diagnose(
83
+ cmd: res_cmd,
84
+ exit_code: process_result[:exit_code] || (process_result[:ok] ? 0 : 1),
85
+ stdout: process_result[:stdout] || "",
86
+ stderr: process_result[:stderr] || (process_result[:ok] ? "" : process_result[:output].to_s),
87
+ duration: process_result[:duration] || 0.0,
88
+ metadata: metadata
89
+ )
90
+ end
91
+
92
+ # Search tools whose ONLY exit-1 meaning is "pattern matched nothing"
93
+ # (session review B8): grep/rg/findstr exit 1 on no match and 2 on a
94
+ # real error, so exit 1 + clean streams is a normal empty result, not
95
+ # an UNKNOWN_FAILURE alarm.
96
+ NO_MATCH_TOOLS = ["grep", "rg", "findstr", "ag", "ack"].freeze
97
+ NO_MATCH_PREFIXES = ["git grep"].freeze
98
+
99
+ private
100
+
101
+ def no_match_tool?(cmd)
102
+ tokens = cmd.to_s.split
103
+ return false if tokens.empty?
104
+
105
+ NO_MATCH_TOOLS.include?(tokens.first) || NO_MATCH_PREFIXES.any? { |p| cmd.to_s.start_with?("#{p} ") }
106
+ end
107
+
108
+ # Cargo plugin convention: `cargo <name>` resolves the binary
109
+ # `cargo-<name>` in PATH. A "no such command" from cargo therefore
110
+ # means the plugin is missing — name the exact install command.
111
+ def cargo_plugin_suggestion(cmd, combined)
112
+ return nil unless combined.match?(/no such (?:command|subcommand)/i)
113
+
114
+ tokens = cmd.to_s.split
115
+ return nil unless tokens.shift == "cargo" && tokens.first.to_s.match?(/\A[a-z][\w-]*\z/i)
116
+
117
+ plugin = tokens.first
118
+ "cargo's plugin binary 'cargo-#{plugin}' is not installed. Install it with " \
119
+ "`cargo install cargo-#{plugin}` (e.g. cargo install cargo-nextest), pick a runner that is " \
120
+ "installed (plain `cargo test`), or configure one via repair_runtime_service."
121
+ end
122
+
123
+ def determine_category(cmd, exit_code, _stderr, stdout, combined)
124
+ return :success if exit_code.zero?
125
+ return :no_match if exit_code == 1 && no_match_tool?(cmd) && stdout.strip.empty? && !combined.match?(/\b(error|fatal)\b/i)
126
+
127
+ if exit_code == 124 || combined.match?(/(timed out|timeout after|\btimeout\b)/i)
128
+ :timeout
129
+ # The second alternative is the runner-wrapper signature (e.g.
130
+ # cargo without its plugin): the wrapper reports on stderr while
131
+ # the suite produced no output. A genuine failing suite that merely
132
+ # prints the phrase keeps its real category.
133
+ elsif (exit_code == 127 || exit_code == 9009 || exit_code == 49 ||
134
+ combined.match?(/(not recognized|\bnot found\b|command not found|cannot find|bulunamad|\bENOENT\b)/i)) ||
135
+ (stdout.strip.empty? && combined.match?(/no such (?:command|subcommand)/i))
136
+ :missing_binary
137
+ elsif combined.match?(/(permission denied|\bEACCES\b|operation not permitted)/i)
138
+ :permission_denied
139
+ elsif combined.match?(/(unrecognized option|unknown option|invalid option|requires stdio|--stdio|flag provided but not defined|unknown flag)/i)
140
+ :missing_flag_or_option
141
+ elsif combined.match?(
142
+ /(cannot load such file|ModuleNotFoundError|No module named|package .* not found|bundle install|npm install|pnpm install|yarn install)/i
143
+ )
144
+ :dependency_missing
145
+ elsif combined.match?(/(syntax error|unexpected token|compilation error|error\[E\d+\]|parse error|fatal error)/i)
146
+ :syntax_or_compile_error
147
+ elsif combined.match?(/(failures:|assertions|tests failed|failed tests|FAILED \(|FAIL:|\d+ failures, \d+ errors)/i)
148
+ :test_failure
149
+ else
150
+ :unknown_failure
151
+ end
152
+ end
153
+
154
+ def generate_suggestion(category, cmd, combined)
155
+ bin = cmd.split.first || "command"
156
+
157
+ case category
158
+ when :no_match
159
+ "'#{cmd.split.first(2).join(' ')}' completed normally: the pattern matched nothing (exit 1 with clean " \
160
+ "output is success for search tools). Refine the pattern or broaden the search path instead of debugging a failure."
161
+ when :missing_binary
162
+ cargo_plugin_suggestion(cmd, combined) ||
163
+ "The executable binary '#{bin}' was not found in PATH or root workspace. " \
164
+ "Install the required tool or use `repair_runtime_service` / `!synthesize` to configure a working alternative."
165
+ when :missing_flag_or_option
166
+ "Command line flags or arguments appear invalid. Check supported flags (e.g. `--stdio`, `stdio`) " \
167
+ "or use `repair_runtime_service` to probe correct options."
168
+ when :dependency_missing
169
+ "Required dependencies are missing. Run appropriate package manager install " \
170
+ "(e.g., `bundle install`, `npm install`, `pip install`) before proceeding."
171
+ when :syntax_or_compile_error
172
+ "A syntax or compilation error occurred. Inspect the reported source file and line numbers to resolve the error."
173
+ when :permission_denied
174
+ "Permission denied for '#{bin}'. Check file permissions (chmod +x) or sandbox security constraints."
175
+ when :timeout
176
+ "Process execution exceeded timeout threshold. Increase timeout limit or investigate potential infinite loops / blocking calls."
177
+ when :test_failure
178
+ "One or more tests failed. Review assertion failure outputs and update the corresponding implementation."
179
+ when :success
180
+ "Command executed successfully."
181
+ else
182
+ "Process exited with non-zero code. Review stderr/stdout traces to investigate cause."
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require_relative "../platform"
5
+
6
+ module Rubik
7
+ module Runtime
8
+ module Discovery
9
+ LSP_CANDIDATES = {
10
+ "ruby" => [
11
+ "solargraph stdio",
12
+ "ruby-lsp",
13
+ "typeprof --lsp --stdio"
14
+ ],
15
+ "python" => [
16
+ "pyright-langserver --stdio",
17
+ "pylsp",
18
+ "jedi-language-server"
19
+ ],
20
+ "rust" => [
21
+ "rust-analyzer"
22
+ ],
23
+ "go" => [
24
+ "gopls"
25
+ ],
26
+ "zig" => [
27
+ "zls"
28
+ ],
29
+ "c" => [
30
+ "clangd"
31
+ ],
32
+ "cpp" => [
33
+ "clangd"
34
+ ],
35
+ "typescript" => [
36
+ "typescript-language-server --stdio",
37
+ "vtsls --stdio"
38
+ ],
39
+ "javascript" => [
40
+ "typescript-language-server --stdio",
41
+ "vtsls --stdio"
42
+ ],
43
+ "bash" => [
44
+ "bash-language-server start"
45
+ ],
46
+ "crystal" => [
47
+ "crystalline"
48
+ ],
49
+ "gleam" => [
50
+ "gleam lsp"
51
+ ],
52
+ "vue" => [
53
+ "vue-language-server --stdio",
54
+ "@vue/language-server --stdio",
55
+ "vls"
56
+ ]
57
+ }.freeze
58
+
59
+ FORMATTER_CANDIDATES = {
60
+ "ruby" => [
61
+ "rubocop -a",
62
+ "standardrb --fix",
63
+ "rufo"
64
+ ],
65
+ "python" => [
66
+ "ruff format",
67
+ "black"
68
+ ],
69
+ "rust" => [
70
+ "rustfmt"
71
+ ],
72
+ "go" => [
73
+ "gofmt -w",
74
+ "goimports -w"
75
+ ],
76
+ "c" => [
77
+ "clang-format -i"
78
+ ],
79
+ "cpp" => [
80
+ "clang-format -i"
81
+ ],
82
+ "zig" => [
83
+ "zig fmt"
84
+ ],
85
+ "typescript" => [
86
+ "prettier --write",
87
+ "biome format --write"
88
+ ],
89
+ "javascript" => [
90
+ "prettier --write",
91
+ "biome format --write"
92
+ ],
93
+ "node" => [
94
+ "prettier --write",
95
+ "biome format --write"
96
+ ],
97
+ "vue" => [
98
+ "prettier --write",
99
+ "biome format --write"
100
+ ],
101
+ "crystal" => [
102
+ "crystal tool format"
103
+ ]
104
+ }.freeze
105
+
106
+ TEST_RUNNER_MARKERS = {
107
+ "ruby" => [
108
+ { marker: "Gemfile", candidates: ["bundle exec rspec", "bundle exec rake test", "rake test"] },
109
+ { marker: "Rakefile", candidates: ["bundle exec rake test", "rake test"] }
110
+ ],
111
+ "python" => [
112
+ { marker: "pyproject.toml", candidates: ["pytest", "python3 -m unittest"] },
113
+ { marker: "pytest.ini", candidates: ["pytest"] },
114
+ { marker: "setup.py", candidates: ["pytest", "python3 -m unittest"] },
115
+ { marker: "requirements.txt", candidates: ["pytest", "python3 -m unittest"] }
116
+ ],
117
+ "rust" => [
118
+ { marker: "Cargo.toml", candidates: ["cargo nextest run", "cargo test"] }
119
+ ],
120
+ "go" => [
121
+ { marker: "go.mod", candidates: ["go test ./..."] }
122
+ ],
123
+ "zig" => [
124
+ { marker: "build.zig", candidates: ["zig build test", "zig test"] }
125
+ ],
126
+ "typescript" => [
127
+ { marker: "pnpm-lock.yaml", candidates: ["pnpm test"] },
128
+ { marker: "yarn.lock", candidates: ["yarn test"] },
129
+ { marker: "package-lock.json", candidates: ["npm test"] },
130
+ { marker: "package.json", candidates: ["npm test", "pnpm test", "yarn test"] }
131
+ ],
132
+ "javascript" => [
133
+ { marker: "pnpm-lock.yaml", candidates: ["pnpm test"] },
134
+ { marker: "yarn.lock", candidates: ["yarn test"] },
135
+ { marker: "package-lock.json", candidates: ["npm test"] },
136
+ { marker: "package.json", candidates: ["npm test", "pnpm test", "yarn test"] }
137
+ ],
138
+ "vue" => [
139
+ { marker: "pnpm-lock.yaml", candidates: ["pnpm test"] },
140
+ { marker: "yarn.lock", candidates: ["yarn test"] },
141
+ { marker: "bun.lockb", candidates: ["bun test"] },
142
+ { marker: "package-lock.json", candidates: ["npm test"] },
143
+ { marker: "package.json", candidates: ["npm test", "pnpm test", "yarn test", "bun test"] }
144
+ ],
145
+ "node" => [
146
+ { marker: "pnpm-lock.yaml", candidates: ["pnpm test"] },
147
+ { marker: "yarn.lock", candidates: ["yarn test"] },
148
+ { marker: "bun.lockb", candidates: ["bun test"] },
149
+ { marker: "package-lock.json", candidates: ["npm test"] },
150
+ { marker: "package.json", candidates: ["npm test", "pnpm test", "yarn test", "bun test"] }
151
+ ],
152
+ "c" => [
153
+ { marker: "CMakeLists.txt", candidates: ["ctest", "make test"] },
154
+ { marker: "Makefile", candidates: ["make test", "ctest"] }
155
+ ],
156
+ "crystal" => [
157
+ { marker: "shard.yml", candidates: ["crystal spec"] },
158
+ { marker: "shard.lock", candidates: ["crystal spec"] }
159
+ ]
160
+ }.freeze
161
+
162
+ DEFAULT_TEST_RUNNERS = {
163
+ "ruby" => "rake test",
164
+ "python" => "pytest",
165
+ "rust" => "cargo test",
166
+ "go" => "go test ./...",
167
+ "zig" => "zig test",
168
+ "c" => "ctest",
169
+ "crystal" => "crystal spec",
170
+ "typescript" => "npm test",
171
+ "javascript" => "npm test",
172
+ "vue" => "npm test",
173
+ "node" => "npm test"
174
+ }.freeze
175
+
176
+ class << self
177
+ def lsp_candidates(lang)
178
+ LSP_CANDIDATES[lang.to_s.downcase] || []
179
+ end
180
+
181
+ def binary_available?(cmd, root_dir: nil)
182
+ return false if cmd.nil? || cmd.to_s.strip.empty?
183
+
184
+ bin = cmd.to_s.strip.split.first
185
+ if root_dir && bin && (bin.include?("/") || (Rubik::Platform.windows? && bin.include?("\\")))
186
+ full_path = File.expand_path(bin, root_dir)
187
+ return true if Rubik::Platform.executable_file?(full_path)
188
+ end
189
+
190
+ !Rubik::Platform.which(cmd).nil?
191
+ end
192
+
193
+ def probe_lsp(lang, dir = nil, root_dir: nil, custom_cmd: nil)
194
+ effective_root = root_dir || dir || Dir.pwd
195
+ lang_key = lang.to_s.downcase
196
+ candidates = []
197
+ candidates << custom_cmd.to_s.strip if custom_cmd && !custom_cmd.to_s.strip.empty?
198
+ candidates.concat(lsp_candidates(lang_key))
199
+ candidates.uniq!
200
+
201
+ candidates.find { |c| binary_available?(c, root_dir: effective_root) }
202
+ end
203
+
204
+ def test_runner_candidates(lang, dir = nil, root_dir: nil, config: nil)
205
+ effective_root = root_dir || dir || Dir.pwd
206
+ lang_key = lang.to_s.downcase
207
+ marker_rules = TEST_RUNNER_MARKERS[lang_key] || []
208
+ matched_candidates = []
209
+ # Ruby-specific: a committed Gemfile.lock pins the gem stack, so any
210
+ # bare (non-bundler) fallback runner must be suppressed — otherwise
211
+ # a default-gem (e.g. minitest 6) silently diverges from the pinned
212
+ # stack. See _plan/test-stratifikasyonu-2026-09-14.md (Package C).
213
+ ruby_lock_pinned = lang_key == "ruby" &&
214
+ File.exist?(File.join(effective_root, "Gemfile.lock"))
215
+
216
+ marker_rules.each do |rule|
217
+ marker_path = File.join(effective_root, rule[:marker])
218
+ next unless File.exist?(marker_path)
219
+
220
+ candidates = rule[:candidates]
221
+ if lang_key == "ruby" && rule[:marker] == "Gemfile"
222
+ has_spec_dir = File.directory?(File.join(effective_root, "spec"))
223
+ has_lock = File.exist?(File.join(effective_root, "Gemfile.lock"))
224
+ # A committed lockfile means the operator pinned the gem stack;
225
+ # running bare `rake`/`ruby` would bypass it and can silently
226
+ # load a different (e.g. default-gem) version. With a lockfile
227
+ # only bundler-wrapped commands are offered; without one the
228
+ # historical candidate order is preserved.
229
+ candidates = if has_spec_dir
230
+ ["bundle exec rspec", "bundle exec rake test", "rake test"]
231
+ elsif has_lock
232
+ ["bundle exec rake test"]
233
+ else
234
+ ["bundle exec rake test", "rake test", "bundle exec rspec"]
235
+ end
236
+ end
237
+ matched_candidates.concat(candidates)
238
+ end
239
+
240
+ # Add default fallback (suppressed when a ruby lockfile pins bundler)
241
+ default_runner = DEFAULT_TEST_RUNNERS[lang_key]
242
+ matched_candidates << default_runner if default_runner && !(ruby_lock_pinned && !default_runner.start_with?("bundle exec"))
243
+
244
+ # Prepend explicit test command from config if configured
245
+ cfg_cmd = resolve_config_test_cmd(config, lang_key)
246
+ matched_candidates.unshift(cfg_cmd) if cfg_cmd && !cfg_cmd.empty?
247
+
248
+ matched_candidates.uniq
249
+ end
250
+
251
+ def discover_test_runners(dir = nil, root_dir: nil, lang: nil, config: nil)
252
+ effective_root = root_dir || (dir.is_a?(String) ? dir : nil) || Dir.pwd
253
+ target_lang = lang ? lang.to_s.downcase : detect_primary_language(effective_root)
254
+ return [] unless target_lang
255
+
256
+ candidates = test_runner_candidates(target_lang, root_dir: effective_root, config: config)
257
+ candidates.select { |cmd| binary_available?(cmd, root_dir: effective_root) }
258
+ end
259
+
260
+ def discover_formatters(dir = nil, root_dir: nil, lang: nil)
261
+ effective_root = root_dir || (dir.is_a?(String) ? dir : nil) || Dir.pwd
262
+ target_lang = lang ? lang.to_s.downcase : detect_primary_language(effective_root)
263
+ return [] unless target_lang
264
+
265
+ candidates = FORMATTER_CANDIDATES[target_lang] || []
266
+ candidates.select { |cmd| binary_available?(cmd, root_dir: effective_root) }
267
+ end
268
+
269
+ def detect_primary_language(dir = nil, root_dir: nil)
270
+ effective_root = root_dir || dir || Dir.pwd
271
+
272
+ # Check root project marker files
273
+ return "ruby" if File.exist?(File.join(effective_root, "Gemfile")) || File.exist?(File.join(effective_root, "Rakefile"))
274
+ return "rust" if File.exist?(File.join(effective_root, "Cargo.toml"))
275
+ return "go" if File.exist?(File.join(effective_root, "go.mod"))
276
+ return "zig" if File.exist?(File.join(effective_root, "build.zig"))
277
+ return "python" if File.exist?(File.join(effective_root,
278
+ "pyproject.toml")) || File.exist?(File.join(effective_root,
279
+ "setup.py")) || File.exist?(File.join(effective_root,
280
+ "requirements.txt"))
281
+ return "typescript" if File.exist?(File.join(effective_root, "tsconfig.json"))
282
+ return "javascript" if File.exist?(File.join(effective_root, "package.json"))
283
+ return "c" if File.exist?(File.join(effective_root, "CMakeLists.txt"))
284
+
285
+ # Fallback: inspect extensions in directory
286
+ ext_counts = Hash.new(0)
287
+ begin
288
+ Dir.children(effective_root).each do |entry|
289
+ next if entry.start_with?(".")
290
+
291
+ full_p = File.join(effective_root, entry)
292
+ if File.file?(full_p)
293
+ ext = File.extname(entry).downcase
294
+ ext_counts[ext] += 1
295
+ end
296
+ end
297
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR
298
+ # ignore
299
+ end
300
+
301
+ ext_to_lang = {
302
+ ".rb" => "ruby",
303
+ ".rs" => "rust",
304
+ ".go" => "go",
305
+ ".py" => "python",
306
+ ".zig" => "zig",
307
+ ".ts" => "typescript",
308
+ ".js" => "javascript",
309
+ ".vue" => "vue",
310
+ ".c" => "c",
311
+ ".cpp" => "c"
312
+ }
313
+
314
+ best_ext = ext_counts.max_by { |_ext, count| count }&.first
315
+ ext_to_lang[best_ext] || "ruby"
316
+ end
317
+
318
+ private
319
+
320
+ def resolve_config_test_cmd(config, lang_key)
321
+ return nil unless config
322
+
323
+ cmd = if config.is_a?(Hash)
324
+ config["adapter.#{lang_key}.test_command"] ||
325
+ config["project.test_command"] ||
326
+ config.dig("adapter", lang_key, "test_command") ||
327
+ config.dig("adapter", lang_key.to_sym, "test_command") ||
328
+ config.dig("project", "test_command")
329
+ elsif config.respond_to?(:fetch)
330
+ begin
331
+ config.fetch("adapter.#{lang_key}.test_command", nil) ||
332
+ config.fetch("project.test_command", nil)
333
+ rescue KeyError, NoMethodError, TypeError
334
+ nil
335
+ end
336
+ end
337
+ cmd&.to_s&.strip
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "context"
4
+
5
+ module Rubik
6
+ module Runtime
7
+ # = Rubik::Runtime::ComponentFiber
8
+ #
9
+ # Component Lifecycle Unit for Spatiotemporal Composability (AGENTS.md §2).
10
+ #
11
+ # == Architecture & State Machine:
12
+ # A ComponentFiber represents the running instance of a pluggable runtime component.
13
+ # It strictly follows the Fiber Lifecycle State Machine defined in AGENTS.md:
14
+ #
15
+ # [Registered] -> PENDING <-------------------------+
16
+ # | |
17
+ # | Coeffects Satisfied | Coeffects Lost /
18
+ # v | Unload Requested
19
+ # ACTIVE --------------------------+
20
+ # |
21
+ # | Error during Activation
22
+ # v
23
+ # FAILED -> (Partial Inverses / Rollback) -> DISPOSED
24
+ #
25
+ # == Distinction from Ruby's ::Fiber:
26
+ # Rubik::Runtime::ComponentFiber is NOT a Ruby standard library coroutine (::Fiber) or
27
+ # lightweight cooperative thread. Instead, it is an architectural lifecycle fiber
28
+ # that encapsulates a component's coeffect dependencies (:inject), scoped sub-context
29
+ # (@sub_context = context.extend), and LIFO effect inverses (Omega accumulator)
30
+ # ensuring zero-restart reloading and atomic rollback.
31
+ #
32
+ # == Concurrency Contract (review 2026-09-17, Finding 2):
33
+ # ComponentFiber is intentionally lock-free: correctness relies on external
34
+ # serialization, NOT on internal locking. State mutations (@state,
35
+ # @sub_context, @instance) and the multi-step activate!/deactivate! sequences
36
+ # MUST happen under the owning Manager's Monitor (via register_component /
37
+ # provide / reconcile_all / swap_component) or in single-threaded code.
38
+ #
39
+ # Why no blanket per-fiber Mutex: the factory passed to activate! runs while
40
+ # the caller holds the Manager's Monitor, and factories may call back into
41
+ # the Manager (Monitor is reentrant). A fiber-local mutex would create a
42
+ # lock-order inversion (fiber lock -> Manager Monitor vs Manager Monitor ->
43
+ # fiber lock) and deadlock. The Manager's reentrant Monitor is the single
44
+ # serialization point; this class deliberately stays lock-free.
45
+ class ComponentFiber
46
+ STATES = [:unloaded, :pending, :active, :failed, :disposed].freeze
47
+
48
+ attr_reader :name, :inject, :requires, :provides, :config, :state, :last_error, :sub_context, :instance
49
+
50
+ def initialize(name:, inject: [], requires: [], provides: [], config: {}, &factory)
51
+ @name = name.to_s
52
+ @inject = Array(inject).map(&:to_s).map(&:downcase)
53
+ @requires = Array(requires).map(&:to_s).map(&:downcase)
54
+ @provides = ([@name.downcase] + Array(provides).map(&:to_s).map(&:downcase)).uniq
55
+ @config = config || {}
56
+ @factory = factory
57
+ @state = :pending
58
+ @last_error = nil
59
+ @sub_context = nil
60
+ @instance = nil
61
+ end
62
+
63
+ def satisfiable?(context)
64
+ @inject.all? { |dep| context.has?(dep) }
65
+ end
66
+
67
+ # A fiber is ready to activate when its inject dependencies are satisfied
68
+ # AND all its requires: coeffect prerequisites are also present in the context.
69
+ # `requires:` is a pre-activation gate (Plan 86 D-3): the fiber stays Pending
70
+ # until every declared requirement is available, preventing partial activation.
71
+ def ready_to_activate?(context)
72
+ satisfiable?(context) && @requires.all? { |dep| context.has?(dep) }
73
+ end
74
+
75
+ def activate!(context)
76
+ return true if @state == :active
77
+ return false unless ready_to_activate?(context)
78
+
79
+ # Scoped sub-context for fiber lifecycle
80
+ @sub_context = context.extend
81
+
82
+ begin
83
+ @instance = if @factory
84
+ @factory.call(@sub_context, @config)
85
+ else
86
+ build_instance(@sub_context, @config)
87
+ end
88
+ @state = :active
89
+ @last_error = nil
90
+ true
91
+ rescue StandardError => e
92
+ # Atomic Rollback on Error: unwind partial inverses immediately
93
+ @sub_context&.dispose
94
+ @sub_context = nil
95
+ @instance = nil
96
+ @state = :failed
97
+ @last_error = e
98
+ false
99
+ end
100
+ end
101
+
102
+ def deactivate!
103
+ return false unless @state == :active
104
+
105
+ if @sub_context
106
+ @sub_context.dispose
107
+ @sub_context = nil
108
+ end
109
+ @instance = nil
110
+ @state = :pending
111
+ true
112
+ end
113
+
114
+ def reactivate!(context)
115
+ deactivate! if @state == :active
116
+ activate!(context)
117
+ end
118
+
119
+ def mark_failed!(error = nil)
120
+ deactivate! if @state == :active
121
+ @state = :failed
122
+ @last_error = error || StandardError.new("Fiber '#{@name}' marked failed")
123
+ end
124
+
125
+ def dispose!
126
+ deactivate! if @state == :active
127
+ @state = :disposed
128
+ end
129
+
130
+ def build_instance(_sub_context, _config)
131
+ nil
132
+ end
133
+ end
134
+
135
+ # Backwards-compatibility alias (AGENTS.md terminology). Kept for external
136
+ # consumers and pin test `test_component_fiber_naming_and_alias_compatibility`;
137
+ # internal code must use ComponentFiber directly — a bare `Fiber` inside
138
+ # Rubik::Runtime shadows ::Fiber and corrupts grep/grep-ability (review
139
+ # Finding 6). Enforced by test_no_bare_fiber_alias_use_in_lib.
140
+ Fiber = ComponentFiber
141
+ end
142
+ end