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,473 @@
1
+ # Testing — RSpec, Minitest, TDD
2
+
3
+ Test **behavior, not implementation**. A test should survive a refactor that keeps behavior identical. If you rename a private method and a test breaks, the test was coupled to implementation. Assert on observable outputs, return values, raised errors, and side effects you actually care about — not on which internal methods got called (unless the interaction *is* the contract, e.g. "enqueues a job").
4
+
5
+ ## TDD red-green-refactor
6
+
7
+ 1. **Red** — write the smallest failing test for the next bit of behavior. Run it; confirm it fails for the *right* reason.
8
+ 2. **Green** — write the least code to pass. Hardcode if needed.
9
+ 3. **Refactor** — clean up with tests green. Don't add behavior here.
10
+
11
+ Keep cycles tiny. The discipline buys you a regression net and forces testable design (DI, small objects — see `references/oo-design.md`).
12
+
13
+ ```ruby
14
+ # Red: describe the behavior you want before it exists
15
+ RSpec.describe Discount do
16
+ it "applies 10% off orders over 100" do
17
+ expect(Discount.new(rate: 0.10).apply(120)).to eq(108)
18
+ end
19
+ end
20
+ ```
21
+
22
+ ## RSpec structure & naming
23
+
24
+ - `describe` a class/method; `context` a condition; `it` one behavior.
25
+ - Method conventions: `describe "#instance_method"`, `describe ".class_method"`.
26
+ - `context` strings start with "when"/"with"/"without". `it` reads as a sentence after "it".
27
+
28
+ ```ruby
29
+ RSpec.describe Order do
30
+ describe "#total" do
31
+ context "when it has line items" do
32
+ it "sums the line item subtotals" do
33
+ # ...
34
+ end
35
+ end
36
+
37
+ context "when empty" do
38
+ it "returns zero" do
39
+ # ...
40
+ end
41
+ end
42
+ end
43
+ end
44
+ ```
45
+
46
+ ### let, let!, subject
47
+
48
+ `let` is **lazy + memoized per example**. `let!` forces eager evaluation in a `before` hook (use when the record must exist even if no example references it). `subject` names the thing under test.
49
+
50
+ ```ruby
51
+ RSpec.describe Invoice do
52
+ subject(:invoice) { Invoice.new(amount: 100, currency: "USD") }
53
+
54
+ let(:customer) { build(:customer) } # lazy: built only when referenced
55
+ let!(:audit) { create(:audit_log) } # eager: row exists before each example
56
+
57
+ it { is_expected.to be_valid } # uses subject
58
+
59
+ it "formats the amount" do
60
+ expect(invoice.formatted).to eq("$100.00")
61
+ end
62
+ end
63
+ ```
64
+
65
+ **Don't** overuse `let` for "mystery guest" data scattered across the file. If a value only matters in one example, declare it inline — locality beats DRY in tests.
66
+
67
+ ```ruby
68
+ # WRONG: forces reader to scroll up to understand the example
69
+ let(:role) { "admin" }
70
+ it("permits deletes") { expect(policy(role)).to be_allowed }
71
+
72
+ # RIGHT: the relevant data is right here
73
+ it "permits deletes for admins" do
74
+ expect(policy("admin")).to be_allowed
75
+ end
76
+ ```
77
+
78
+ ### Hooks
79
+
80
+ `before(:each)` (default) runs per example; `before(:all)`/`before(:context)` runs once — avoid it for DB state (leaks between examples, not rolled back by transactional fixtures). `after` for cleanup that `ensure`-style needs guaranteeing.
81
+
82
+ ## Expectations & built-in matchers
83
+
84
+ ```ruby
85
+ expect(value).to eq(5) # == (value equality)
86
+ expect(value).to be(obj) # equal? (same object identity)
87
+ expect(result).to be_truthy / be_nil / be_present
88
+ expect(list).to include(2, 3) # subset / substring / hash pair
89
+ expect(list).to contain_exactly(3, 1, 2) # same elements, ANY order
90
+ expect(list).to match_array([1, 2, 3]) # alias of contain_exactly
91
+ expect(str).to match(/\Aord_\w+\z/) # regex / nested structure
92
+ expect(user).to have_attributes(name: "Ada", admin: false)
93
+ expect(hash).to match(id: kind_of(Integer), name: a_string_matching(/x/))
94
+
95
+ # Predicate magic: be_<predicate> calls value.<predicate>?
96
+ expect(user).to be_admin # => user.admin?
97
+ expect(order).to be_a_kind_of(Order)
98
+
99
+ # change matcher — assert a side effect, by/from-to
100
+ expect { post.publish! }
101
+ .to change(post, :status).from("draft").to("published")
102
+ expect { create(:user) }.to change(User, :count).by(1)
103
+ expect { noop }.not_to change(User, :count)
104
+
105
+ # raising
106
+ expect { parse("oops") }.to raise_error(ParseError, /unexpected/)
107
+ expect { parse("oops") }.to raise_error(ParseError) { |e| expect(e.line).to eq(3) }
108
+
109
+ # composing matchers
110
+ expect(response).to include("ok").and have_attributes(status: 200)
111
+ expect(numbers).to all(be > 0)
112
+ ```
113
+
114
+ Prefer `eq` over `==` assertions; prefer `contain_exactly` over sorting both sides; prefer the predicate form (`be_valid`) over `eq(true)` on a boolean method.
115
+
116
+ ### aggregate_failures
117
+
118
+ By default an example stops at the first failure. `aggregate_failures` reports *all* failures in the block — great for API response assertions so you fix everything in one run.
119
+
120
+ ```ruby
121
+ it "returns the created user" do
122
+ aggregate_failures do
123
+ expect(response).to have_http_status(:created)
124
+ expect(json[:name]).to eq("Ada")
125
+ expect(json[:id]).to be_present
126
+ end
127
+ end
128
+ ```
129
+
130
+ ## Doubles, stubbing, mocking
131
+
132
+ **Verifying doubles** (`instance_double`, `class_double`, `object_double`) check that the stubbed methods actually exist with the right arity on the real class. **Always prefer them** — a plain `double` will happily stub a method that was renamed/deleted, giving green tests against dead code.
133
+
134
+ ```ruby
135
+ # WRONG: passes even after PaymentGateway#charge is renamed
136
+ gateway = double("gateway", charge: true)
137
+
138
+ # RIGHT: fails loudly if #charge no longer exists / wrong arity
139
+ gateway = instance_double(PaymentGateway, charge: true)
140
+ ```
141
+
142
+ ### allow vs expect
143
+
144
+ `allow` = stub (set up a canned return, no requirement it's called). `expect` = mock (it *must* be called, fails otherwise). Use `expect` only when the call is the behavior you're verifying.
145
+
146
+ ```ruby
147
+ # stub a query/collaborator return
148
+ allow(clock).to receive(:now).and_return(Time.utc(2026, 1, 1))
149
+
150
+ # mock a command you assert happens (and with what args)
151
+ expect(mailer).to receive(:deliver).with(hash_including(to: "a@b.com")).once
152
+
153
+ # return values, sequences, raising
154
+ allow(api).to receive(:fetch).and_return(:first, :second) # successive calls
155
+ allow(api).to receive(:fetch).and_raise(Timeout::Error)
156
+ allow(api).to receive(:fetch) { |id| cache[id] } # compute from args
157
+ ```
158
+
159
+ ### Spies (assert-after-the-fact)
160
+
161
+ Spies let you Arrange-Act-Assert without the awkward expect-before-act ordering. Use `spy` or `have_received`.
162
+
163
+ ```ruby
164
+ notifier = instance_spy(SlackNotifier)
165
+ service = Deployer.new(notifier:)
166
+
167
+ service.run # Act
168
+
169
+ expect(notifier).to have_received(:post).with("deployed").once # Assert
170
+ ```
171
+
172
+ **Don't** mock what you don't own deeply (HTTP libs, ActiveRecord internals). Wrap third parties in a thin adapter and mock *your* adapter — or stub at the network boundary (below).
173
+
174
+ ## Mocking external HTTP — WebMock / VCR
175
+
176
+ Never hit real networks in tests (slow, flaky, non-deterministic). Block all real connections and stub explicitly.
177
+
178
+ ```ruby
179
+ # spec/support/webmock.rb
180
+ require "webmock/rspec"
181
+ WebMock.disable_net_connect!(allow_localhost: true) # localhost for Capybara
182
+
183
+ it "fetches the rate" do
184
+ stub_request(:get, "https://api.fx.test/usd")
185
+ .with(query: { to: "eur" }, headers: { "Authorization" => "Bearer t" })
186
+ .to_return(status: 200, body: { rate: 0.9 }.to_json,
187
+ headers: { "Content-Type" => "application/json" })
188
+
189
+ expect(FxClient.new.rate("eur")).to eq(0.9)
190
+ expect(a_request(:get, /api.fx.test/)).to have_been_made.once
191
+ end
192
+ ```
193
+
194
+ VCR records a real interaction once into a "cassette" and replays it. Good for complex third-party flows; **filter secrets** and avoid letting cassettes hide real contract drift (re-record periodically).
195
+
196
+ ```ruby
197
+ VCR.configure do |c|
198
+ c.cassette_library_dir = "spec/cassettes"
199
+ c.hook_into :webmock
200
+ c.filter_sensitive_data("<TOKEN>") { ENV["API_TOKEN"] }
201
+ c.default_cassette_options = { record: :none } # CI must not record
202
+ end
203
+
204
+ it "lists charges", :vcr do # cassette named from example
205
+ expect(Stripe::Charge.list.size).to eq(3)
206
+ end
207
+ ```
208
+
209
+ ## Time control
210
+
211
+ Frozen/relative time prevents clock-flakiness. Rails ships `ActiveSupport::Testing::TimeHelpers` (`travel_to`, `freeze_time`, `travel_back`); non-Rails projects can use the `timecop` gem.
212
+
213
+ ```ruby
214
+ # Rails (preferred — no extra gem)
215
+ RSpec.configure { |c| c.include ActiveSupport::Testing::TimeHelpers }
216
+
217
+ travel_to(Time.utc(2026, 6, 9, 12)) do
218
+ expect(Token.new.expires_at).to eq(Time.utc(2026, 6, 9, 13))
219
+ end
220
+
221
+ freeze_time do
222
+ record.touch
223
+ expect(record.updated_at).to eq(Time.current)
224
+ end
225
+
226
+ # plain Ruby
227
+ Timecop.freeze(Time.utc(2026)) { ... }
228
+ ```
229
+
230
+ **Don't** assert against `Time.now` without freezing — `eq(Time.now)` races. Inject a clock (`clock: Time` default) for pure-Ruby objects; see DI in `references/oo-design.md`.
231
+
232
+ ## Custom matchers, shared examples & contexts
233
+
234
+ Custom matcher for repeated, intention-revealing assertions:
235
+
236
+ ```ruby
237
+ RSpec::Matchers.define :be_a_valid_slug do
238
+ match { |str| str.match?(/\A[a-z0-9-]+\z/) }
239
+ failure_message { |str| "expected #{str.inspect} to be a valid slug" }
240
+ end
241
+
242
+ expect(post.slug).to be_a_valid_slug
243
+ ```
244
+
245
+ Shared examples = reusable behavior contracts (e.g. every `Searchable`). Shared context = reusable setup.
246
+
247
+ ```ruby
248
+ RSpec.shared_examples "a timestamped record" do
249
+ it { is_expected.to respond_to(:created_at, :updated_at) }
250
+ end
251
+
252
+ RSpec.describe Comment do
253
+ subject { build(:comment) }
254
+ it_behaves_like "a timestamped record"
255
+ end
256
+
257
+ RSpec.shared_context "authenticated", :auth do
258
+ let(:current_user) { create(:user) }
259
+ before { sign_in(current_user) }
260
+ end
261
+
262
+ RSpec.describe "Dashboard", :auth do # pulls in the context via tag
263
+ # current_user + sign_in available
264
+ end
265
+ ```
266
+
267
+ ## Tags & focus
268
+
269
+ ```ruby
270
+ it("slow path", :slow) { ... }
271
+ # run a subset: rspec --tag slow ; exclude: rspec --tag ~slow
272
+ ```
273
+
274
+ `fit`/`fdescribe`/`fcontext` (or `:focus`) restrict the run to focused examples — handy locally. **Never commit focus**; configure `config.filter_run_when_matching :focus` and add a RuboCop/CI guard so a stray `fit` fails the build (CI config lives in `references/tooling.md`).
275
+
276
+ ## FactoryBot — build the lightest thing that works
277
+
278
+ Prefer, in order: `build_stubbed` > `build` > `create`. Only `create` when you truly need a persisted row (queries, DB constraints, associations loaded from DB).
279
+
280
+ ```ruby
281
+ FactoryBot.define do
282
+ factory :user do
283
+ sequence(:email) { |n| "user#{n}@example.com" } # unique values
284
+ name { "Ada" }
285
+
286
+ trait :admin do
287
+ role { "admin" }
288
+ end
289
+
290
+ # association: only created when the strategy needs it
291
+ factory :author do
292
+ association :profile
293
+ end
294
+ end
295
+ end
296
+ ```
297
+
298
+ ```ruby
299
+ build_stubbed(:user) # in-memory, fake id, NO DB hit — fastest; great for unit/policy specs
300
+ build(:user) # in-memory, not saved (associations may still touch DB)
301
+ create(:user, :admin) # persisted, with trait
302
+ create_list(:post, 3, author: user)
303
+ ```
304
+
305
+ ### Avoid factory cascades
306
+
307
+ A factory whose associations create more factories which create more rows = slow, brittle tests. Keep factories **minimal and valid** (only attributes required for validity). Pass collaborators explicitly instead of letting the factory deep-create them.
308
+
309
+ ```ruby
310
+ # WRONG: creating a comment silently inserts a post + user + account...
311
+ create(:comment)
312
+
313
+ # RIGHT: share parents, create only what the test needs
314
+ post = create(:post)
315
+ create(:comment, post:)
316
+ ```
317
+
318
+ Use `traits` for variation, not a forest of named factories. Run `FactoryBot.lint` in CI to catch factories that no longer build valid records.
319
+
320
+ ## Database state — transactional fixtures vs database_cleaner
321
+
322
+ For most specs use Rails' **transactional fixtures** (`config.use_transactional_fixtures = true` in `rspec-rails`): each example runs in a transaction rolled back at the end — fast and isolated. **System/feature specs** that run the app in a separate thread/process (Capybara + real browser) can't see uncommitted transaction data, so use `database_cleaner` with the `:truncation` (or `:deletion`) strategy for those, and `:transaction` elsewhere.
323
+
324
+ ```ruby
325
+ DatabaseCleaner.strategy = :transaction
326
+ RSpec.configure do |c|
327
+ c.before(:each, type: :system) { DatabaseCleaner.strategy = :truncation }
328
+ c.around(:each) { |ex| DatabaseCleaner.cleaning { ex.run } }
329
+ end
330
+ ```
331
+
332
+ ## The test pyramid — which spec type to favor
333
+
334
+ Lots of fast **unit specs** (models, services, POROs, values), fewer integration, very few end-to-end.
335
+
336
+ - **Model specs** — validations, scopes, methods. Fast.
337
+ - **Service / PORO specs** — your business logic; the bulk of value. Inject collaborators, stub the boundaries.
338
+ - **Request specs** (`type: :request`) — full controller stack via real HTTP (`get/post`, assert status + JSON/body). **Favor these over controller specs** (deprecated style) for API/HTTP coverage.
339
+ - **Job specs** — `expect { Thing.perform_later }.to have_enqueued_job`; test `perform` logic directly and idempotency.
340
+ - **Mailer specs** — assert recipients, subject, body, and that mail is enqueued/delivered.
341
+ - **System specs** (`type: :system`, Capybara, headless Chrome via `selenium`/`cuprite`) — true browser, JS, multi-page flows. Slow and most flake-prone; keep to critical happy paths only.
342
+
343
+ ```ruby
344
+ # request spec
345
+ RSpec.describe "POST /api/users", type: :request do
346
+ it "creates a user" do
347
+ expect { post "/api/users", params: { user: { name: "Ada" } } }
348
+ .to change(User, :count).by(1)
349
+ expect(response).to have_http_status(:created)
350
+ end
351
+ end
352
+
353
+ # job spec
354
+ RSpec.describe ChargeJob do
355
+ include ActiveJob::TestHelper
356
+ it "enqueues on the payments queue" do
357
+ expect { ChargeJob.perform_later(1) }
358
+ .to have_enqueued_job.on_queue("payments").with(1)
359
+ end
360
+ end
361
+
362
+ # mailer spec
363
+ it "emails the user" do
364
+ expect { UserMailer.welcome(user).deliver_now }
365
+ .to change { ActionMailer::Base.deliveries.size }.by(1)
366
+ expect(ActionMailer::Base.deliveries.last.to).to eq([user.email])
367
+ end
368
+ ```
369
+
370
+ (Rails app layout/types: `references/rails.md`.)
371
+
372
+ ## Coverage — use SimpleCov wisely
373
+
374
+ Coverage shows *unexecuted* lines, not *untested behavior*. 100% line coverage with no assertions proves nothing. Use it to find blind spots, not as a target to game.
375
+
376
+ ```ruby
377
+ # spec/spec_helper.rb (very top, before app code loads)
378
+ require "simplecov"
379
+ SimpleCov.start "rails" do
380
+ add_filter "/spec/"
381
+ enable_coverage :branch # branch coverage catches untested conditionals
382
+ end
383
+ ```
384
+
385
+ ## Minitest equivalent
386
+
387
+ For projects on Minitest (Rails default). Assertion style:
388
+
389
+ ```ruby
390
+ require "test_helper"
391
+
392
+ class OrderTest < ActiveSupport::TestCase
393
+ test "totals line items" do
394
+ order = orders(:one) # fixtures
395
+ assert_equal 108, order.total
396
+ assert order.valid?
397
+ refute order.empty?
398
+ assert_includes order.tags, "vip"
399
+ assert_nil order.coupon
400
+ assert_raises(ArgumentError) { order.apply(nil) }
401
+ assert_difference("Order.count", 1) { Order.create!(...) }
402
+ assert_changes -> { order.status }, from: "draft", to: "open" do
403
+ order.open!
404
+ end
405
+ end
406
+ end
407
+ ```
408
+
409
+ Spec-style (`Minitest::Spec`) reads like RSpec-lite:
410
+
411
+ ```ruby
412
+ require "minitest/autorun"
413
+ describe Discount do
414
+ it "applies a rate" do
415
+ _(Discount.new(0.1).apply(120)).must_equal 108
416
+ end
417
+ end
418
+ ```
419
+
420
+ Mocking with built-in `Minitest::Mock` and `stub`:
421
+
422
+ ```ruby
423
+ mock = Minitest::Mock.new
424
+ mock.expect(:charge, true, [100]) # method, return, expected args
425
+ service.run(gateway: mock)
426
+ mock.verify # fails if not called as specified
427
+
428
+ gateway.stub(:online?, true) do # temporary stub within block
429
+ assert service.available?
430
+ end
431
+ ```
432
+
433
+ For richer mocking/time helpers add `mocha` and `timecop`; `assert_enqueued_with`, `travel_to`, `assert_emails` ship with Rails' test helpers.
434
+
435
+ ## Fast, deterministic, isolated — and avoiding flakes
436
+
437
+ Common flaky-test causes and fixes:
438
+
439
+ - **Time** — never assert on `Time.now`; freeze/travel time. Beware DST and `Time.zone` vs `Time.now`.
440
+ - **Ordering** — `contain_exactly`/`match_array`, never assume DB row order without `ORDER BY`. Run `rspec --order random` (the default `--seed`) so order-dependence surfaces.
441
+ - **Shared/global state** — leaking class vars, memoized singletons, `ENV`, `Thread.current`, registered observers, or `before(:all)` DB rows. Reset between examples.
442
+ - **Randomness** — seed `srand`, or inject the RNG; stub `SecureRandom`/`S-equence`.
443
+ - **External I/O** — block the network (WebMock), don't read real clocks/files; stub the boundary.
444
+ - **Async/system specs** — use Capybara's auto-waiting matchers (`have_content`), never `sleep`; let it retry until timeout.
445
+ - **Test interdependence** — each example must pass in isolation: `rspec path/to/spec.rb:42`. If it only passes with the whole file, you have leakage.
446
+
447
+ ```ruby
448
+ # WRONG: order-dependent and sleep-based
449
+ sleep 2
450
+ expect(page.text).to include("Saved") # races the render
451
+
452
+ # RIGHT: auto-waiting assertion retries until matched or timeout
453
+ expect(page).to have_content("Saved")
454
+ ```
455
+
456
+ Keep tests **independent** (no example relies on another), **deterministic** (same result every run), and **fast** (push logic into POROs you can unit-test without the DB/HTTP stack).
457
+
458
+ ## Quick checklist
459
+
460
+ - Test behavior and public contracts, not private methods or call sequences.
461
+ - Red → green → refactor in small steps; watch the test fail first.
462
+ - `instance_double`/verifying doubles over plain `double`, always.
463
+ - `allow` for stubs (queries), `expect`/`have_received` for commands you assert.
464
+ - Prefer `build_stubbed` > `build` > `create`; keep factories minimal; avoid cascades.
465
+ - Stub the network (WebMock/VCR); never hit real services; filter secrets.
466
+ - Freeze/travel time; never assert against live `Time.now`.
467
+ - Use `contain_exactly` for unordered collections; run specs in random order.
468
+ - Request specs over controller specs; keep system specs to critical paths only.
469
+ - `aggregate_failures` for multi-assertion API checks.
470
+ - No committed `fit`/`fdescribe`/focus; no `sleep` in system specs — use auto-waiting matchers.
471
+ - Coverage (SimpleCov, branch) finds gaps; it is not a quality target.
472
+ - Each example must pass in isolation; reset global/shared state.
473
+ - Rails app layout → `references/rails.md`; CI & RuboCop config → `references/tooling.md`.