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,460 @@
1
+ # Errors, exceptions & type checking
2
+
3
+ Modern Ruby (3.2–3.4) error handling, plus optional static typing with RBS and Sorbet. Dense, idiomatic, do/don't.
4
+
5
+ ## The exception hierarchy
6
+
7
+ ```
8
+ Exception
9
+ ├── NoMemoryError, SystemExit, SignalException, ScriptError, ... # do NOT rescue these
10
+ └── StandardError # rescue THIS
11
+ ├── ArgumentError, TypeError, KeyError, IndexError, NameError
12
+ ├── RuntimeError # default class for `raise "msg"`
13
+ ├── IOError, Errno::*
14
+ └── your custom errors (subclass StandardError)
15
+ ```
16
+
17
+ `rescue` with no class rescues `StandardError`, **not** `Exception`. Rescuing `Exception` swallows `SignalException` (Ctrl-C), `SystemExit` (`exit`), and `NoMemoryError` — breaking process control and hiding fatal bugs.
18
+
19
+ ```ruby
20
+ # WRONG — catches Ctrl-C, exit, and out-of-memory; nearly always a bug
21
+ begin
22
+ do_work
23
+ rescue Exception => e
24
+ log(e)
25
+ end
26
+
27
+ # RIGHT — bare rescue defaults to StandardError
28
+ begin
29
+ do_work
30
+ rescue => e # == rescue StandardError => e
31
+ log(e)
32
+ end
33
+ ```
34
+
35
+ Only rescue what you can handle. Catch specific classes when you can do something specific; let everything else propagate.
36
+
37
+ ```ruby
38
+ begin
39
+ parse(payload)
40
+ rescue JSON::ParserError => e # specific: we know how to recover
41
+ fallback
42
+ rescue KeyError => e # specific: missing field
43
+ report_missing(e.key)
44
+ end
45
+ # Anything else bubbles up — good.
46
+ ```
47
+
48
+ ## Raising well
49
+
50
+ ```ruby
51
+ raise "boom" # RuntimeError, message "boom"
52
+ raise ArgumentError, "name required" # class + message — the common form
53
+ raise ArgumentError.new("name required")
54
+ raise MyError.new(code: 42) # custom class with structured data
55
+ raise # re-raise the current exception (inside rescue)
56
+ ```
57
+
58
+ Prefer `raise Class, "message"` over constructing the instance unless you need to pass extra constructor args. Always raise a *class*, never a string-only `raise` for library code where callers may want to rescue a specific type.
59
+
60
+ `fail` is an alias of `raise`. Some style guides used `fail` for the first raise and `raise` for re-raises; today **prefer `raise` everywhere** for consistency (RuboCop's default `Style/SignalException` enforces `raise`).
61
+
62
+ ## Custom error classes & a per-library base error
63
+
64
+ Give every library/app a single base error so callers can `rescue MyLib::Error` to catch *anything* from it. Subclass `StandardError`, never `Exception`.
65
+
66
+ ```ruby
67
+ module Billing
68
+ class Error < StandardError; end # base — one per library
69
+
70
+ class PaymentDeclined < Error
71
+ attr_reader :code, :gateway_ref
72
+
73
+ def initialize(code:, gateway_ref:, message: nil)
74
+ @code = code
75
+ @gateway_ref = gateway_ref
76
+ super(message || "Payment declined (#{code})")
77
+ end
78
+ end
79
+
80
+ class RateLimited < Error
81
+ attr_reader :retry_after
82
+ def initialize(retry_after)
83
+ @retry_after = retry_after
84
+ super("Rate limited; retry after #{retry_after}s")
85
+ end
86
+ end
87
+ end
88
+
89
+ # Caller can be coarse or fine-grained:
90
+ begin
91
+ Billing.charge(card)
92
+ rescue Billing::PaymentDeclined => e
93
+ notify_user(e.code)
94
+ rescue Billing::Error => e # catch-all for this library only
95
+ retry_later(e)
96
+ end
97
+ ```
98
+
99
+ Rules for custom errors:
100
+ - Always call `super(message)` so `#message`/`#to_s` work.
101
+ - Expose structured data via `attr_reader`, not by stuffing it into the message string.
102
+ - Keep the hierarchy shallow (base + a handful of leaf classes).
103
+
104
+ ## begin / rescue / else / ensure / retry
105
+
106
+ ```ruby
107
+ begin
108
+ result = risky
109
+ rescue SomeError => e
110
+ handle(e) # runs only on error
111
+ else
112
+ use(result) # runs only when NO exception was raised
113
+ ensure
114
+ cleanup # ALWAYS runs (success, error, return, or break)
115
+ end
116
+ ```
117
+
118
+ - `else` holds the "happy path" code that must *not* be guarded by the rescue. Keeps the `begin` block to just the risky call.
119
+ - `ensure` always runs — use it for cleanup. Do **not** `return` from `ensure`; it silently swallows the in-flight exception/return value.
120
+
121
+ ```ruby
122
+ # WRONG — return in ensure eats the exception
123
+ def f
124
+ raise "x"
125
+ ensure
126
+ return 1 # caller gets 1, exception vanishes. Never do this.
127
+ end
128
+ ```
129
+
130
+ ### Method-level rescue (no explicit begin)
131
+
132
+ `def`, blocks, and `do...end` have implicit begin/ensure scopes:
133
+
134
+ ```ruby
135
+ def fetch
136
+ api_call
137
+ rescue Timeout::Error => e
138
+ retry_or_raise(e)
139
+ ensure
140
+ close_connection
141
+ end
142
+ ```
143
+
144
+ ### retry with a backoff cap
145
+
146
+ `retry` re-runs the `begin` block. **Always cap attempts** or you get infinite loops. Add backoff (ideally with jitter) for network calls.
147
+
148
+ ```ruby
149
+ def with_retries(max: 3, base: 0.5)
150
+ attempts = 0
151
+ begin
152
+ yield
153
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
154
+ attempts += 1
155
+ raise if attempts >= max # give up — re-raise last error
156
+ sleep(base * (2 ** (attempts - 1)) + rand * 0.1) # exp backoff + jitter
157
+ retry
158
+ end
159
+ end
160
+
161
+ with_retries(max: 4) { http.get(url) }
162
+ ```
163
+
164
+ Do **not** `retry` on programmer errors (`ArgumentError`, `NoMethodError`) — they won't fix themselves. Only retry transient failures.
165
+
166
+ ## Rescue modifier & inline-rescue pitfalls
167
+
168
+ The one-line `expr rescue fallback` form catches **`StandardError`** and discards the exception object. It's a blunt instrument.
169
+
170
+ ```ruby
171
+ value = Integer(str) rescue 0 # ok-ish: narrow, intentional fallback
172
+ ```
173
+
174
+ Pitfalls:
175
+ - It hides *every* `StandardError`, not just the one you expect (e.g. a typo `NameError` becomes `0`).
176
+ - You can't inspect the error.
177
+ - It's easy to over-scope.
178
+
179
+ ```ruby
180
+ # WRONG — masks bugs; if `parse` raises NoMethodError you silently get nil
181
+ data = parse(payload) rescue nil
182
+
183
+ # RIGHT — name the error you actually expect
184
+ data =
185
+ begin
186
+ parse(payload)
187
+ rescue JSON::ParserError
188
+ nil
189
+ end
190
+ ```
191
+
192
+ For "give me nil on a known failure", prefer purpose-built methods: `Integer(s, exception: false)`, `Float(s, exception: false)`, `hash.dig`, `Array.fetch` with default, etc.
193
+
194
+ ```ruby
195
+ Integer("x", exception: false) # => nil, no rescue needed
196
+ ```
197
+
198
+ ## Inspecting the rescued exception
199
+
200
+ ```ruby
201
+ rescue => e
202
+ e.message # the message string
203
+ e.class # e.g. KeyError
204
+ e.backtrace # Array<String>
205
+ e.full_message # formatted message + backtrace + cause chain (great for logs)
206
+ e.cause # the exception that was in flight when this one was raised
207
+ ```
208
+
209
+ `e.full_message(highlight: false)` gives plain text suited for log files; `highlight: true` (default on a TTY) adds ANSI color.
210
+
211
+ ## Cause chaining (`Exception#cause`)
212
+
213
+ When you `raise` *inside* a `rescue`, Ruby automatically sets the new exception's `#cause` to the one being handled — preserving the original. Do **not** manually thread the original through unless you want to override it.
214
+
215
+ ```ruby
216
+ def load_config
217
+ YAML.safe_load_file(path)
218
+ rescue Psych::SyntaxError => e
219
+ raise ConfigError, "config #{path} is invalid"
220
+ # e becomes the implicit cause; full_message shows both
221
+ end
222
+ ```
223
+
224
+ `full_message` then prints:
225
+
226
+ ```
227
+ ConfigError: config /etc/app.yml is invalid
228
+ ...
229
+ caused by: Psych::SyntaxError: (...) ...
230
+ ```
231
+
232
+ Override or suppress the implicit cause explicitly:
233
+
234
+ ```ruby
235
+ raise ConfigError, "bad config", cause: nil # drop the chain
236
+ raise ConfigError.new("bad config"), cause: original # set a specific cause
237
+ ```
238
+
239
+ Wrap low-level errors in your library's error type while keeping the cause, so callers get a stable interface and you don't lose the root cause:
240
+
241
+ ```ruby
242
+ rescue PG::Error => e
243
+ raise Repo::DatabaseError, "query failed" # cause = the PG error, preserved
244
+ ```
245
+
246
+ ## Resource cleanup: prefer auto-closing blocks over manual ensure
247
+
248
+ If an API offers a block form that closes/releases automatically, use it. Reach for `ensure` only when there's no block form.
249
+
250
+ ```ruby
251
+ # RIGHT — block form closes the file even on exception
252
+ File.open(path, "r") do |f|
253
+ process(f)
254
+ end
255
+
256
+ # Manual equivalent — only when no block form exists
257
+ f = acquire_resource
258
+ begin
259
+ process(f)
260
+ ensure
261
+ f.release # runs on success and on error
262
+ end
263
+ ```
264
+
265
+ Common block-closing APIs: `File.open`, `Tempfile.create`, `Net::HTTP.start`, `Mutex#synchronize`, `connection_pool.with`, `ActiveRecord::Base.transaction` (rolls back on raise). Don't hand-roll `ensure` when one of these fits.
266
+
267
+ ## Structured error data & custom messages
268
+
269
+ Put machine-readable detail in attributes; keep the message human-readable.
270
+
271
+ ```ruby
272
+ class ValidationError < StandardError
273
+ attr_reader :errors # e.g. { email: ["is invalid"], age: ["too low"] }
274
+
275
+ def initialize(errors)
276
+ @errors = errors
277
+ super("Validation failed: #{errors.keys.join(', ')}")
278
+ end
279
+ end
280
+
281
+ begin
282
+ validate!(form)
283
+ rescue ValidationError => e
284
+ render json: e.errors, status: :unprocessable_entity
285
+ end
286
+ ```
287
+
288
+ This is cleaner than parsing strings out of `e.message`, and it survives i18n of the message.
289
+
290
+ ## Exceptions vs Result objects
291
+
292
+ Use **exceptions** for genuinely exceptional / unexpected conditions and for cross-cutting failures you want to bubble up (DB down, bug, programmer error). Use a **Result object** when failure is an expected, *modeled* outcome that the caller must branch on (validation, "user not found", payment declined in a flow). Don't use exceptions for ordinary control flow — they're slow on the raise path and obscure intent.
293
+
294
+ ```ruby
295
+ # Exception: unexpected
296
+ raise Repo::DatabaseError if conn.dead?
297
+
298
+ # Result: expected branch the caller handles
299
+ result = ChargeCard.call(card)
300
+ if result.success?
301
+ render :receipt
302
+ else
303
+ render :declined, locals: { reason: result.error }
304
+ end
305
+ ```
306
+
307
+ Result/Either object design, `.success?`/`.failure?` shapes, and the `dry-monads` approach are covered in **See references/oo-design.md**. Testing that code raises (`raise_error` matcher, etc.) is in **See references/testing.md**.
308
+
309
+ ## Warnings & deprecations
310
+
311
+ `warn` writes to `$stderr` (suppressed by `-W0` / `$VERBOSE = nil`). Use it for non-fatal advisories.
312
+
313
+ ```ruby
314
+ warn "[MyLib] #{old} is deprecated; use #{new}", category: :deprecated
315
+ ```
316
+
317
+ `category: :deprecated` (Ruby 3.0+) lets users filter: `Warning[:deprecated] = false` silences deprecation warnings globally. Gate noisy ones behind it.
318
+
319
+ Intercept/route warnings (e.g. to your logger, or to fail tests on warnings) via the `Warning` module:
320
+
321
+ ```ruby
322
+ module Warning
323
+ def self.warn(msg, category: nil)
324
+ Rails.logger.warn(msg) # or: raise in test env to surface them
325
+ end
326
+ end
327
+ ```
328
+
329
+ Rails: prefer `ActiveSupport::Deprecation` instances for library-style deprecations so behavior, horizon, and silencing are configurable:
330
+
331
+ ```ruby
332
+ DEPRECATOR = ActiveSupport::Deprecation.new("2.0", "MyGem")
333
+ def old_api(*) = DEPRECATOR.warn("old_api is deprecated; use new_api") || new_api(*)
334
+ ```
335
+
336
+ ---
337
+
338
+ # Type checking (optional)
339
+
340
+ Ruby is dynamically typed; static typing is **opt-in** and additive. Two ecosystems: **RBS + Steep** (official, separate sig files) and **Sorbet** (inline `sig`, runtime + static). They don't mix per-file; pick one per project.
341
+
342
+ ## RBS + Steep
343
+
344
+ RBS is Ruby's standard type-signature language. Signatures live in separate `.rbs` files (typically under `sig/`). `steep` is the type checker; `rbs collection` manages third-party signatures.
345
+
346
+ ```rbs
347
+ # sig/billing.rbs
348
+ module Billing
349
+ class PaymentDeclined < StandardError
350
+ attr_reader code: Integer
351
+ attr_reader gateway_ref: String
352
+ def initialize: (code: Integer, gateway_ref: String, ?message: String?) -> void
353
+ end
354
+
355
+ def self.charge: (Card) -> Result
356
+ end
357
+ ```
358
+
359
+ ```ruby
360
+ # Steepfile
361
+ target :app do
362
+ signature "sig"
363
+ check "lib"
364
+ # library "json", "logger" # pull in stdlib sigs
365
+ end
366
+ ```
367
+
368
+ ```bash
369
+ gem install rbs steep
370
+ rbs collection init # creates rbs_collection.yaml (gem_rbs_collection)
371
+ rbs collection install # vendors third-party .rbs into .gem_rbs_collection
372
+ steep check # type-check the project
373
+ rbs prototype rb lib/x.rb # scaffold an initial .rbs from existing code
374
+ ```
375
+
376
+ Pros: official, no runtime cost, no source pollution, gradual. Cons: signatures live apart from code (can drift), tooling/editor support less mature than Sorbet, inference is weaker.
377
+
378
+ ## Inline RBS comments (Ruby 3.x)
379
+
380
+ Ruby 3.x (with RBS 3.x / Steep) supports type annotations as **special comments** next to the code, so signatures sit beside implementation without a separate file:
381
+
382
+ ```ruby
383
+ # @rbs name: String
384
+ # @rbs return: Integer
385
+ def length_of(name)
386
+ name.length
387
+ end
388
+
389
+ xs = [] #: Array[String]
390
+ config = fetch #: Config
391
+ ```
392
+
393
+ This is a pragmatic middle ground: keeps types near code while staying valid Ruby (they're comments). Steep reads them. Good for incremental adoption.
394
+
395
+ ## Sorbet
396
+
397
+ Sorbet uses inline `sig` blocks plus `T.*` helpers, and checks both **statically** (`srb tc`) and **at runtime** (the `sig` enforces types when the method runs, raising `TypeError` on violation).
398
+
399
+ ```ruby
400
+ # typed: true
401
+ require "sorbet-runtime"
402
+
403
+ class Box
404
+ extend T::Sig
405
+
406
+ sig { params(value: Integer).returns(String) }
407
+ def label(value)
408
+ "##{value}"
409
+ end
410
+
411
+ sig { void }
412
+ def initialize
413
+ @items = T.let([], T::Array[String]) # declare ivar type
414
+ end
415
+ end
416
+ ```
417
+
418
+ ```bash
419
+ gem install sorbet sorbet-runtime
420
+ srb init # generates sorbet/ + RBI files for gems
421
+ srb tc # static type check
422
+ ```
423
+
424
+ - `# typed: false | true | strict | strong` sigil per file controls strictness.
425
+ - `T.let`, `T.cast`, `T.must` (assert non-nil), `T.nilable(X)`, `T.any(A, B)`, `T.untyped`.
426
+ - RBI files (`.rbi`) describe gems/Rails; `tapioca` generates them.
427
+
428
+ Pros: mature IDE support, runtime enforcement catches violations in tests, strong inference. Cons: runtime overhead from `sig` checks, source is more verbose / Sorbet-specific, RBI maintenance, `T.untyped` escape hatches erode guarantees.
429
+
430
+ ## When typing pays off — tradeoffs
431
+
432
+ Worth it:
433
+ - Large/long-lived codebases and libraries with many callers (signatures = enforced docs).
434
+ - Public gem APIs — ship `.rbs` so consumers get checking.
435
+ - Refactors across big surfaces; catching `nil` and arity errors before runtime.
436
+
437
+ Skip / defer:
438
+ - Small scripts, spikes, short-lived code — overhead outweighs benefit.
439
+ - Highly metaprogrammed code (`method_missing`, dynamic `define_method`) — hard to type; needs manual sigs and often `T.untyped`. See references/metaprogramming.md.
440
+
441
+ Guidance: start `# typed: false`/loose, type the **boundaries** (public methods, models, service `#call`) first, tighten incrementally. Don't let `T.untyped`/`T.must` proliferate — each one is an unchecked hole. Types complement tests; they don't replace them (See references/testing.md).
442
+
443
+ ## Quick checklist
444
+
445
+ - `rescue` (bare) == `rescue StandardError`. **Never `rescue Exception`** in normal code.
446
+ - Rescue the **most specific** class you can actually handle; let the rest propagate.
447
+ - `raise Class, "message"` is the default form; raise classes, not strings, in libraries.
448
+ - Give each library one base error subclassing `StandardError`; keep the hierarchy shallow.
449
+ - Put structured detail in `attr_reader`s; keep `#message` human-readable.
450
+ - Always `super(message)` in custom error `initialize`.
451
+ - Cap `retry` with a max-attempt count and exponential backoff + jitter; only retry transient errors.
452
+ - Never `return` from `ensure`. Use `else` for the happy path.
453
+ - Prefer block-closing APIs (`File.open { }`, `transaction { }`) over manual `ensure`.
454
+ - Avoid `expr rescue fallback` for anything but narrow, intentional fallbacks; prefer `Integer(s, exception: false)` & friends.
455
+ - Re-raising inside `rescue` sets `#cause` automatically — wrap low-level errors in your type without losing the root.
456
+ - Log with `e.full_message`; it includes the cause chain.
457
+ - Use `fail`? No — prefer `raise` everywhere.
458
+ - Exceptions for the unexpected; Result objects for expected, branchable failures (See references/oo-design.md).
459
+ - `warn(..., category: :deprecated)`; route via `Warning.warn`; use `ActiveSupport::Deprecation` in Rails.
460
+ - Typing: RBS+Steep (official, separate sigs, inline `#:` comments) or Sorbet (`sig`, runtime+static). Type boundaries first; minimize `T.untyped`.