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,339 @@
1
+ # Metaprogramming & DSLs
2
+
3
+ Metaprogramming is Ruby code that defines or alters code at runtime. It is powerful and abused often. **Default to plain Ruby.** Reach for these tools only when they delete real, repeated duplication that no normal abstraction (method, module, value object) can. Every dynamic method must be **documented and tested** — it is invisible to `grep`, LSP, and the next reader.
4
+
5
+ See `references/oo-design.md` for when a value/service object beats a DSL, and `references/language-idioms.md` for blocks/procs/Enumerable basics this file assumes.
6
+
7
+ ## The object model
8
+
9
+ Three facts drive everything below:
10
+
11
+ 1. **Classes are objects** (instances of `Class`). `class Foo; end` is sugar for assigning a `Class` instance to constant `Foo`.
12
+ 2. **Every object has a singleton class** (a.k.a. eigenclass / metaclass) holding methods unique to that one object. "Class methods" are just instance methods on the class's singleton class.
13
+ 3. **Method lookup walks `ancestors`** left to right: singleton class → prepended modules → the class → included modules → superclass (recursively) → `BasicObject`.
14
+
15
+ ```ruby
16
+ class A; end
17
+ module M; end
18
+ class B < A; include M; end
19
+
20
+ B.ancestors # => [B, M, A, Object, Kernel, BasicObject]
21
+ B.singleton_class.ancestors.first(3)
22
+ # => [#<Class:B>, #<Class:A>, #<Class:Object>] (class-method lookup chain)
23
+
24
+ obj = Object.new
25
+ def obj.greet = "hi" # defines on obj's singleton class
26
+ obj.singleton_class.instance_methods(false) # => [:greet]
27
+ ```
28
+
29
+ ### include / prepend / extend + super
30
+
31
+ ```ruby
32
+ module Logged
33
+ def save
34
+ puts "before"
35
+ r = super # calls the next save in ancestors
36
+ puts "after"
37
+ r
38
+ end
39
+ end
40
+
41
+ class Doc
42
+ prepend Logged # Logged sits BEFORE Doc -> its #save wins, super hits Doc#save
43
+ def save = :saved
44
+ end
45
+ Doc.new.save # before / after / => :saved
46
+ ```
47
+
48
+ - `include M` — inserts `M` **after** the class in ancestors (instance methods, overridable by the class).
49
+ - `prepend M` — inserts `M` **before** the class; ideal for wrapping/decorating an existing method via `super` (the modern replacement for `alias_method` chains).
50
+ - `extend M` — adds `M`'s methods to a single object's singleton class. `obj.extend(M)`; at class level `extend M` makes M's methods class methods.
51
+
52
+ ```ruby
53
+ # WRONG (old pattern): alias-method chaining is fragile and order-dependent
54
+ alias_method :save_without_log, :save
55
+ def save; log; save_without_log; end
56
+
57
+ # RIGHT: prepend a module and call super
58
+ prepend Logged
59
+ ```
60
+
61
+ ## define_method — dynamic method generation
62
+
63
+ Use when you'd otherwise copy-paste near-identical methods. The block is a closure (captures surrounding scope), unlike `def`.
64
+
65
+ ```ruby
66
+ class Settings
67
+ %i[host port timeout].each do |name|
68
+ define_method(name) { @config[name] }
69
+ define_method("#{name}=") { |v| @config[name] = v }
70
+ end
71
+ end
72
+ ```
73
+
74
+ Prefer `define_method` over `class_eval("def ...")` with string interpolation: it is faster to define, safer (no string injection), and shows in backtraces. Only use string `class_eval` if you need the absolute fastest *call-time* method and have profiled it (see `references/performance.md`).
75
+
76
+ ```ruby
77
+ # Acceptable string form when call-time speed is proven-critical & names are trusted constants:
78
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
79
+ def #{name}; @#{name}; end
80
+ RUBY
81
+ ```
82
+
83
+ Always pass `__FILE__, __LINE__ + 1` to string evals so backtraces point at the real source.
84
+
85
+ ## method_missing — always paired with respond_to_missing?
86
+
87
+ `method_missing` is the fallback when lookup fails. **Never define it without `respond_to_missing?`** — otherwise `respond_to?`, `method()`, `Symbol#to_proc`, and duck-typing checks lie about the object.
88
+
89
+ ```ruby
90
+ class DynamicConfig
91
+ def initialize(data = {}) = @data = data
92
+
93
+ def method_missing(name, *args)
94
+ key = name.to_s.chomp("=")
95
+ if name.to_s.end_with?("=")
96
+ @data[key] = args.first
97
+ elsif @data.key?(key)
98
+ @data[key]
99
+ else
100
+ super # let Ruby raise a proper NoMethodError
101
+ end
102
+ end
103
+
104
+ def respond_to_missing?(name, include_private = false)
105
+ @data.key?(name.to_s.chomp("=")) || super
106
+ end
107
+ end
108
+ ```
109
+
110
+ Rules: always call `super` for the unhandled case (gives a correct `NoMethodError` with `did_you_mean`); keep the matching logic identical in both methods.
111
+
112
+ **Costs:** every missing call walks the *entire* ancestor chain before reaching `method_missing`, so it is far slower than a real method, and it defeats tooling/autocomplete. Prefer defining real methods up front:
113
+
114
+ ```ruby
115
+ # BETTER than method_missing when the key set is known: define them once
116
+ class Config
117
+ def self.attribute(name)
118
+ define_method(name) { @data[name] }
119
+ end
120
+ end
121
+ ```
122
+
123
+ A common upgrade is to **define the method on first use** inside `method_missing`, so later calls hit a real method (define-on-miss):
124
+
125
+ ```ruby
126
+ def method_missing(name, *args, &blk)
127
+ if dynamic?(name)
128
+ self.class.define_method(name) { @data[name] } # define once
129
+ send(name)
130
+ else
131
+ super
132
+ end
133
+ end
134
+ ```
135
+
136
+ ## send / public_send
137
+
138
+ `send` invokes a method by name, **including private methods**. `public_send` respects visibility — use it unless you specifically need private access.
139
+
140
+ ```ruby
141
+ public_send(action) # respects private/protected — safe default
142
+ send(:internal_helper) # only when you intentionally bypass privacy
143
+ ```
144
+
145
+ **Security:** never pass unsanitized user input to `send`/`public_send` — it lets a caller invoke arbitrary methods (`destroy`, `system`, ...). Allowlist first.
146
+
147
+ ```ruby
148
+ # WRONG — arbitrary method invocation
149
+ record.public_send(params[:field])
150
+
151
+ # RIGHT — allowlist
152
+ ALLOWED = %w[name email created_at].freeze
153
+ record.public_send(field) if ALLOWED.include?(field)
154
+ ```
155
+
156
+ See `references/security.md`.
157
+
158
+ ## Instance variables & singleton methods reflectively
159
+
160
+ ```ruby
161
+ obj.instance_variable_get(:@name) # read; returns nil if unset (no warning)
162
+ obj.instance_variable_set(:@name, "Ada")
163
+ obj.instance_variables # => [:@name]
164
+
165
+ obj.define_singleton_method(:shout) { @name.upcase } # method on this object only
166
+ ```
167
+
168
+ Use sparingly — reaching into another object's ivars breaks encapsulation. It is legitimate inside serializers, test setup, and the object's own metaprogramming.
169
+
170
+ ## Module / Class hooks
171
+
172
+ These callbacks fire when modules/classes are used, enabling DSLs that inject both instance and class behavior.
173
+
174
+ ```ruby
175
+ module Trackable
176
+ def self.included(base) # fires on `include Trackable`
177
+ base.extend(ClassMethods) # add class-level methods
178
+ base.class_eval { @records = [] }
179
+ end
180
+
181
+ module ClassMethods
182
+ def all = @records
183
+ def track(r) = (@records << r)
184
+ end
185
+
186
+ def save = self.class.track(self)
187
+ end
188
+ ```
189
+
190
+ Hooks: `included(base)`, `extended(obj)`, `prepended(base)`, `inherited(subclass)` (subclass registration), and `method_added` / `method_removed`. In Rails, prefer `ActiveSupport::Concern` which formalizes the include+extend+`included do` pattern — see `references/rails.md`.
191
+
192
+ ### Anonymous classes/modules
193
+
194
+ ```ruby
195
+ klass = Class.new(StandardError) # anonymous, assign to a const to name it
196
+ NotFound = Class.new(StandardError) # now named "NotFound"
197
+
198
+ mod = Module.new do
199
+ define_method(:tag) { "x" }
200
+ end
201
+ Object.include(mod) # generate behavior at runtime
202
+ ```
203
+
204
+ `Class.new(Super) { ... }` and `Module.new { ... }` are the runtime-construction primitives behind many DSLs and factories.
205
+
206
+ ## class_eval / instance_eval / instance_exec
207
+
208
+ - `Klass.class_eval { def foo; end }` — runs in **class context**; `def`/`define_method` add **instance** methods. Reopens a class given only a reference.
209
+ - `obj.instance_eval { @ivar }` — runs with `self = obj`; `def` here defines a **singleton** method. Reads/writes the object's ivars.
210
+ - `instance_exec(*args) { |x| ... }` — like `instance_eval` but passes arguments into the block. Essential for DSL blocks that need outside data.
211
+
212
+ ```ruby
213
+ config.instance_exec(env) { |e| @url = e.fetch("URL") }
214
+ ```
215
+
216
+ Note the asymmetry: inside `class_eval`, `def` makes instance methods but `define_method` is needed to capture closures; inside `instance_eval`, `def` makes singleton methods.
217
+
218
+ ### binding
219
+
220
+ `binding` captures the local scope (variables, `self`) as a `Binding` object — used by templating (ERB) and debuggers (`binding.irb`, the `debug` gem's `binding.break`; see `references/tooling.md`).
221
+
222
+ ```ruby
223
+ require "erb"
224
+ name = "Ada"
225
+ ERB.new("Hi <%= name %>").result(binding)
226
+ ```
227
+
228
+ ## Internal DSLs
229
+
230
+ Two clean styles. Prefer **explicit `define_method`/declared macros** over `method_missing` DSLs — they are greppable, autocompletable, and fail loudly on typos.
231
+
232
+ ### Class-macro DSL (declarative, preferred)
233
+
234
+ ```ruby
235
+ class Mapper
236
+ def self.field(name, from:)
237
+ fields[name] = from
238
+ define_method(name) { @data[from] }
239
+ end
240
+ def self.fields = @fields ||= {}
241
+
242
+ def initialize(data) = @data = data
243
+ end
244
+
245
+ class UserMapper < Mapper
246
+ field :email, from: "email_address" # reads as configuration
247
+ end
248
+ ```
249
+
250
+ ### Block/builder DSL with instance_eval
251
+
252
+ Good for nested config. Beware: inside `instance_eval`, the block can't see the caller's methods/ivars (self is swapped) — pass needed data via `instance_exec`, and document that gotcha.
253
+
254
+ ```ruby
255
+ class RouteBuilder
256
+ def self.draw(&blk)
257
+ b = new
258
+ b.instance_eval(&blk)
259
+ b.routes
260
+ end
261
+ def initialize = @routes = []
262
+ def get(path, to:) = @routes << [:get, path, to]
263
+ attr_reader :routes
264
+ end
265
+
266
+ RouteBuilder.draw do
267
+ get "/health", to: "system#health"
268
+ end
269
+ ```
270
+
271
+ ### method_missing DSL (use only when keys are open-ended)
272
+
273
+ Justified when the vocabulary is genuinely unbounded (e.g. a builder for arbitrary HTML tags). Otherwise prefer macros above.
274
+
275
+ ## Introspection
276
+
277
+ ```ruby
278
+ String.instance_methods(false) # methods defined directly on String
279
+ obj.methods - Object.instance_methods # what this object adds
280
+ obj.method(:foo).source_location # ["file.rb", 12] — find dynamic defs
281
+ obj.respond_to?(:foo)
282
+ Foo.const_get(:Bar) # resolve "Foo::Bar" dynamically
283
+ Object.const_get("A::B::C") # const_get follows :: in a string
284
+ Foo.instance_method(:foo).parameters # [[:req, :x], [:key, :y]]
285
+ ```
286
+
287
+ `source_location` is the best tool for *finding* where a dynamic method was defined — make sure your generators produce a usable one.
288
+
289
+ ### ObjectSpace caveats
290
+
291
+ `ObjectSpace.each_object(SomeClass)` enumerates live instances but is **slow, GC-dependent, and disabled/limited on JRuby & TruffleRuby**. Use only for debugging/diagnostics, never in production logic. `ObjectSpace.count_objects` and `memsize_of` are useful in profiling (see `references/performance.md`).
292
+
293
+ ## TracePoint
294
+
295
+ `TracePoint` hooks runtime events (`:call`, `:line`, `:raise`, `:class`). For diagnostics/instrumentation only — it is expensive and global.
296
+
297
+ ```ruby
298
+ tp = TracePoint.new(:call) { |t| puts "#{t.defined_class}##{t.method_id}" }
299
+ tp.enable { run_something } # active only inside the block
300
+ ```
301
+
302
+ Never ship TracePoint in a hot path. It powers debuggers and coverage tools, not application logic.
303
+
304
+ ## Refinements vs monkey-patching
305
+
306
+ **Monkey-patching** (reopening a class globally) is a last resort: it is action-at-a-distance, can break other gems, and is invisible. If you must, do it in a clearly named file, only add (never silently override) behavior, and consider `prepend` so the original is reachable via `super`.
307
+
308
+ **Refinements** scope a patch lexically — active only in files that `using` them. Safer than global patches but have sharp edges (no dynamic dispatch into refined methods from `send` in some versions, ignored by metaprogramming, lexical-only).
309
+
310
+ ```ruby
311
+ module StringExt
312
+ refine String do
313
+ def shout = upcase + "!"
314
+ end
315
+ end
316
+
317
+ # in another file:
318
+ using StringExt # active only for the rest of THIS file (lexical scope)
319
+ "hi".shout # => "HI!"
320
+ ```
321
+
322
+ Guidance:
323
+ - **First choice:** add a method to *your own* class/module, or a helper/service object — no patching at all.
324
+ - **Acceptable:** a refinement for a small, localized extension of a core class within your own code.
325
+ - **Avoid:** global monkey-patches of stdlib/core or third-party gem internals. If unavoidable, isolate and test heavily.
326
+
327
+ ## Quick checklist
328
+
329
+ - Prefer plain Ruby; use metaprogramming only to remove real, repeated duplication.
330
+ - Document and write tests for every dynamically defined method; ensure `source_location` works.
331
+ - `method_missing` ⇒ always define `respond_to_missing?` too, and `super` for the unhandled case.
332
+ - Prefer declarative `define_method`/class-macros over `method_missing` DSLs (greppable, autocompletable).
333
+ - Use `define_method` over string `class_eval`; if you must use string eval, pass `__FILE__, __LINE__ + 1`.
334
+ - Use `prepend` + `super` instead of `alias_method` chains to wrap methods.
335
+ - Use `public_send` by default; `send` only to intentionally bypass privacy.
336
+ - Never pass user input to `send`/`public_send`/`const_get` without an allowlist.
337
+ - Use `prepend`/refinements over global monkey-patching; never override core/gem internals globally.
338
+ - `ObjectSpace.each_object` and `TracePoint` are diagnostics-only — keep them out of production paths.
339
+ - In Rails, reach for `ActiveSupport::Concern` instead of hand-rolled `included`/`extend` plumbing.