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,466 @@
1
+ # Tooling, environment & debugging
2
+
3
+ The Ruby toolchain for Ruby 3.2–3.4 and Rails 7.1–8.x. Practical commands, modern idioms, and explicit do/don't. For test frameworks see `references/testing.md`; for building/publishing your own gem see `references/gem-authoring.md`.
4
+
5
+ ## Bundler
6
+
7
+ `Bundler` resolves and locks dependencies. The `Gemfile` declares them; `Gemfile.lock` pins the exact resolved versions.
8
+
9
+ ### Gemfile
10
+
11
+ ```ruby
12
+ source "https://rubygems.org"
13
+
14
+ ruby file: ".ruby-version" # single source of truth; or: ruby "3.4.2"
15
+
16
+ gem "rails", "~> 8.0.1" # pessimistic: >= 8.0.1, < 8.1
17
+ gem "pg", "~> 1.5" # >= 1.5, < 2.0
18
+ gem "puma", ">= 6.0"
19
+
20
+ group :development, :test do
21
+ gem "debug", require: false # require: false → load only when you need it
22
+ gem "rspec-rails"
23
+ gem "factory_bot_rails"
24
+ end
25
+
26
+ group :development do
27
+ gem "rubocop", require: false
28
+ gem "ruby-lsp", require: false
29
+ end
30
+
31
+ group :production do
32
+ gem "rack-mini-profiler", require: false
33
+ end
34
+
35
+ gem "nokogiri", platforms: %i[ruby]
36
+ gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby]
37
+ ```
38
+
39
+ ### Version constraints
40
+
41
+ | Constraint | Means | Use when |
42
+ |---|---|---|
43
+ | `~> 1.4.2` | `>= 1.4.2, < 1.5.0` | lock to a patch line |
44
+ | `~> 1.4` | `>= 1.4.0, < 2.0.0` | allow minor upgrades |
45
+ | `>= 1.4` | any future version | libraries, rarely apps |
46
+ | `1.4.2` | exact | pinning a known-good/broken version |
47
+
48
+ Prefer `~>` (pessimistic) for apps. Avoid bare `gem "foo"` with no constraint in a long-lived app — it lets a major bump slip in on a fresh `bundle install` against a deleted lock.
49
+
50
+ ### Gemfile.lock commit policy
51
+
52
+ - **Applications**: ALWAYS commit `Gemfile.lock`. It guarantees every machine/CI/prod runs identical versions.
53
+ - **Gems/libraries**: do NOT commit `Gemfile.lock` (it's typically git-ignored). The consumer's app resolves versions. See `references/gem-authoring.md`.
54
+
55
+ ### Everyday commands
56
+
57
+ ```bash
58
+ bundle install # install per the lock; resolve only new/changed gems
59
+ bundle install --jobs 4 # parallel
60
+ bundle update # re-resolve EVERYTHING to newest allowed — be careful
61
+ bundle update rails # update ONLY rails (and its deps) — prefer this
62
+ bundle update --conservative rails # update rails, hold its shared deps if possible
63
+ bundle outdated # list gems with newer versions available
64
+ bundle outdated --filter-major # only majors
65
+ bundle lock --add-platform x86_64-linux # add a platform for CI/Docker
66
+ bundle lock --update # refresh lock without installing
67
+ bundle clean --force # remove unused installed gem versions
68
+ ```
69
+
70
+ WRONG: running `bundle update` (no args) to fix one gem — it churns the whole lock and ships unreviewed upgrades.
71
+ RIGHT: `bundle update <gem>` for the specific dependency, then review the lock diff.
72
+
73
+ ### bundle exec, binstubs
74
+
75
+ `bundle exec <cmd>` runs a command with the gem versions from the lock, not whatever is globally installed.
76
+
77
+ ```bash
78
+ bundle exec rspec
79
+ bundle exec rake db:migrate
80
+ ```
81
+
82
+ Binstubs are wrappers in `bin/` that pin the bundle so you can drop `bundle exec`:
83
+
84
+ ```bash
85
+ bundle binstubs rspec-core # creates bin/rspec
86
+ bundle binstubs rubocop --force
87
+ ./bin/rspec # equivalent to `bundle exec rspec`
88
+ ```
89
+
90
+ Rails apps ship `bin/rails`, `bin/rake`, `bin/setup`. Commit `bin/` so the team shares the same entrypoints. A `Gemfile`-aware shim is also provided by `mise`/`asdf` reshims and by `rbenv`'s rehash.
91
+
92
+ ## Ruby version managers
93
+
94
+ Pick ONE per machine — mixing rbenv and rvm corrupts `PATH`. The `.ruby-version` file in the project root names the interpreter; most managers auto-switch on `cd`.
95
+
96
+ ```
97
+ # .ruby-version
98
+ 3.4.2
99
+ ```
100
+
101
+ | Manager | Mechanism | Notes |
102
+ |---|---|---|
103
+ | **rbenv** | shims + `PATH`, rehash | lightweight, no shell hijack; `ruby-build` plugin installs versions |
104
+ | **rvm** | shell function overrides `cd` | heavyweight, manages gemsets; older, more invasive |
105
+ | **chruby** | tiny shell function + `ruby-install` | minimal, no shims, explicit |
106
+ | **asdf** | shims, multi-language (`.tool-versions`) | one tool for ruby+node+python |
107
+ | **mise** | fast Rust asdf-compatible (`.mise.toml` or `.tool-versions`) | modern default; auto-installs, env management |
108
+ | **rv** | new Rust Ruby manager (early/experimental) | watch but don't depend on it in prod yet |
109
+
110
+ ```bash
111
+ # rbenv
112
+ rbenv install 3.4.2
113
+ rbenv local 3.4.2 # writes .ruby-version
114
+ rbenv rehash # after installing a gem with a binstub
115
+
116
+ # mise
117
+ mise use ruby@3.4.2 # writes to .mise.toml / .tool-versions
118
+ mise install
119
+ mise exec -- ruby -v
120
+
121
+ # chruby + ruby-install
122
+ ruby-install ruby 3.4.2
123
+ chruby 3.4.2
124
+
125
+ # asdf
126
+ asdf install ruby 3.4.2
127
+ asdf local ruby 3.4.2
128
+ ```
129
+
130
+ DO commit `.ruby-version` (and `.tool-versions`/`.mise.toml` if the team uses that manager). It keeps dev and CI on the same interpreter. Don't hardcode the version string in the `Gemfile` AND `.ruby-version` separately — point the Gemfile at the file: `ruby file: ".ruby-version"`.
131
+
132
+ ## Debugging
133
+
134
+ ### The `debug` gem (stdlib since Ruby 3.1)
135
+
136
+ `debug` is the modern, official debugger — it replaces `byebug`. Drop a breakpoint with `binding.break` (aliases: `binding.b`, `debugger`).
137
+
138
+ ```ruby
139
+ require "debug" # not needed in Rails dev; add `gem "debug"` to :development/:test
140
+
141
+ def process(order)
142
+ binding.break # execution stops here; opens a console
143
+ total = order.line_items.sum(&:price)
144
+ total
145
+ end
146
+ ```
147
+
148
+ At the prompt:
149
+
150
+ ```
151
+ (rdbg) n # next line (step over)
152
+ (rdbg) s # step into
153
+ (rdbg) c # continue
154
+ (rdbg) fin # finish current frame (step out)
155
+ (rdbg) bt # backtrace
156
+ (rdbg) info # local variables
157
+ (rdbg) p total # evaluate Ruby
158
+ (rdbg) break Order#total # set a breakpoint by method
159
+ (rdbg) catch StandardError # break when an exception is raised
160
+ (rdbg) outline # methods available on the current object
161
+ ```
162
+
163
+ Conditional and one-shot breakpoints:
164
+
165
+ ```ruby
166
+ binding.break(do: "p user.id") # run a command then continue
167
+ binding.break if order.total > 1_000 # plain Ruby guard
168
+ ```
169
+
170
+ Remote/attach debugging (great for servers, Docker):
171
+
172
+ ```bash
173
+ rdbg --open --port 12345 -- ruby app.rb # or: bundle exec rdbg -O ...
174
+ rdbg --attach 12345 # attach from another terminal
175
+ ```
176
+
177
+ ### pry / pry-byebug
178
+
179
+ `pry` is a richer REPL; `pry-byebug` adds stepping. Still common in older codebases.
180
+
181
+ ```ruby
182
+ gem "pry-byebug", group: %i[development test]
183
+ binding.pry # drops into Pry; `next`/`step`/`continue`/`finish`, `ls`, `cd obj`, `show-source`
184
+ ```
185
+
186
+ Prefer `debug` for new code (no extra dep on modern Ruby, official, faster). Reach for Pry when you want its introspection (`ls`, `show-source`, `cd`).
187
+
188
+ ### irb (modern)
189
+
190
+ Ruby 3.x ships a vastly upgraded `irb`: multiline editing, autocomplete, syntax highlighting, and a built-in debugger bridge.
191
+
192
+ ```bash
193
+ irb # autocomplete + colorized
194
+ ```
195
+
196
+ ```
197
+ irb> ls Order # list methods/constants (Pry-style)
198
+ irb> show_source Order#total
199
+ irb> edit Order#total # open in $EDITOR
200
+ irb> debug # hand off to the debug gem mid-session
201
+ ```
202
+
203
+ `bin/rails console` uses irb under the hood; `--sandbox` rolls back DB writes on exit.
204
+
205
+ ### Print debugging vs a real debugger
206
+
207
+ ```ruby
208
+ puts order # → to_s, often useless for structured data
209
+ p order # → inspect + returns the value (chainable)
210
+ pp order # pretty-printed, good for nested hashes/arrays
211
+ warn "got #{x}" # → STDERR, won't pollute stdout
212
+ ```
213
+
214
+ `p` returns its argument, so you can wrap an expression without changing control flow: `total = p compute_total`.
215
+
216
+ DO use `p`/`pp` for a quick look. DON'T leave them in committed code (RuboCop's `Lint/Debugger` flags `binding.break`/`pry`; add `Rails/Output` or a custom cop for stray `puts`). For anything non-trivial — wrong values deep in a call stack, conditional state — use `binding.break` instead of sprinkling prints.
217
+
218
+ ### caller / backtrace
219
+
220
+ ```ruby
221
+ puts caller # array of "file:line:in `method'" for the current stack
222
+ puts caller(1, 5) # skip 1 frame, take 5
223
+ rescue => e
224
+ e.backtrace.first(5) # where it was raised
225
+ e.full_message # message + class + backtrace, colorized
226
+ ```
227
+
228
+ `Thread.current.backtrace` and `caller_locations` (returns `Thread::Backtrace::Location` objects with `#path`, `#lineno`, `#label`) are useful for logging the call site without parsing strings.
229
+
230
+ ### Logger
231
+
232
+ ```ruby
233
+ require "logger"
234
+ logger = Logger.new($stdout)
235
+ logger.level = Logger::INFO
236
+ logger.formatter = proc { |sev, time, _prog, msg| "#{time.iso8601} #{sev} #{msg}\n" }
237
+
238
+ logger.debug("query: #{sql}") # below level → suppressed
239
+ logger.info("processed order %d", order.id)
240
+ logger.warn { "expensive #{compute}" } # block form: only evaluated if level allows
241
+ ```
242
+
243
+ Use the block form for expensive messages so the cost is skipped when the level filters them out. In Rails use `Rails.logger` and `Rails.logger.tagged("Orders") { ... }`; configure structured/JSON logging via `config.log_formatter` or the `lograge`/semantic_logger gems. See `references/rails.md`.
244
+
245
+ ## Linting & formatting
246
+
247
+ ### RuboCop
248
+
249
+ Static analyzer + autocorrector. Configure with `.rubocop.yml`:
250
+
251
+ ```yaml
252
+ # .rubocop.yml
253
+ require:
254
+ - rubocop-performance
255
+ plugins: # RuboCop 1.72+ prefers `plugins:` over `require:` for extensions
256
+ - rubocop-rails
257
+ - rubocop-rspec
258
+
259
+ AllCops:
260
+ TargetRubyVersion: 3.4
261
+ NewCops: enable
262
+ Exclude:
263
+ - "db/schema.rb"
264
+ - "vendor/**/*"
265
+ - "bin/**/*"
266
+
267
+ Style/Documentation:
268
+ Enabled: false
269
+
270
+ Metrics/MethodLength:
271
+ Max: 15
272
+
273
+ Layout/LineLength:
274
+ Max: 120
275
+ ```
276
+
277
+ Commands:
278
+
279
+ ```bash
280
+ bundle exec rubocop # lint
281
+ bundle exec rubocop -a # autocorrect SAFE cops only
282
+ bundle exec rubocop -A # autocorrect ALL incl. unsafe — review the diff!
283
+ bundle exec rubocop app/models/order.rb # one file
284
+ bundle exec rubocop --only Style/FrozenStringLiteralComment
285
+ bundle exec rubocop --format github # annotations in CI
286
+ ```
287
+
288
+ `-a` (safe) won't change behavior; `-A` (aggressive/unsafe) may — always re-run tests after `-A`.
289
+
290
+ ### The TODO file
291
+
292
+ When adopting RuboCop on an existing codebase, generate a `.rubocop_todo.yml` that grandfathers current offenses so CI passes on day one:
293
+
294
+ ```bash
295
+ bundle exec rubocop --auto-gen-config
296
+ # creates .rubocop_todo.yml, inherited via inherit_from in .rubocop.yml
297
+ ```
298
+
299
+ Then burn it down over time. Regenerate after a big cleanup. Don't hand-disable cops globally to silence them; let the todo track the debt.
300
+
301
+ ### Disabling inline (sparingly)
302
+
303
+ ```ruby
304
+ # rubocop:disable Metrics/MethodLength
305
+ def big_legacy_method
306
+ ...
307
+ end
308
+ # rubocop:enable Metrics/MethodLength
309
+
310
+ result = compute # rubocop:disable Style/SomeCop -- single line, comment why
311
+ ```
312
+
313
+ Always re-enable, scope it as narrowly as possible, and add a `--` reason. A whole-file `# rubocop:disable all` is a smell.
314
+
315
+ ### Plugins
316
+
317
+ - `rubocop-rails` — Rails-aware cops (`pluck` over `map`, `find_each`, time zones).
318
+ - `rubocop-rspec` — spec style (`describe` naming, `let` usage, example length).
319
+ - `rubocop-performance` — flags slow idioms with faster equivalents.
320
+ - `rubocop-rails-omakase` — Rails' own shared config (used by new Rails 8 apps).
321
+
322
+ ### `standard` — zero-config alternative
323
+
324
+ The `standard` gem wraps RuboCop with a fixed, non-negotiable style (no `.rubocop.yml` bikeshedding).
325
+
326
+ ```bash
327
+ gem "standard", group: %i[development test]
328
+ bundle exec standardrb # lint
329
+ bundle exec standardrb --fix # autocorrect
330
+ ```
331
+
332
+ Use `standard` when you want to end style debates; use raw RuboCop when you need fine-grained control or the Rails/RSpec/Performance cops (you can still layer `standard` + extensions). Don't run both as competing formatters on the same project.
333
+
334
+ ## Editor / LSP
335
+
336
+ - **ruby-lsp** (Shopify) — the modern, actively developed language server. Fast, ships an addon API; integrates RuboCop, debugging, and indexing. Preferred default.
337
+ - **solargraph** — older LSP; relies on YARD docs and `.solargraph.yml`. Still works, slower indexing.
338
+
339
+ ```bash
340
+ gem "ruby-lsp", group: :development, require: false
341
+ # VS Code: install the "Ruby LSP" extension; it manages the gem per-project.
342
+ ```
343
+
344
+ ruby-lsp reads your `.rubocop.yml` for diagnostics/formatting and uses the bundle's gems. Add `gem "ruby-lsp-rails"` for Rails-aware features (routes, model schema). Keep these in the `Gemfile` (development group) so versions match the project, or use the global install the extension offers.
345
+
346
+ ## Rake
347
+
348
+ Task runner. Define tasks in `Rakefile` or `lib/tasks/*.rake`.
349
+
350
+ ```ruby
351
+ # lib/tasks/data.rake
352
+ namespace :data do
353
+ desc "Backfill order totals"
354
+ task backfill: :environment do # :environment loads Rails
355
+ Order.find_each { |o| o.update!(total: o.recompute_total) }
356
+ end
357
+
358
+ desc "Export, depends on backfill"
359
+ task export: %i[backfill] do |_t, args|
360
+ puts "exporting..."
361
+ end
362
+ end
363
+
364
+ # Task with arguments
365
+ task :greet, %i[name] => :environment do |_t, args|
366
+ puts "Hello #{args.name || 'world'}"
367
+ end
368
+ ```
369
+
370
+ ```bash
371
+ bundle exec rake data:backfill # run namespaced task
372
+ bundle exec rake "data:greet[Ada]" # pass args (quote for zsh)
373
+ bundle exec rake -T # list tasks with descriptions (need `desc`)
374
+ bundle exec rake -P # show prerequisites
375
+ ```
376
+
377
+ Prerequisites (`task x: :y`) run once and in dependency order. Only tasks with a `desc` show in `-T`. For anything with real logic, extract to a plain Ruby class and have the task call it — keep tasks thin and testable.
378
+
379
+ ## CI — GitHub Actions matrix
380
+
381
+ ```yaml
382
+ # .github/workflows/ci.yml
383
+ name: CI
384
+ on: [push, pull_request]
385
+
386
+ jobs:
387
+ test:
388
+ runs-on: ubuntu-latest
389
+ strategy:
390
+ fail-fast: false
391
+ matrix:
392
+ ruby: ["3.2", "3.3", "3.4"]
393
+ services:
394
+ postgres:
395
+ image: postgres:16
396
+ env: { POSTGRES_PASSWORD: postgres }
397
+ ports: ["5432:5432"]
398
+ options: >-
399
+ --health-cmd pg_isready --health-interval 10s
400
+ --health-timeout 5s --health-retries 5
401
+ steps:
402
+ - uses: actions/checkout@v4
403
+ - uses: ruby/setup-ruby@v1
404
+ with:
405
+ ruby-version: ${{ matrix.ruby }}
406
+ bundler-cache: true # runs bundle install + caches gems
407
+ - run: bundle exec rubocop
408
+ - run: bundle exec rake db:prepare
409
+ env: { DATABASE_URL: postgres://postgres:postgres@localhost:5432/test }
410
+ - run: bundle exec rspec
411
+ ```
412
+
413
+ `ruby/setup-ruby` with `bundler-cache: true` installs and caches gems keyed on `Gemfile.lock`. Test the Ruby versions you support (and `head` non-blocking with `continue-on-error` if you want early warning). Run lint as a separate fast job/step so style failures don't block the test signal. See `references/testing.md` for what to run.
414
+
415
+ ## Environment management — dotenv
416
+
417
+ Keep secrets and per-environment config out of the repo.
418
+
419
+ ```ruby
420
+ # Gemfile
421
+ gem "dotenv-rails", groups: %i[development test]
422
+ ```
423
+
424
+ ```bash
425
+ # .env (gitignored!) — commit a .env.example with blank/placeholder values
426
+ DATABASE_URL=postgres://localhost/myapp_dev
427
+ STRIPE_KEY=sk_test_xxx
428
+ ```
429
+
430
+ ```ruby
431
+ ENV.fetch("STRIPE_KEY") # raise if missing — prefer in app code
432
+ ENV["STRIPE_KEY"] # nil if missing — only when truly optional
433
+ ```
434
+
435
+ DO add `.env` (and `.env.local`) to `.gitignore`; commit `.env.example` documenting the keys. DON'T read raw `.env` files in production — use real env vars or Rails encrypted credentials (`bin/rails credentials:edit`, `Rails.application.credentials.stripe[:key]`). See `references/security.md` for secrets handling. Prefer `ENV.fetch("X")` over `ENV["X"]` so a missing var fails loudly at boot, not silently at runtime.
436
+
437
+ ## Reading gem source
438
+
439
+ ```bash
440
+ gem which nokogiri # path to the loaded file: .../nokogiri.rb
441
+ bundle show rails # install path of the bundled rails gem
442
+ bundle open activerecord # open the gem's source in $EDITOR (set EDITOR/BUNDLER_EDITOR)
443
+ bundle info rails # version, summary, path, deps
444
+ gem list -d rails # installed versions + details
445
+ gem contents rails # list files the gem installs
446
+ ```
447
+
448
+ `bundle open <gem>` is the fastest way to read exactly the version your app runs (edits there persist until `bundle pristine <gem>` resets it — `bundle pristine` restores any modified gem). Use `gem which` to confirm which file/version actually loaded when there's a conflict.
449
+
450
+ ## Quick checklist
451
+
452
+ - Commit `Gemfile.lock` for apps; never for libraries.
453
+ - Prefer `~>` pessimistic constraints; pin exact only to dodge a known bad version.
454
+ - `bundle update <gem>` — never bare `bundle update` to fix one dependency.
455
+ - Commit `.ruby-version`; point `Gemfile` at it with `ruby file: ".ruby-version"`.
456
+ - One version manager per machine (rbenv/mise/chruby/asdf) — don't mix.
457
+ - Use the `debug` gem + `binding.break` for real debugging; `p`/`pp` only for quick looks.
458
+ - `rubocop -a` is safe; `rubocop -A` is unsafe — re-run tests after `-A`.
459
+ - Adopt RuboCop via `--auto-gen-config` and burn down `.rubocop_todo.yml`; don't disable cops globally.
460
+ - Inline `# rubocop:disable` must be narrow, re-enabled, and justified with `--`.
461
+ - Use `standard` to kill style debates; raw RuboCop + plugins for fine control.
462
+ - ruby-lsp over solargraph for new setups.
463
+ - Keep Rake tasks thin; put logic in plain Ruby classes.
464
+ - CI: matrix the Ruby versions you support; `ruby/setup-ruby` with `bundler-cache: true`.
465
+ - `.env` gitignored + `.env.example` committed; `ENV.fetch` to fail loud; credentials/env vars in prod.
466
+ - `bundle open <gem>` / `gem which` to read the exact source you run.