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,465 @@
1
+ # Ruby language & core idioms
2
+
3
+ Modern Ruby (3.2–3.4) fundamentals. Dense, idiomatic, do/don't. For metaprogramming see `references/metaprogramming.md`; OO design see `references/oo-design.md`; exceptions see `references/errors-and-types.md`; concurrency see `references/concurrency.md`.
4
+
5
+ ## Truthiness & nil
6
+
7
+ Only `false` and `nil` are falsey. `0`, `""`, `[]`, `{}` are all **truthy**.
8
+
9
+ ```ruby
10
+ if count # true even when count == 0
11
+ if count.positive? # what you usually meant
12
+ if list.any? # not `if list` — empty array is truthy
13
+ ```
14
+
15
+ Distinguish "missing" from "falsey value". `||` collapses both `nil` and `false`; use `nil?` / `key?` when `false` is a legitimate value.
16
+
17
+ ```ruby
18
+ flag = opts[:enabled] || true # WRONG: explicit false becomes true
19
+ flag = opts.fetch(:enabled, true) # RIGHT: only defaults when key absent
20
+ ```
21
+
22
+ ### Safe navigation `&.`
23
+
24
+ `&.` short-circuits on `nil` only. It does **not** guard against other return values.
25
+
26
+ ```ruby
27
+ user&.address&.city # nil if any link is nil
28
+ config&.fetch(:host) # still raises if config present but key missing
29
+
30
+ # DON'T chain &. to paper over a design smell (Law of Demeter — see oo-design.md).
31
+ # DON'T mix with ||: `a &. b || c` parses as `(a&.b) || c` — usually fine, but be explicit.
32
+ ```
33
+
34
+ `&.` differs from `try` (Rails): `&.` calls the method and raises NoMethodError if the receiver is non-nil but doesn't respond; `try` swallows that. Prefer `&.`.
35
+
36
+ ### `||=`, `&&=`
37
+
38
+ ```ruby
39
+ @cache ||= compute # memoize (NOT thread-safe — see performance.md / concurrency.md)
40
+ h[:k] ||= [] # default-then-append; for hashes prefer Hash.new { |h,k| h[k] = [] }
41
+ config &&= config.dup # reassign only if already truthy
42
+ ```
43
+
44
+ `||=` on a falsey-but-valid value recomputes every time. If `compute` can return `false`/`nil`, memoize with a sentinel:
45
+
46
+ ```ruby
47
+ @result = compute unless defined?(@result)
48
+ ```
49
+
50
+ ## Symbols vs strings
51
+
52
+ Symbols are immutable, interned, identity-comparable — use them as **identifiers/keys**. Strings are for **data/text**.
53
+
54
+ ```ruby
55
+ status == :active # state/identifier -> symbol
56
+ record.name == "Acme" # human data -> string
57
+ ```
58
+
59
+ Don't `to_sym` untrusted/unbounded input in long-lived processes (symbols from user input are GC'd since 2.2, but still avoid for clarity/security — see security.md).
60
+
61
+ ### Frozen string literals
62
+
63
+ Put this **magic comment** on line 1 of every file (it must be the first line, or after `#!`):
64
+
65
+ ```ruby
66
+ # frozen_string_literal: true
67
+ ```
68
+
69
+ All string literals in the file become frozen → fewer allocations (see performance.md), and accidental mutation raises. When you need a mutable buffer, allocate explicitly:
70
+
71
+ ```ruby
72
+ buf = +"" # unary + = dup'd, mutable string
73
+ buf << "a" << "b"
74
+ name = -"active" # unary - = frozen/deduplicated
75
+ ```
76
+
77
+ ### String building
78
+
79
+ ```ruby
80
+ # DON'T build with repeated + in a loop (O(n²) allocations)
81
+ out = ""; items.each { |i| out = out + i.to_s } # BAD
82
+
83
+ # DO use << (mutating) or join
84
+ out = +""; items.each { |i| out << i.to_s }
85
+ out = items.map(&:to_s).join(", ")
86
+ out = "#{name} (#{count})" # interpolation > concatenation
87
+ out = format("%.2f%%", pct) # format/sprintf for padding/precision
88
+ ```
89
+
90
+ `<<` mutates left operand; `+` allocates a new string. Heredocs with `<<~` strip leading indentation:
91
+
92
+ ```ruby
93
+ sql = <<~SQL
94
+ SELECT *
95
+ FROM users
96
+ SQL
97
+ ```
98
+
99
+ ## Method arguments
100
+
101
+ ### Keyword arguments (Ruby 3.x keeps them fully separate from positionals)
102
+
103
+ ```ruby
104
+ def connect(host:, port: 5432, **opts) # host required, port optional, rest in opts
105
+ ...
106
+ end
107
+ connect(host: "db", timeout: 5) # timeout lands in opts
108
+ ```
109
+
110
+ - `name:` (no default) = required keyword.
111
+ - `name: default` = optional.
112
+ - `**opts` = collect extra keywords; `**nil` forbids any keywords.
113
+ - Prefer keyword args when a method takes 3+ params or any boolean flag (avoids mystery `true, false, nil` call sites).
114
+
115
+ ```ruby
116
+ render(partial, true, false) # DON'T: unreadable
117
+ render(partial, layout: true, cache: false) # DO
118
+ ```
119
+
120
+ Splatting a hash into keywords needs explicit `**` in 3.x:
121
+
122
+ ```ruby
123
+ opts = { host: "db", port: 5432 }
124
+ connect(**opts) # required; bare `connect(opts)` raises ArgumentError
125
+ ```
126
+
127
+ ### Argument forwarding `...`
128
+
129
+ Forward all args (positional, keyword, block, and 3.2+ anonymous splats) verbatim:
130
+
131
+ ```ruby
132
+ def log_and_call(...)
133
+ logger.info("calling")
134
+ target.call(...)
135
+ end
136
+ ```
137
+
138
+ Ruby 3.2 also allows anonymous `*`, `**`, `&` forwarding:
139
+
140
+ ```ruby
141
+ def wrap(*, **, &) = inner(*, **, &)
142
+ ```
143
+
144
+ ## Blocks, procs, lambdas
145
+
146
+ ```ruby
147
+ [1,2,3].each { |n| puts n } # block — not an object, passed implicitly
148
+ sq = ->(n) { n * n } # lambda (stabby)
149
+ pr = proc { |n| n * n } # proc
150
+ ```
151
+
152
+ ### Return semantics (the key difference)
153
+
154
+ - **lambda**: `return` returns from the lambda; strict arity.
155
+ - **proc / block**: `return` returns from the **enclosing method**; lenient arity (missing args → nil, extra ignored).
156
+
157
+ ```ruby
158
+ def find_first(list)
159
+ list.each { |x| return x if x.positive? } # block return -> exits find_first ✔
160
+ end
161
+
162
+ def m
163
+ p = proc { return 42 }
164
+ p.call # returns from m
165
+ 99 # never reached
166
+ end
167
+ # DON'T store a proc and call it later expecting local return — it LocalJumpErrors once m has returned.
168
+ ```
169
+
170
+ Prefer lambdas for stored callables (predictable return + arity checking).
171
+
172
+ ### yield, block_given?, &block
173
+
174
+ ```ruby
175
+ def each_pair
176
+ return enum_for(:each_pair) unless block_given? # return Enumerator if no block
177
+ yield :a, 1
178
+ yield :b, 2
179
+ end
180
+
181
+ def with_capture(&block) # capture only when you must pass it on / store it
182
+ block.call(self)
183
+ end
184
+ ```
185
+
186
+ `yield` is faster than `&block` + `block.call` (no Proc allocation). Capture with `&block` only when forwarding or storing it.
187
+
188
+ ### Symbol#to_proc and method references
189
+
190
+ ```ruby
191
+ %w[a b].map(&:upcase) # &:sym -> ->(x){ x.upcase }
192
+ [1,-2].map(&:abs)
193
+ nums.map(&method(:format_row)) # method object as block
194
+ ```
195
+
196
+ ## Enumerable toolbox — pick the right tool
197
+
198
+ ```ruby
199
+ map # transform 1:1
200
+ flat_map # map then flatten one level (map+flatten -> flat_map)
201
+ filter_map # map + compact + select in one pass (3.x); great for "transform then drop nils"
202
+ select / filter # keep matching reject # drop matching
203
+ each_with_object # build a collection; returns the object (not the block value)
204
+ reduce / inject # fold to single value
205
+ sum # numeric/string fold (sum(0.0) to start as float)
206
+ tally # frequency Hash {elem => count}
207
+ group_by # Hash {key => [elems]}
208
+ partition # [matching, non_matching]
209
+ chunk_while / slice_when # split into runs by adjacent-pair predicate
210
+ each_slice(n) # fixed-size batches each_cons(n) # sliding windows
211
+ zip # interleave/pair parallel collections
212
+ min_by/max_by/sort_by/minmax_by # by derived key (computed once via Schwartzian)
213
+ find / detect # first match
214
+ ```
215
+
216
+ ```ruby
217
+ # filter_map vs map+compact
218
+ emails = users.filter_map { |u| u.email if u.active? } # one pass ✔
219
+ emails = users.map { |u| u.email if u.active? }.compact # two passes ✗
220
+
221
+ # each_with_object vs reduce for building
222
+ index = items.each_with_object({}) { |i, h| h[i.id] = i } # clean
223
+ index = items.reduce({}) { |h, i| h[i.id] = i; h } # must return h — error-prone
224
+
225
+ # counting -> tally, not manual hash
226
+ %w[a b a].tally # {"a"=>2, "b"=>1}
227
+
228
+ # grouped sums
229
+ totals = orders.group_by(&:user_id).transform_values { |os| os.sum(&:amount) }
230
+ ```
231
+
232
+ `sort_by`/`max_by` compute the key once per element — prefer over `sort { |a,b| f(a) <=> f(b) }` when the key is expensive. Use `sort`/`<=>` block only for multi-key or mixed-direction sorts:
233
+
234
+ ```ruby
235
+ people.sort_by { |p| [p.last, p.first] } # multi-key ascending
236
+ people.sort_by { |p| [-p.age, p.name] } # age desc, name asc (negate numeric key)
237
+ ```
238
+
239
+ ### Lazy enumerators (large/infinite/streaming)
240
+
241
+ `lazy` defers and pipelines per-element — no giant intermediate arrays. Essential for files and infinite ranges.
242
+
243
+ ```ruby
244
+ (1..Float::INFINITY).lazy.select(&:even?).first(5) # [2,4,6,8,10]
245
+
246
+ File.foreach("huge.log").lazy
247
+ .map(&:chomp)
248
+ .select { |l| l.include?("ERROR") }
249
+ .first(100) # stops reading after 100 matches
250
+ ```
251
+
252
+ Call a terminal op (`first`, `to_a`, `force`, `each`) to materialize. DON'T `.lazy` short in-memory arrays — overhead outweighs benefit.
253
+
254
+ ## Comparable & Enumerable mixins on your own classes
255
+
256
+ Define `<=>` (returns -1/0/1/nil) and include `Comparable` to get `< <= == > >= between? clamp`:
257
+
258
+ ```ruby
259
+ class Version
260
+ include Comparable
261
+ attr_reader :parts
262
+ def initialize(str) = @parts = str.split(".").map(&:to_i)
263
+ def <=>(other) = parts <=> other.parts # Array#<=> compares elementwise
264
+ end
265
+ Version.new("1.2.0") < Version.new("1.10.0") # => true
266
+ ```
267
+
268
+ Define `each` and include `Enumerable` to get the whole toolbox above:
269
+
270
+ ```ruby
271
+ class Roster
272
+ include Enumerable
273
+ def initialize(members) = @members = members
274
+ def each(&block) = @members.each(&block) # yield each element
275
+ end
276
+ Roster.new(people).map(&:name).sort # map/select/sort_by all work now
277
+ ```
278
+
279
+ ## Pattern matching (`case/in`)
280
+
281
+ Structural matching with binding. `in` raises `NoMatchingPatternError` if nothing matches (use `else` to handle). Use `case/in` for shape; `case/when` for simple equality.
282
+
283
+ ```ruby
284
+ case response
285
+ in { status: 200, body: } # binds body
286
+ body
287
+ in { status: 404 }
288
+ raise NotFound
289
+ in { status: Integer => code } if code >= 500
290
+ retry_later(code)
291
+ else
292
+ raise "unexpected"
293
+ end
294
+ ```
295
+
296
+ Array, find, and alternative patterns:
297
+
298
+ ```ruby
299
+ case command
300
+ in [:move, Integer => x, Integer => y] # array pattern, type + bind
301
+ move(x, y)
302
+ in [:say, *words] # splat captures rest
303
+ say(words.join(" "))
304
+ in [*, {error:}, *] # find pattern: locate elem anywhere
305
+ fail error
306
+ in :start | :resume # alternative pattern
307
+ begin!
308
+ end
309
+ ```
310
+
311
+ One-line `=>` (rightward assignment / destructuring) and `in` as a boolean test:
312
+
313
+ ```ruby
314
+ config => { host:, port: } # raises if no match; binds host, port
315
+ record in { id: Integer } # => true/false, no raise
316
+ ```
317
+
318
+ ### deconstruct / deconstruct_keys
319
+
320
+ Make your objects matchable. Array patterns call `deconstruct`; hash patterns call `deconstruct_keys(keys)`. `Data` and `Struct` implement both automatically.
321
+
322
+ ```ruby
323
+ Point = Data.define(:x, :y)
324
+ case Point.new(1, 2)
325
+ in [x, y] then ... # via deconstruct
326
+ in { x:, y: } then ... # via deconstruct_keys
327
+ end
328
+ ```
329
+
330
+ ## Struct vs Data.define
331
+
332
+ Use **`Data.define`** (Ruby 3.2+) for immutable value objects — the modern default. Use `Struct` only when you need mutability or backward compat.
333
+
334
+ ```ruby
335
+ Point = Data.define(:x, :y) do
336
+ def dist = Math.hypot(x, y)
337
+ end
338
+ p = Point.new(x: 1, y: 2) # or Point.new(1, 2)
339
+ p.with(y: 9) # returns a NEW Point (copy-with-change)
340
+ p.x = 0 # NoMethodError — frozen, no setters ✔
341
+ ```
342
+
343
+ - `Data`: immutable, no setters, value `==`, `deconstruct`/`deconstruct_keys`, `#with`. Ideal for DTOs/value objects (see oo-design.md).
344
+ - `Struct`: mutable (`s.x = 1`), positional **or** `keyword_init: true`. Pick one and be consistent.
345
+
346
+ ```ruby
347
+ Mutable = Struct.new(:x, :y, keyword_init: true) # always pass keyword_init explicitly
348
+ ```
349
+
350
+ DON'T use `OpenStruct` — slow, defeats method-missing safety, allocations galore.
351
+
352
+ ## Hash idioms
353
+
354
+ ```ruby
355
+ h.fetch(:k) # raises KeyError if missing — use when key MUST exist
356
+ h.fetch(:k, default) # default value
357
+ h.fetch(:k) { expensive } # block form — default computed only if needed
358
+ h[:k] # returns nil if missing (can't tell missing from nil value)
359
+
360
+ h.dig(:a, :b, :c) # safe nested access; nil if any level missing
361
+ data.dig(:users, 0, :name) # works across Hash/Array
362
+
363
+ Hash.new(0) # default value 0 (SHARED — don't use mutable default!)
364
+ Hash.new { |hash, key| hash[key] = [] } # default BLOCK — fresh array per key ✔
365
+
366
+ counts = Hash.new(0); words.each { |w| counts[w] += 1 } # or just words.tally
367
+
368
+ h.transform_values { |v| v * 2 }
369
+ h.transform_keys(&:to_sym)
370
+ h.filter_map { |k, v| [k, v] if v } # works on hashes too
371
+ h.slice(:a, :b) / h.except(:c)
372
+ h1.merge(h2) { |key, old, new| old + new } # block resolves conflicts
373
+ ```
374
+
375
+ ```ruby
376
+ Hash.new([]).tap { |h| h[:a] << 1 } # BUG: every key shares ONE array
377
+ Hash.new { |h,k| h[k] = [] } # correct
378
+ ```
379
+
380
+ ## Syntax niceties
381
+
382
+ ### Endless methods (Ruby 3.0+)
383
+
384
+ ```ruby
385
+ def square(n) = n * n
386
+ def full_name = "#{first} #{last}"
387
+ def active? = status == :active
388
+ ```
389
+
390
+ Use for true one-liners (no `begin`/multi-statement). Keep the `= expr` on one logical line.
391
+
392
+ ### Numbered & `it` block params
393
+
394
+ ```ruby
395
+ [1,2,3].map { _1 * 2 } # _1.._9 numbered params (2.7+)
396
+ pairs.each { puts "#{_1}=#{_2}" }
397
+ [1,2,3].map { it * 2 } # `it` = single implicit param (Ruby 3.4)
398
+ ```
399
+
400
+ Prefer named params for anything non-trivial or nested (you can't use `_1` across nesting levels cleanly). `it`/`_1` shine for short single-arg blocks.
401
+
402
+ ### Range tricks
403
+
404
+ ```ruby
405
+ (1..5) # inclusive (1...5) # exclusive end
406
+ (1..) # beginless/endless ranges
407
+ arr[2..] # from index 2 to end
408
+ arr[..3] # up to index 3
409
+ ("a".."e").to_a
410
+ (1..10).step(2).to_a
411
+ case score
412
+ in 90.. then "A" # endless range in pattern
413
+ in 80...90 then "B"
414
+ end
415
+ (Time.now..).cover?(t) # ranges as predicates via cover?/include?
416
+ ```
417
+
418
+ ## Numbers & money
419
+
420
+ `Float` is binary floating point — **never** use it for money or exact decimals.
421
+
422
+ ```ruby
423
+ 0.1 + 0.2 == 0.3 # => false (0.30000000000000004)
424
+ ```
425
+
426
+ Use `BigDecimal` (from `require "bigdecimal"`/`"bigdecimal/util"`) for currency:
427
+
428
+ ```ruby
429
+ require "bigdecimal"
430
+ require "bigdecimal/util"
431
+
432
+ price = "19.99".to_d # BigDecimal, exact
433
+ total = price * 3 # 59.97 exact
434
+ BigDecimal("0.1") + BigDecimal("0.2") == BigDecimal("0.3") # => true
435
+
436
+ # DON'T construct BigDecimal from a Float — you inherit the float error:
437
+ BigDecimal(0.1, 10) # BAD-ish; use the string: BigDecimal("0.1")
438
+ ```
439
+
440
+ Better: store money as **integer cents** and format on display. `Rational` gives exact fractions; `Integer` is arbitrary precision (no overflow). Divide carefully:
441
+
442
+ ```ruby
443
+ 7 / 2 # => 3 (integer division, truncates)
444
+ 7.0 / 2 # => 3.5
445
+ 7.fdiv(2) # => 3.5 (explicit float division)
446
+ Rational(7, 2) # => (7/2) exact
447
+ (0.30 * 100).round # float rounding lands you in trouble; round BigDecimal/cents instead
448
+ ```
449
+
450
+ For rounding modes use `BigDecimal#round(2, :half_up)` etc.; default Ruby `Float#round` is round-half-to-even-ish — be explicit for financial math.
451
+
452
+ ## Quick checklist
453
+
454
+ - Add `# frozen_string_literal: true` to line 1 of every file; build with `<<`/`join`, allocate mutable with `+""`.
455
+ - Only `nil`/`false` are falsey; use `fetch` (not `||`) when `false`/`nil` is a valid value.
456
+ - Symbols = identifiers/keys; strings = data. Don't `to_sym` untrusted input.
457
+ - Keyword args for 3+ params or any boolean flag; splat hashes with `**`; forward with `...`.
458
+ - Lambdas for stored callables (strict arity, local `return`); `yield` over `&block` unless you must capture.
459
+ - `filter_map`, `each_with_object`, `tally`, `group_by`, `sort_by`/`min_by`/`max_by` over manual loops; `.lazy` for huge/infinite/streamed data.
460
+ - `include Comparable` (+`<=>`) and `include Enumerable` (+`each`) to enrich your own classes.
461
+ - `case/in` for structure; bind with `{ key: }`, `=>`, guards; `else` to avoid NoMatchingPatternError.
462
+ - `Data.define` for immutable value objects; `Struct` only when mutable; never `OpenStruct`.
463
+ - `fetch`/`dig`/`transform_values`; `Hash.new { |h,k| h[k] = [] }` (block, not shared mutable default).
464
+ - Endless `def x = ...` for one-liners; `it`/`_1` for short single-arg blocks.
465
+ - Money: `BigDecimal("...")` from strings or integer cents — never `Float`; `fdiv`/`Rational` to avoid integer-division surprises.