rubino-agent 0.3.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 (376) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +115 -0
  4. data/.rubocop_todo.yml +955 -0
  5. data/.ruby-version +1 -0
  6. data/AGENTS.md +97 -0
  7. data/CHANGELOG.md +344 -0
  8. data/CONTRIBUTING.md +69 -0
  9. data/LICENSE +21 -0
  10. data/README.md +200 -0
  11. data/Rakefile +8 -0
  12. data/docs/agents.md +190 -0
  13. data/docs/api/v1.md +414 -0
  14. data/docs/architecture.md +177 -0
  15. data/docs/commands.md +375 -0
  16. data/docs/configuration.md +590 -0
  17. data/docs/getting-started.md +143 -0
  18. data/docs/jobs.md +332 -0
  19. data/docs/mcp.md +128 -0
  20. data/docs/memory.md +98 -0
  21. data/docs/models-and-keys.md +173 -0
  22. data/docs/oauth-providers.md +145 -0
  23. data/docs/plugins.md +195 -0
  24. data/docs/security.md +145 -0
  25. data/docs/skills.md +322 -0
  26. data/docs/tools.md +395 -0
  27. data/docs/troubleshooting.md +73 -0
  28. data/exe/rubino +9 -0
  29. data/install.sh +275 -0
  30. data/lib/rubino/active_skill.rb +50 -0
  31. data/lib/rubino/agent/agent_registry.rb +120 -0
  32. data/lib/rubino/agent/backoff_policy.rb +116 -0
  33. data/lib/rubino/agent/definition.rb +128 -0
  34. data/lib/rubino/agent/degenerate_recovery.rb +271 -0
  35. data/lib/rubino/agent/fallback_chain.rb +194 -0
  36. data/lib/rubino/agent/iteration_budget.rb +50 -0
  37. data/lib/rubino/agent/loop.rb +617 -0
  38. data/lib/rubino/agent/model_call_runner.rb +383 -0
  39. data/lib/rubino/agent/prompts/build.txt +69 -0
  40. data/lib/rubino/agent/prompts/compaction.txt +20 -0
  41. data/lib/rubino/agent/prompts/explore.txt +19 -0
  42. data/lib/rubino/agent/prompts/general.txt +20 -0
  43. data/lib/rubino/agent/prompts/plan.txt +31 -0
  44. data/lib/rubino/agent/response_validator.rb +70 -0
  45. data/lib/rubino/agent/router.rb +65 -0
  46. data/lib/rubino/agent/runner.rb +195 -0
  47. data/lib/rubino/agent/tool_executor.rb +402 -0
  48. data/lib/rubino/agent/truncation_continuation.rb +137 -0
  49. data/lib/rubino/api/middleware/auth.rb +43 -0
  50. data/lib/rubino/api/middleware/error_handler.rb +65 -0
  51. data/lib/rubino/api/middleware/json_parser.rb +100 -0
  52. data/lib/rubino/api/middleware/observability.rb +59 -0
  53. data/lib/rubino/api/middleware/rate_limit.rb +136 -0
  54. data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
  55. data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
  56. data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
  57. data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
  58. data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
  59. data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
  60. data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
  61. data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
  62. data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
  63. data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
  64. data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
  65. data/lib/rubino/api/operations/files/read_operation.rb +40 -0
  66. data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
  67. data/lib/rubino/api/operations/health_operation.rb +46 -0
  68. data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
  69. data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
  70. data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
  71. data/lib/rubino/api/operations/metrics_operation.rb +18 -0
  72. data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
  73. data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
  74. data/lib/rubino/api/operations/models/list_operation.rb +45 -0
  75. data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
  76. data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
  77. data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
  78. data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
  79. data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
  80. data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
  81. data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
  82. data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
  83. data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
  84. data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
  85. data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
  86. data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
  87. data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
  88. data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
  89. data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
  90. data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
  91. data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
  92. data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
  93. data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
  94. data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
  95. data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
  96. data/lib/rubino/api/request.rb +54 -0
  97. data/lib/rubino/api/responses.rb +64 -0
  98. data/lib/rubino/api/router.rb +72 -0
  99. data/lib/rubino/api/schemas.rb +103 -0
  100. data/lib/rubino/api/server.rb +102 -0
  101. data/lib/rubino/api/tls.rb +108 -0
  102. data/lib/rubino/attachments/classification.rb +16 -0
  103. data/lib/rubino/attachments/classify.rb +171 -0
  104. data/lib/rubino/attachments/defang.rb +47 -0
  105. data/lib/rubino/attachments/policy.rb +36 -0
  106. data/lib/rubino/attachments/preamble.rb +120 -0
  107. data/lib/rubino/boot/encryption_key.rb +32 -0
  108. data/lib/rubino/cli/chat/bang_shell.rb +257 -0
  109. data/lib/rubino/cli/chat/completion_builder.rb +290 -0
  110. data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
  111. data/lib/rubino/cli/chat/image_inbox.rb +168 -0
  112. data/lib/rubino/cli/chat/session_resolver.rb +176 -0
  113. data/lib/rubino/cli/chat_command.rb +1674 -0
  114. data/lib/rubino/cli/commands.rb +250 -0
  115. data/lib/rubino/cli/config_command.rb +96 -0
  116. data/lib/rubino/cli/doctor_command.rb +251 -0
  117. data/lib/rubino/cli/jobs_command.rb +60 -0
  118. data/lib/rubino/cli/memory_command.rb +135 -0
  119. data/lib/rubino/cli/onboarding_wizard.rb +207 -0
  120. data/lib/rubino/cli/server_command.rb +139 -0
  121. data/lib/rubino/cli/session_command.rb +125 -0
  122. data/lib/rubino/cli/setup_command.rb +107 -0
  123. data/lib/rubino/cli/skills_command.rb +85 -0
  124. data/lib/rubino/cli/tools_command.rb +81 -0
  125. data/lib/rubino/cli/trust_gate.rb +71 -0
  126. data/lib/rubino/commands/built_ins.rb +46 -0
  127. data/lib/rubino/commands/command.rb +116 -0
  128. data/lib/rubino/commands/executor.rb +550 -0
  129. data/lib/rubino/commands/handlers/agents.rb +510 -0
  130. data/lib/rubino/commands/handlers/config.rb +88 -0
  131. data/lib/rubino/commands/handlers/help.rb +148 -0
  132. data/lib/rubino/commands/handlers/jobs.rb +71 -0
  133. data/lib/rubino/commands/handlers/mcp.rb +229 -0
  134. data/lib/rubino/commands/handlers/memory.rb +200 -0
  135. data/lib/rubino/commands/handlers/sessions.rb +207 -0
  136. data/lib/rubino/commands/handlers/skills.rb +195 -0
  137. data/lib/rubino/commands/handlers/status.rb +211 -0
  138. data/lib/rubino/commands/loader.rb +90 -0
  139. data/lib/rubino/config/configuration.rb +455 -0
  140. data/lib/rubino/config/defaults.rb +569 -0
  141. data/lib/rubino/config/loader.rb +115 -0
  142. data/lib/rubino/config/reasoning_prefs.rb +67 -0
  143. data/lib/rubino/config/writer.rb +72 -0
  144. data/lib/rubino/context/compressor.rb +149 -0
  145. data/lib/rubino/context/environment_inspector.rb +176 -0
  146. data/lib/rubino/context/file_discovery.rb +45 -0
  147. data/lib/rubino/context/message_boundary.rb +39 -0
  148. data/lib/rubino/context/prompt_assembler.rb +382 -0
  149. data/lib/rubino/context/summary_builder.rb +159 -0
  150. data/lib/rubino/context/token_budget.rb +68 -0
  151. data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
  152. data/lib/rubino/database/connection.rb +77 -0
  153. data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
  154. data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
  155. data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
  156. data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
  157. data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
  158. data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
  159. data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
  160. data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
  161. data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
  162. data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
  163. data/lib/rubino/database/migrator.rb +48 -0
  164. data/lib/rubino/documents/converters/csv.rb +79 -0
  165. data/lib/rubino/documents/converters/docx.rb +129 -0
  166. data/lib/rubino/documents/converters/html.rb +28 -0
  167. data/lib/rubino/documents/converters/json.rb +35 -0
  168. data/lib/rubino/documents/converters/pdf.rb +59 -0
  169. data/lib/rubino/documents/converters/plain.rb +68 -0
  170. data/lib/rubino/documents/converters/pptx.rb +64 -0
  171. data/lib/rubino/documents/converters/xlsx.rb +62 -0
  172. data/lib/rubino/documents/converters/xml.rb +45 -0
  173. data/lib/rubino/documents/html.rb +71 -0
  174. data/lib/rubino/documents/registry.rb +68 -0
  175. data/lib/rubino/documents/table.rb +63 -0
  176. data/lib/rubino/documents.rb +50 -0
  177. data/lib/rubino/errors.rb +119 -0
  178. data/lib/rubino/files/workspace.rb +93 -0
  179. data/lib/rubino/interaction/cancel_token.rb +43 -0
  180. data/lib/rubino/interaction/clipboard_image.rb +84 -0
  181. data/lib/rubino/interaction/event_bus.rb +48 -0
  182. data/lib/rubino/interaction/events.rb +101 -0
  183. data/lib/rubino/interaction/image_input.rb +127 -0
  184. data/lib/rubino/interaction/input_queue.rb +117 -0
  185. data/lib/rubino/interaction/lifecycle.rb +299 -0
  186. data/lib/rubino/interaction/probe.rb +65 -0
  187. data/lib/rubino/interaction/state.rb +56 -0
  188. data/lib/rubino/jobs/cron_job_repository.rb +75 -0
  189. data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
  190. data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
  191. data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
  192. data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
  193. data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
  194. data/lib/rubino/jobs/queue.rb +184 -0
  195. data/lib/rubino/jobs/registry.rb +45 -0
  196. data/lib/rubino/jobs/runner.rb +79 -0
  197. data/lib/rubino/jobs/scheduler.rb +138 -0
  198. data/lib/rubino/jobs/webhook_delivery.rb +225 -0
  199. data/lib/rubino/jobs/worker.rb +59 -0
  200. data/lib/rubino/llm/adapter_factory.rb +47 -0
  201. data/lib/rubino/llm/adapter_response.rb +65 -0
  202. data/lib/rubino/llm/auxiliary_client.rb +61 -0
  203. data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
  204. data/lib/rubino/llm/content_builder.rb +55 -0
  205. data/lib/rubino/llm/credential_check.rb +93 -0
  206. data/lib/rubino/llm/error_classifier.rb +364 -0
  207. data/lib/rubino/llm/fake_provider.rb +292 -0
  208. data/lib/rubino/llm/inline_think_filter.rb +58 -0
  209. data/lib/rubino/llm/model_catalog.rb +29 -0
  210. data/lib/rubino/llm/provider_resolver.rb +48 -0
  211. data/lib/rubino/llm/reasoning_manager.rb +100 -0
  212. data/lib/rubino/llm/request.rb +56 -0
  213. data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
  214. data/lib/rubino/llm/scenario_loader.rb +68 -0
  215. data/lib/rubino/llm/scenario_selector.rb +80 -0
  216. data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
  217. data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
  218. data/lib/rubino/llm/scenarios/analysis.yml +501 -0
  219. data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
  220. data/lib/rubino/llm/scenarios/failure.yml +65 -0
  221. data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
  222. data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
  223. data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
  224. data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
  225. data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
  226. data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
  227. data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
  228. data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
  229. data/lib/rubino/llm/thinking_support.rb +84 -0
  230. data/lib/rubino/llm/tool_bridge.rb +89 -0
  231. data/lib/rubino/logger.rb +99 -0
  232. data/lib/rubino/mcp/manager.rb +180 -0
  233. data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
  234. data/lib/rubino/mcp.rb +57 -0
  235. data/lib/rubino/memory/backend.rb +104 -0
  236. data/lib/rubino/memory/backends/default.rb +101 -0
  237. data/lib/rubino/memory/backends/sqlite.rb +653 -0
  238. data/lib/rubino/memory/backends.rb +53 -0
  239. data/lib/rubino/memory/deduplicator.rb +74 -0
  240. data/lib/rubino/memory/extractor.rb +85 -0
  241. data/lib/rubino/memory/flusher.rb +31 -0
  242. data/lib/rubino/memory/retriever.rb +50 -0
  243. data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
  244. data/lib/rubino/memory/sqlite_graph.rb +154 -0
  245. data/lib/rubino/memory/store.rb +228 -0
  246. data/lib/rubino/memory/threat_scanner.rb +68 -0
  247. data/lib/rubino/metrics.rb +175 -0
  248. data/lib/rubino/modes.rb +93 -0
  249. data/lib/rubino/oauth/connection_repository.rb +95 -0
  250. data/lib/rubino/oauth/provider/github.rb +75 -0
  251. data/lib/rubino/oauth/provider/google.rb +59 -0
  252. data/lib/rubino/oauth/provider.rb +149 -0
  253. data/lib/rubino/oauth/registry.rb +86 -0
  254. data/lib/rubino/oauth/token_encryptor.rb +87 -0
  255. data/lib/rubino/plugins/registry.rb +75 -0
  256. data/lib/rubino/plugins.rb +86 -0
  257. data/lib/rubino/run/approval_gate.rb +243 -0
  258. data/lib/rubino/run/attachment_downloader.rb +166 -0
  259. data/lib/rubino/run/event_store.rb +74 -0
  260. data/lib/rubino/run/executor.rb +383 -0
  261. data/lib/rubino/run/gate_registry.rb +39 -0
  262. data/lib/rubino/run/recorder.rb +69 -0
  263. data/lib/rubino/run/repository.rb +118 -0
  264. data/lib/rubino/run/session_approval_cache.rb +118 -0
  265. data/lib/rubino/security/allowlist_persister.rb +55 -0
  266. data/lib/rubino/security/approval_policy.rb +227 -0
  267. data/lib/rubino/security/command_allowlist.rb +24 -0
  268. data/lib/rubino/security/dangerous_patterns.rb +118 -0
  269. data/lib/rubino/security/deny_persister.rb +73 -0
  270. data/lib/rubino/security/doom_loop_detector.rb +43 -0
  271. data/lib/rubino/security/hardline_guard.rb +105 -0
  272. data/lib/rubino/security/pattern_matcher.rb +62 -0
  273. data/lib/rubino/security/prefix_deriver.rb +124 -0
  274. data/lib/rubino/security/readonly_commands.rb +211 -0
  275. data/lib/rubino/session/exporter.rb +101 -0
  276. data/lib/rubino/session/message.rb +77 -0
  277. data/lib/rubino/session/repository.rb +295 -0
  278. data/lib/rubino/session/store.rb +198 -0
  279. data/lib/rubino/session/summary_store.rb +65 -0
  280. data/lib/rubino/skills/prompt_index.rb +85 -0
  281. data/lib/rubino/skills/registry.rb +208 -0
  282. data/lib/rubino/skills/skill.rb +176 -0
  283. data/lib/rubino/skills/skill_tool.rb +215 -0
  284. data/lib/rubino/skills/state_repository.rb +37 -0
  285. data/lib/rubino/skills/toggle.rb +26 -0
  286. data/lib/rubino/tools/answer_child_tool.rb +83 -0
  287. data/lib/rubino/tools/ask_parent_tool.rb +232 -0
  288. data/lib/rubino/tools/attach_file_tool.rb +120 -0
  289. data/lib/rubino/tools/background_tasks.rb +520 -0
  290. data/lib/rubino/tools/base.rb +222 -0
  291. data/lib/rubino/tools/custom_tool_loader.rb +119 -0
  292. data/lib/rubino/tools/edit_tool.rb +122 -0
  293. data/lib/rubino/tools/git_tool.rb +71 -0
  294. data/lib/rubino/tools/github_tool.rb +233 -0
  295. data/lib/rubino/tools/glob_tool.rb +69 -0
  296. data/lib/rubino/tools/grep_tool.rb +206 -0
  297. data/lib/rubino/tools/memory_tool.rb +184 -0
  298. data/lib/rubino/tools/multi_edit_tool.rb +110 -0
  299. data/lib/rubino/tools/patch_tool.rb +260 -0
  300. data/lib/rubino/tools/probe_tool.rb +175 -0
  301. data/lib/rubino/tools/question_tool.rb +128 -0
  302. data/lib/rubino/tools/read_attachment_tool.rb +180 -0
  303. data/lib/rubino/tools/read_tool.rb +212 -0
  304. data/lib/rubino/tools/read_tracker.rb +98 -0
  305. data/lib/rubino/tools/registry.rb +166 -0
  306. data/lib/rubino/tools/result.rb +113 -0
  307. data/lib/rubino/tools/ruby_tool.rb +0 -0
  308. data/lib/rubino/tools/session_search_tool.rb +103 -0
  309. data/lib/rubino/tools/shell_input_tool.rb +96 -0
  310. data/lib/rubino/tools/shell_kill_tool.rb +76 -0
  311. data/lib/rubino/tools/shell_output_tool.rb +72 -0
  312. data/lib/rubino/tools/shell_registry.rb +158 -0
  313. data/lib/rubino/tools/shell_tail_tool.rb +118 -0
  314. data/lib/rubino/tools/shell_tool.rb +330 -0
  315. data/lib/rubino/tools/steer_tool.rb +118 -0
  316. data/lib/rubino/tools/subagent_probe.rb +89 -0
  317. data/lib/rubino/tools/summarize_file_tool.rb +182 -0
  318. data/lib/rubino/tools/task_result_tool.rb +90 -0
  319. data/lib/rubino/tools/task_stop_tool.rb +80 -0
  320. data/lib/rubino/tools/task_tool.rb +622 -0
  321. data/lib/rubino/tools/test_tool.rb +454 -0
  322. data/lib/rubino/tools/todo_tool.rb +93 -0
  323. data/lib/rubino/tools/tool_call_repository.rb +33 -0
  324. data/lib/rubino/tools/vision_tool.rb +85 -0
  325. data/lib/rubino/tools/webfetch_tool.rb +153 -0
  326. data/lib/rubino/tools/websearch_tool.rb +179 -0
  327. data/lib/rubino/tools/write_tool.rb +61 -0
  328. data/lib/rubino/trust.rb +88 -0
  329. data/lib/rubino/ui/api.rb +296 -0
  330. data/lib/rubino/ui/base.rb +252 -0
  331. data/lib/rubino/ui/bottom_composer.rb +1599 -0
  332. data/lib/rubino/ui/cli.rb +1987 -0
  333. data/lib/rubino/ui/completion_menu.rb +321 -0
  334. data/lib/rubino/ui/completion_source.rb +284 -0
  335. data/lib/rubino/ui/escape_reader.rb +169 -0
  336. data/lib/rubino/ui/indented_io.rb +88 -0
  337. data/lib/rubino/ui/input_history.rb +108 -0
  338. data/lib/rubino/ui/live_region.rb +183 -0
  339. data/lib/rubino/ui/markdown_renderer.rb +506 -0
  340. data/lib/rubino/ui/notifier.rb +163 -0
  341. data/lib/rubino/ui/null.rb +195 -0
  342. data/lib/rubino/ui/paste_store.rb +176 -0
  343. data/lib/rubino/ui/printer_base.rb +79 -0
  344. data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
  345. data/lib/rubino/ui/queued_indicators.rb +66 -0
  346. data/lib/rubino/ui/status_bar.rb +100 -0
  347. data/lib/rubino/ui/stdout_proxy.rb +161 -0
  348. data/lib/rubino/ui/streaming_markdown.rb +186 -0
  349. data/lib/rubino/ui/subagent_cards.rb +134 -0
  350. data/lib/rubino/ui/subagent_view.rb +255 -0
  351. data/lib/rubino/ui.rb +21 -0
  352. data/lib/rubino/update_check.rb +187 -0
  353. data/lib/rubino/util/duration.rb +23 -0
  354. data/lib/rubino/util/hyperlink.rb +105 -0
  355. data/lib/rubino/util/output.rb +145 -0
  356. data/lib/rubino/util/secrets_mask.rb +83 -0
  357. data/lib/rubino/version.rb +5 -0
  358. data/lib/rubino/workspace.rb +85 -0
  359. data/lib/rubino-agent.rb +5 -0
  360. data/lib/rubino.rb +318 -0
  361. data/mise.toml +2 -0
  362. data/rubino-agent.gemspec +103 -0
  363. data/skills/ruby-expert/SKILL.md +67 -0
  364. data/skills/ruby-expert/references/concurrency.md +357 -0
  365. data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
  366. data/skills/ruby-expert/references/errors-and-types.md +460 -0
  367. data/skills/ruby-expert/references/gem-authoring.md +459 -0
  368. data/skills/ruby-expert/references/language-idioms.md +465 -0
  369. data/skills/ruby-expert/references/metaprogramming.md +339 -0
  370. data/skills/ruby-expert/references/oo-design.md +553 -0
  371. data/skills/ruby-expert/references/performance.md +383 -0
  372. data/skills/ruby-expert/references/rails.md +424 -0
  373. data/skills/ruby-expert/references/security.md +404 -0
  374. data/skills/ruby-expert/references/testing.md +473 -0
  375. data/skills/ruby-expert/references/tooling.md +466 -0
  376. metadata +856 -0
@@ -0,0 +1,383 @@
1
+ # Performance & memory
2
+
3
+ Making Ruby (3.2–3.4) and Rails (7.1–8.x) fast and lean. **Golden loop: profile → fix the single biggest cost → re-measure.** Never optimize on a hunch; Ruby's hotspots are routinely counter-intuitive (allocation and GC, not arithmetic).
4
+
5
+ ## Measure first
6
+
7
+ Do not micro-optimize without data. A "faster" expression that runs 1% of the time is worthless. Find where the wall-clock and allocations actually go, then act.
8
+
9
+ ### Benchmark / benchmark-ips
10
+
11
+ `Benchmark` (stdlib) gives raw timings; **`benchmark-ips`** (gem) is the right tool for *comparing implementations* — it warms up, runs to a stable iteration rate, and reports a relative comparison with error bars.
12
+
13
+ ```ruby
14
+ require "benchmark/ips"
15
+
16
+ ARR = (1..10_000).to_a
17
+
18
+ Benchmark.ips do |x|
19
+ x.report("map+compact") { ARR.map { |n| n if n.even? }.compact }
20
+ x.report("filter_map") { ARR.filter_map { |n| n if n.even? } }
21
+ x.report("select+map") { ARR.select(&:even?).map { |n| n } }
22
+ x.compare! # prints "filter_map: 1.42x faster" etc.
23
+ end
24
+ ```
25
+
26
+ Rules:
27
+ - Always `compare!` — absolute ips numbers are meaningless across machines.
28
+ - Put real-sized data in the benchmark; tiny inputs hide allocation/GC cost.
29
+ - For allocation comparisons, pair ips with `memory_profiler` (below) — CPU and memory rankings often disagree.
30
+
31
+ ```ruby
32
+ # WRONG: timing once, no warmup, GC noise dominates
33
+ t = Time.now; do_work; puts Time.now - t
34
+
35
+ # RIGHT: benchmark-ips handles warmup, GC, and statistical stability
36
+ ```
37
+
38
+ ## Profilers
39
+
40
+ ### stackprof — the default CPU/wall/object profiler
41
+
42
+ `stackprof` is a sampling profiler — low overhead, safe in production-like loads. Three modes:
43
+ - `:cpu` — on-CPU time (find compute hotspots).
44
+ - `:wall` — wall-clock (find where you *wait*: IO, locks, sleeps).
45
+ - `:object` — samples allocations (find what allocates the most).
46
+
47
+ ```ruby
48
+ require "stackprof"
49
+
50
+ StackProf.run(mode: :cpu, out: "tmp/stackprof-cpu.dump", interval: 1000) do
51
+ expensive_call
52
+ end
53
+ ```
54
+
55
+ ```bash
56
+ stackprof tmp/stackprof-cpu.dump --text --limit 20 # top frames
57
+ stackprof tmp/stackprof-cpu.dump --method 'MyClass#slow' # callers/callees
58
+ stackprof tmp/stackprof-cpu.dump --flamegraph > fg.html
59
+ ```
60
+
61
+ Use `mode: :object` when GC time is high — it tells you *which line* allocates so you can cut it.
62
+
63
+ ### vernier — modern sampling profiler (prefer for Ruby 3.2+)
64
+
65
+ `vernier` is the current best-in-class sampler: thread-aware, captures GC and idle/IO time, low overhead, and emits a profile for the Firefox Profiler UI. Prefer it over stackprof on Ruby 3.2+, especially for multi-threaded apps (Puma, Sidekiq).
66
+
67
+ ```ruby
68
+ require "vernier"
69
+
70
+ Vernier.profile(out: "tmp/profile.json.gz") do
71
+ do_work
72
+ end
73
+ ```
74
+
75
+ ```bash
76
+ vernier run -- ruby script.rb # wrap a whole process
77
+ # Open tmp/profile.json.gz at https://profiler.firefox.com
78
+ ```
79
+
80
+ Vernier shows time *between* threads and GC pauses that single-thread profilers miss. See `references/concurrency.md` for the threading model it visualizes.
81
+
82
+ ### ruby-prof — deterministic, call-graph detail
83
+
84
+ `ruby-prof` instruments every call (high overhead, NOT for production) but gives exact call counts and a full call graph. Reach for it when you need precise *call counts* or a callgrind graph, not for sampling under load.
85
+
86
+ ```ruby
87
+ require "ruby-prof"
88
+ result = RubyProf.profile { do_work }
89
+ RubyProf::FlatPrinter.new(result).print(STDOUT, min_percent: 2)
90
+ ```
91
+
92
+ ### rack-mini-profiler — web request profiling
93
+
94
+ For Rails/Rack, `rack-mini-profiler` adds an in-page speed badge with SQL, view, and allocation breakdowns per request. Pair with `flamegraph` and `stackprof` gems for `?pp=flamegraph`.
95
+
96
+ ```ruby
97
+ # Gemfile (development)
98
+ gem "rack-mini-profiler"
99
+ gem "stackprof" # enables ?pp=flamegraph
100
+ gem "memory_profiler" # enables ?pp=profile-memory
101
+ ```
102
+
103
+ ```
104
+ GET /page?pp=flamegraph # request flamegraph
105
+ GET /page?pp=profile-memory # allocation report for the request
106
+ ```
107
+
108
+ ## Memory & allocation
109
+
110
+ In Ruby, **allocations are the dominant cost** — every object created is future GC work. Cutting allocations usually beats algorithmic tweaks.
111
+
112
+ ### memory_profiler — what allocates and what retains
113
+
114
+ ```ruby
115
+ require "memory_profiler"
116
+ report = MemoryProfiler.report { build_response }
117
+ report.pretty_print(to_file: "tmp/mem.txt")
118
+ ```
119
+
120
+ Read two numbers: **allocated** (churn → GC pressure) and **retained** (lives past the block → leak/bloat). High allocated with low retained = GC thrash; high retained = a leak or oversized cache.
121
+
122
+ ### derailed_benchmarks — Rails memory at boot and per-request
123
+
124
+ `derailed_benchmarks` finds gem memory bloat and per-request allocations.
125
+
126
+ ```bash
127
+ bundle exec derailed bundle:mem # memory used by requiring each gem
128
+ bundle exec derailed exec perf:mem # per-request memory
129
+ bundle exec derailed exec perf:objects
130
+ ```
131
+
132
+ ### Frozen string literals cut allocations
133
+
134
+ Each unfrozen string literal allocates a *new* object every time it's evaluated; `# frozen_string_literal: true` dedups them into one shared frozen object, and you allocate a mutable buffer explicitly with `+""`. Mandatory at the top of every file you control — the allocation win is why. Mechanics (the magic comment, `+""`/`-"..."`, building with `<<`/`join`): see `references/language-idioms.md`.
135
+
136
+ ### Avoid intermediate arrays
137
+
138
+ Chained `map`/`select`/`reject` each allocate a full intermediate array. Collapse them.
139
+
140
+ ```ruby
141
+ # WRONG: 3 intermediate arrays of size ~N
142
+ users.select(&:active?).map(&:email).uniq
143
+
144
+ # RIGHT (single pass, one accumulator):
145
+ users.each_with_object(Set.new) { |u, s| s << u.email if u.active? }
146
+
147
+ # RIGHT (fuse two passes into one):
148
+ users.filter_map { |u| u.email if u.active? }.uniq
149
+ ```
150
+
151
+ For large or lazy streams, use `lazy` so nothing is materialized until `.first`/`.take`/`.force`:
152
+
153
+ ```ruby
154
+ # Reads/transforms only until 10 matches — no full intermediate arrays
155
+ File.foreach("huge.log").lazy
156
+ .map { |line| parse(line) }
157
+ .select { |e| e.error? }
158
+ .first(10)
159
+ ```
160
+
161
+ Tool choice within these pipelines (`filter_map`, `each_with_object` vs `reduce`, `lazy`): see `references/language-idioms.md` for the full Enumerable toolbox.
162
+
163
+ ### Symbol vs string allocation
164
+
165
+ Symbols are interned (one object per name) and never churn the heap, so they're the right key/identifier type for hot hash lookups. Never `to_sym` untrusted/unbounded input — those symbols accumulate as memory growth. Symbol-vs-string semantics: see `references/language-idioms.md`.
166
+
167
+ ### Build strings without churn
168
+
169
+ `out += ...` in a loop is O(n²) copying; build with `<<` into a `+""` buffer, or `map`/`join` once — see `references/language-idioms.md`. For very large output, stream instead of accumulating (see Streaming below).
170
+
171
+ ## The garbage collector
172
+
173
+ MRI's GC is **generational** (young objects collected cheaply and often; long-lived ones promoted to old gen and scanned rarely) and **incremental** (the costly old-gen mark is sliced to bound pause time). The practical lever you control is *allocating fewer objects*; tuning is a second-order adjustment.
174
+
175
+ ### GC.stat — read before you tune
176
+
177
+ ```ruby
178
+ GC.stat(:major_gc_count) # major (full) collections — expensive; want few
179
+ GC.stat(:minor_gc_count) # minor — cheap & frequent is fine
180
+ GC.stat(:heap_live_slots)
181
+ GC.stat(:total_allocated_objects) # churn proxy across run
182
+ GC.total_time # ns spent in GC (Ruby 3.x)
183
+ ```
184
+
185
+ If `major_gc_count` climbs fast or `GC.total_time` is a large fraction of wall time, you have an allocation problem — go back to `memory_profiler`/stackprof `:object`, do not jump to env tuning.
186
+
187
+ ### GC tuning env vars
188
+
189
+ Set via environment at process start (cannot change most after boot). Sensible starting points for a server that allocates heavily — raise *initial* slots so the heap doesn't repeatedly grow at boot:
190
+
191
+ ```bash
192
+ RUBY_GC_HEAP_INIT_SLOTS=600000 # 3.2 (3.3+ split per size pool)
193
+ RUBY_GC_HEAP_GROWTH_FACTOR=1.1 # grow heap gently, fewer big jumps
194
+ RUBY_GC_HEAP_FREE_SLOTS_MAX_RATIO=0.20
195
+ RUBY_GC_MALLOC_LIMIT=64000000 # delay GC triggered by malloc growth
196
+ RUBY_GC_OLDMALLOC_LIMIT=128000000
197
+ ```
198
+
199
+ Don't cargo-cult these — measure `GC.stat` before/after under realistic load. Wrong values waste RAM or trigger more majors. On Ruby 3.3+ the per-size-pool slot vars (`RUBY_GC_HEAP_%d_INIT_SLOTS`) exist; defaults are usually fine.
200
+
201
+ ### GC.compact and auto-compaction
202
+
203
+ `GC.compact` defragments the heap, improving locality and CoW sharing across forked workers. Use `GC.auto_compact = true` or compact once after boot (after eager-load), before forking Puma/Sidekiq workers.
204
+
205
+ ```ruby
206
+ # In an initializer / after eager load, before fork:
207
+ GC.compact
208
+ ```
209
+
210
+ ### Out-of-band GC
211
+
212
+ In forking servers, run a major GC *between* requests (off the hot path) so requests don't pay for it. Puma's `out_of_band` hook or `gctools`:
213
+
214
+ ```ruby
215
+ # config/puma.rb
216
+ out_of_band { GC.start } if defined?(out_of_band)
217
+ ```
218
+
219
+ Modern Ruby's incremental GC reduces the need; measure pause percentiles before adding this. Concurrency/fork details: see `references/concurrency.md`.
220
+
221
+ ## Common hotspots
222
+
223
+ ### N+1 queries
224
+
225
+ The single most common Rails performance bug. Eager-load associations; never trigger a query per row.
226
+
227
+ ```ruby
228
+ # WRONG: 1 + N queries
229
+ Post.all.each { |p| puts p.author.name }
230
+
231
+ # RIGHT: 2 queries
232
+ Post.includes(:author).each { |p| puts p.author.name }
233
+ ```
234
+
235
+ Detect with the `bullet` gem in dev. Full query-interface guidance (`includes`/`preload`/`eager_load`, `select`/`pluck`): see `references/rails.md`.
236
+
237
+ ### Unbounded loads
238
+
239
+ Never load an unbounded result set into memory.
240
+
241
+ ```ruby
242
+ # WRONG: loads the whole table at once
243
+ User.all.each { |u| process(u) }
244
+
245
+ # RIGHT: batches of 1000, constant memory
246
+ User.find_each { |u| process(u) } # iterate rows
247
+ User.in_batches(of: 5000) { |rel| rel.update_all(...) } # batch operate
248
+ User.where(active: true).pluck(:id) # only the column you need
249
+ ```
250
+
251
+ `find_each`/`in_batches`/`pluck`/`select` semantics: see `references/rails.md`.
252
+
253
+ ### Building huge strings / payloads
254
+
255
+ Stream, don't accumulate (see Streaming). For JSON, prefer `oj` or stream rather than building one giant string in memory.
256
+
257
+ ### Regexp catastrophic backtracking
258
+
259
+ Nested quantifiers over the same input (`(a+)+`, `(\w+)*`) cause exponential time on a near-match — a CPU hotspot as well as a DoS vector. Fix by anchoring and using possessive/atomic groups (`/\A(?>\w+)\z/`), and set `Regexp.timeout` as a safety net. The defensive patterns and the `Regexp.timeout` API live in `references/security.md` (ReDoS).
260
+
261
+ ### Date/Time parsing costs
262
+
263
+ `Date.parse`/`Time.parse` are slow (they sniff arbitrary formats) and ambiguous. When you know the format, use `strptime` — often 5–10x faster — or compare against precomputed values.
264
+
265
+ ```ruby
266
+ # WRONG: format-sniffing on every row
267
+ Time.parse("2026-06-09T12:00:00Z")
268
+
269
+ # RIGHT: explicit format
270
+ Time.strptime("2026-06-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%Z")
271
+ Date.strptime("2026-06-09", "%Y-%m-%d")
272
+ ```
273
+
274
+ ## Caching strategies
275
+
276
+ ### Memoization with `||=`
277
+
278
+ `@config ||= load_config` computes once per instance — a real win for repeated expensive calls. Two caveats live in the owning files: the nil/false `defined?` sentinel (`references/language-idioms.md`) and the thread-safety hazard — `||=` is not atomic, so for shared mutable caches use a `Mutex`/`Concurrent::Map` (`references/concurrency.md`). For a *parameterized* method, memoize into a hash keyed by args, not one ivar:
279
+
280
+ ```ruby
281
+ def stats_for(id) = (@stats ||= {})[id] ||= compute(id)
282
+ ```
283
+
284
+ ### Store-based caching
285
+
286
+ For cross-request/process caching, use `Rails.cache` (low-level) with a sane expiry; pick the most specific store (Solid Cache / Redis / Memcached). Use `fetch` so misses populate atomically-enough.
287
+
288
+ ```ruby
289
+ Rails.cache.fetch("user/#{user.id}/summary", expires_in: 1.hour) { build_summary(user) }
290
+ ```
291
+
292
+ Russian-doll / fragment caching for views: see `references/rails.md`.
293
+
294
+ ## Streaming & batching large data
295
+
296
+ Hold a window, not the whole dataset. Combine `find_each`/`in_batches` (DB) with `lazy` (transforms) and streaming output.
297
+
298
+ ```ruby
299
+ # CSV streaming export — constant memory, no giant String
300
+ require "csv"
301
+ File.open("export.csv", "w") do |f|
302
+ f.write CSV.generate_line(%w[id email])
303
+ User.find_each { |u| f.write CSV.generate_line([u.id, u.email]) }
304
+ end
305
+ ```
306
+
307
+ ```ruby
308
+ # Rails: stream a response body instead of buffering it
309
+ self.response_body = Enumerator.new do |yielder|
310
+ yielder << CSV.generate_line(%w[id email])
311
+ User.find_each { |u| yielder << CSV.generate_line([u.id, u.email]) }
312
+ end
313
+ ```
314
+
315
+ ```ruby
316
+ # Lazy pipeline over an infinite/huge source
317
+ (1..Float::INFINITY).lazy.select(&:even?).map { |n| n**2 }.first(5)
318
+ ```
319
+
320
+ ## Choosing data structures
321
+
322
+ Lookup cost dominates in hot loops. `Array#include?` is **O(n)**; `Set#include?` and `Hash#key?` are **O(1)**.
323
+
324
+ ```ruby
325
+ # WRONG: O(n) per check inside a loop → O(n*m) total
326
+ BANNED = ["a", "b", "c", "..."] # array
327
+ ids.select { |id| BANNED.include?(id) }
328
+
329
+ # RIGHT: O(1) membership
330
+ require "set"
331
+ BANNED = Set["a", "b", "c"].freeze
332
+ ids.select { |id| BANNED.include?(id) }
333
+
334
+ # RIGHT: Hash when you also need an associated value
335
+ INDEX = records.index_by(&:id) # Rails; one pass, then O(1) lookups
336
+ INDEX[some_id]
337
+ ```
338
+
339
+ Use `Hash#group_by`/`tally`/`index_by` to pre-build O(1) indexes instead of repeated scans. Prefer `Comparable`/`<=>`-based sorting once over repeated `min`/`max` scans.
340
+
341
+ ## YJIT
342
+
343
+ YJIT is Ruby's production JIT (mature since 3.2, faster/leaner each release). It speeds up CPU-bound Ruby method dispatch and arithmetic — typically 15–40% on real Rails apps — at a small memory cost. **Enable it; it rarely hurts.**
344
+
345
+ ```bash
346
+ ruby --yjit script.rb
347
+ RUBYOPT="--yjit" rails server
348
+ # Or in code, early at boot:
349
+ ```
350
+ ```ruby
351
+ RubyVM::YJIT.enable # Ruby 3.3+: turn on after boot (e.g. after fork-safe point)
352
+ ```
353
+
354
+ ```ruby
355
+ RubyVM::YJIT.runtime_stats # inspect compiled ratio, etc.
356
+ ```
357
+
358
+ When it helps most: CPU-bound, method-dispatch-heavy code (rendering, serialization). When it helps least: IO-bound waits (no Ruby running to compile) — there YJIT is neutral. Tune memory with `--yjit-exec-mem-size` if RSS matters. There is no good reason to leave YJIT off in production on 3.2+.
359
+
360
+ ## When to drop to a lower level
361
+
362
+ After you've cut allocations, fixed algorithms, indexed lookups, and enabled YJIT, and a *measured* hotspot is still dominated by pure Ruby compute:
363
+ - Replace with a maintained C-extension gem (e.g. `oj` for JSON, `nokogiri` for XML, `blake3`/`bcrypt`) before writing your own.
364
+ - Write a C extension or use **Rust via `rb-sys`/Magnus** only for a tight, well-bounded, heavily-profiled kernel — it's a real maintenance/portability cost.
365
+ - Consider pushing work into the database (aggregate in SQL) or another process.
366
+ - Evaluate an alternate runtime (TruffleRuby/JRuby) for CPU-bound or true-parallel workloads — see `references/concurrency.md`.
367
+
368
+ Never reach here without a profile proving the Ruby code (not IO, not GC, not the DB) is the bottleneck.
369
+
370
+ ## Quick checklist
371
+
372
+ - Profile first (vernier or stackprof; rack-mini-profiler for web). Fix the biggest item, then re-measure.
373
+ - Compare implementations with `benchmark-ips` + `compare!`, on realistically-sized data.
374
+ - Treat allocations as the cost: use `memory_profiler` (allocated vs retained); cut intermediate arrays with `filter_map`/`each_with_object`/`lazy`.
375
+ - Put `# frozen_string_literal: true` in every file; build with `<<` into a `+""` buffer or `join`, never `+=` in a loop.
376
+ - Use symbols for fixed keys; never `to_sym` untrusted/unbounded input.
377
+ - Eager-load to kill N+1; never load unbounded sets — `find_each`/`in_batches`/`pluck`/`select`.
378
+ - Use `Set`/`Hash` for membership in hot loops, not `Array#include?`; pre-build indexes with `index_by`/`tally`.
379
+ - Avoid `Time.parse`/`Date.parse` in hot paths — use `strptime`. Guard regexes against ReDoS; set `Regexp.timeout`.
380
+ - Memoize with `||=` (mind nil/false and thread-safety); use `Rails.cache.fetch` for cross-request caching.
381
+ - Read `GC.stat`/`GC.total_time` before touching `RUBY_GC_HEAP_*`; `GC.compact` after eager-load before fork.
382
+ - Enable YJIT in production on Ruby 3.2+.
383
+ - Drop to a C/Rust extension only for a profiled, isolated kernel — prefer an existing native gem.