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,459 @@
1
+ # Authoring & publishing gems
2
+
3
+ Building, structuring, testing and releasing a Ruby gem (Ruby 3.2–3.4, modern RubyGems/Bundler). For app dependency management with Bundler see `references/tooling.md`; for class/module design see `references/oo-design.md`.
4
+
5
+ ## Scaffold a gem
6
+
7
+ Use Bundler's generator — never hand-roll the layout.
8
+
9
+ ```bash
10
+ bundle gem rubino --test=rspec --ci=github --linter=rubocop
11
+ # flags: --exe (CLI binstub), --coc (code of conduct), --mit (license)
12
+ # --changelog (Keep a Changelog stub)
13
+ ```
14
+
15
+ This produces the canonical skeleton:
16
+
17
+ ```
18
+ rubino/
19
+ ├── rubino.gemspec
20
+ ├── Gemfile # only: gemspec + dev-only tools
21
+ ├── Rakefile
22
+ ├── README.md
23
+ ├── CHANGELOG.md
24
+ ├── LICENSE.txt
25
+ ├── .rubocop.yml
26
+ ├── .github/workflows/main.yml
27
+ ├── bin/ # dev helpers: console, setup (NOT shipped)
28
+ ├── exe/ # user-facing executables (shipped, on PATH)
29
+ ├── sig/rubino.rbs # RBS signatures (see errors-and-types.md)
30
+ ├── lib/
31
+ │ ├── rubino.rb # entrypoint: requires version + sets up loader
32
+ │ └── rubino/
33
+ │ └── version.rb # Rubino::VERSION = "0.1.0"
34
+ └── spec/
35
+ ```
36
+
37
+ **Gemfile vs gemspec.** Runtime + development *gem* dependencies belong in the `.gemspec`. The `Gemfile` is one line — `gemspec` — plus dev-only tools you don't want as formal dev dependencies. Don't duplicate dependency lists between them.
38
+
39
+ ```ruby
40
+ # Gemfile
41
+ source "https://rubygems.org"
42
+ gemspec
43
+ gem "rake", "~> 13.0" # tooling-only, fine to keep out of gemspec
44
+ ```
45
+
46
+ ## The entrypoint and version file
47
+
48
+ `version.rb` holds *only* the version constant so tooling (and `rake release`) can read it without loading the whole library.
49
+
50
+ ```ruby
51
+ # lib/rubino/version.rb
52
+ module Rubino
53
+ VERSION = "0.1.0"
54
+ end
55
+ ```
56
+
57
+ ```ruby
58
+ # lib/rubino.rb
59
+ # frozen_string_literal: true
60
+
61
+ require_relative "rubino/version"
62
+ require "zeitwerk"
63
+
64
+ module Rubino
65
+ class Error < StandardError; end # library base error (see errors-and-types.md)
66
+
67
+ Loader = Zeitwerk::Loader.for_gem
68
+ Loader.setup
69
+ end
70
+ ```
71
+
72
+ `Zeitwerk::Loader.for_gem` configures the loader to manage `lib/`, automatically ignoring `lib/rubino.rb` itself and `lib/rubino/version.rb` (already required). Do not `require` your own source files after this — Zeitwerk autoloads them on first constant reference.
73
+
74
+ ## The gemspec
75
+
76
+ ```ruby
77
+ # rubino.gemspec
78
+ # frozen_string_literal: true
79
+
80
+ require_relative "lib/rubino/version"
81
+
82
+ Gem::Specification.new do |spec|
83
+ spec.name = "rubino"
84
+ spec.version = Rubino::VERSION
85
+ spec.authors = ["Jane Dev"]
86
+ spec.email = ["jane@example.com"]
87
+
88
+ spec.summary = "Short one-line description (< ~100 chars, no trailing period)."
89
+ spec.description = "A longer paragraph describing what the gem does and why."
90
+ spec.homepage = "https://github.com/acme/rubino"
91
+ spec.license = "MIT"
92
+ spec.required_ruby_version = ">= 3.2.0"
93
+
94
+ # Metadata powers rubygems.org links + enables MFA-protected pushes.
95
+ spec.metadata["homepage_uri"] = spec.homepage
96
+ spec.metadata["source_code_uri"] = "https://github.com/acme/rubino"
97
+ spec.metadata["changelog_uri"] = "https://github.com/acme/rubino/blob/main/CHANGELOG.md"
98
+ spec.metadata["bug_tracker_uri"] = "https://github.com/acme/rubino/issues"
99
+ spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/rubino"
100
+ spec.metadata["rubygems_mfa_required"] = "true"
101
+
102
+ # File list driven by git — never glob the whole dir (avoids shipping junk).
103
+ gemspec = File.basename(__FILE__)
104
+ spec.files = IO.popen(
105
+ %w[git ls-files -z], chdir: __dir__, err: IO::NULL
106
+ ) do |ls|
107
+ ls.readlines("\x0", chomp: true).reject do |f|
108
+ (f == gemspec) ||
109
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
110
+ end
111
+ end
112
+
113
+ spec.bindir = "exe"
114
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
115
+ spec.require_paths = ["lib"]
116
+
117
+ # Runtime deps: required for the gem to function.
118
+ spec.add_dependency "zeitwerk", "~> 2.6"
119
+ spec.add_dependency "thor", "~> 1.3"
120
+
121
+ # Dev deps: needed only to develop/test the gem.
122
+ spec.add_development_dependency "rspec", "~> 3.13"
123
+ end
124
+ ```
125
+
126
+ Notes:
127
+
128
+ - **`git ls-files`** is the modern idiom — only tracked files ship. Untracked build artifacts, `.env`, `tmp/` never leak into the `.gem`.
129
+ - **`required_ruby_version`** gates installs on old Rubies with a clear error instead of a mysterious syntax failure.
130
+ - **`rubygems_mfa_required = "true"`** forces MFA for anyone pushing the gem — set it.
131
+ - Modern RubyGems: `add_dependency` *is* a runtime dependency. `add_runtime_dependency` is the old alias; either works, but don't pass a `:development` type to `add_dependency`.
132
+
133
+ ### Dependency version policy
134
+
135
+ ```ruby
136
+ spec.add_dependency "thor", "~> 1.3" # >= 1.3.0, < 2.0 (RIGHT — pessimistic)
137
+ spec.add_dependency "thor", ">= 1.3", "< 3" # explicit range when you support 2 majors
138
+ ```
139
+
140
+ Do / don't:
141
+
142
+ - DO use the pessimistic `~>` operator to allow compatible upgrades while excluding the next breaking major.
143
+ - DON'T pin to an exact version (`"= 1.3.2"`) in a library — it causes unresolvable conflicts in apps that depend on you. Exact pins belong in `Gemfile.lock` (apps), not gemspecs.
144
+ - DON'T leave a dependency unbounded (`>= 0`) — a future major can break your users silently.
145
+ - Keep the *floor* honest: require the lowest version whose API you actually use.
146
+
147
+ ## Autoloading with Zeitwerk
148
+
149
+ Zeitwerk maps file paths to constant names. Follow its conventions and you never write `require` again.
150
+
151
+ ```
152
+ lib/rubino.rb -> (root file, loads the gem)
153
+ lib/rubino/client.rb -> Rubino::Client
154
+ lib/rubino/http_client.rb -> Rubino::HTTPClient (acronym, see below)
155
+ lib/rubino/cli/runner.rb -> Rubino::CLI::Runner
156
+ ```
157
+
158
+ ```ruby
159
+ # RIGHT: file name and constant agree
160
+ # lib/rubino/http_client.rb
161
+ module Rubino
162
+ class HTTPClient; end
163
+ end
164
+ ```
165
+
166
+ Acronyms need an inflection rule, or Zeitwerk expects `Rubino::HttpClient`:
167
+
168
+ ```ruby
169
+ Loader = Zeitwerk::Loader.for_gem
170
+ Loader.inflector.inflect("http_client" => "HTTPClient", "cli" => "CLI")
171
+ Loader.setup
172
+ ```
173
+
174
+ **Lazy (default) vs eager loading.** Lazy autoloading loads each file on first reference — ideal for libraries (fast boot, only pay for what's used). Eager-load when a host process forks (e.g. Puma/Sidekiq) or in CI to surface load errors:
175
+
176
+ ```ruby
177
+ Loader.setup
178
+ Loader.eager_load if ENV["RUBINO_EAGER_LOAD"] # or eager_load_force in tests
179
+ ```
180
+
181
+ In CI, run `bin/rubocop` plus a tiny spec that calls `Rubino::Loader.eager_load` to catch naming mismatches before release.
182
+
183
+ ## CLI gems: `exe/` vs `bin/`
184
+
185
+ - `bin/` holds **development** helpers (`bin/console`, `bin/setup`) that are *not* packaged.
186
+ - `exe/` holds **user-facing** executables that go on the user's PATH (`spec.bindir = "exe"`).
187
+
188
+ The executable file should be thin — parse nothing, delegate to a class.
189
+
190
+ ```ruby
191
+ #!/usr/bin/env ruby
192
+ # frozen_string_literal: true
193
+ # exe/rubino (chmod +x)
194
+
195
+ require "rubino"
196
+ Rubino::CLI.start(ARGV)
197
+ ```
198
+
199
+ Use **Thor** for anything beyond a single flag — it gives subcommands, options, and help for free.
200
+
201
+ ```ruby
202
+ # lib/rubino/cli.rb
203
+ require "thor"
204
+
205
+ module Rubino
206
+ class CLI < Thor
207
+ def self.exit_on_failure? = true # exit 1 on errors, not raise
208
+
209
+ desc "build PATH", "Build the project at PATH"
210
+ option :force, type: :boolean, aliases: "-f", desc: "Overwrite existing output"
211
+ def build(path)
212
+ Builder.new(path, force: options[:force]).call
213
+ end
214
+
215
+ desc "version", "Print the version"
216
+ def version = say(Rubino::VERSION)
217
+ end
218
+ end
219
+ ```
220
+
221
+ DON'T put business logic in the Thor class — it's hard to test and couples your domain to the CLI framework. Keep Thor as a thin adapter over plain objects (`Builder` above).
222
+
223
+ ## Shipping non-code assets
224
+
225
+ Data files (templates, fixtures, certs, YAML) must be (a) tracked by git so `git ls-files` includes them, and (b) located at runtime relative to the file, never relative to CWD.
226
+
227
+ ```ruby
228
+ # RIGHT: anchor on the gem's own directory
229
+ module Rubino
230
+ ROOT = File.expand_path("..", __dir__) # gem root from lib/rubino.rb
231
+ DATA_DIR = File.expand_path("templates", __dir__) # lib/rubino/templates
232
+
233
+ def self.template(name)
234
+ File.read(File.join(DATA_DIR, "#{name}.erb"))
235
+ end
236
+ end
237
+ ```
238
+
239
+ ```ruby
240
+ # WRONG: relative to the process working directory — breaks once installed
241
+ File.read("templates/default.erb") # NoMethodError-adjacent: file not found
242
+ File.read(Dir.pwd + "/lib/rubino/...") # depends on where the user ran the command
243
+ ```
244
+
245
+ Use `__dir__` (the directory of the current file) over `File.dirname(__FILE__)` — same result, less noise. Place assets *under* `lib/` (e.g. `lib/rubino/templates/`) so Zeitwerk's path mapping isn't disturbed by them — Zeitwerk ignores non-`.rb` files automatically, but keeping them in a clearly-data subdir is cleanest. For larger data trees, `Loader.ignore("#{__dir__}/rubino/templates")` is explicit.
246
+
247
+ ## Namespacing & avoiding constant pollution
248
+
249
+ Everything lives under your top-level module. One namespace, one top-level constant.
250
+
251
+ ```ruby
252
+ # RIGHT
253
+ module Rubino
254
+ class Client; end
255
+ Config = Data.define(:timeout, :retries)
256
+ end
257
+ ```
258
+
259
+ ```ruby
260
+ # WRONG: leaks Client and Config into Object — collides with other gems
261
+ class Client; end
262
+ Config = Struct.new(:timeout)
263
+ ```
264
+
265
+ Don't reopen/monkey-patch core classes from a gem; if you must extend, use **refinements** (lexically scoped) — see `references/metaprogramming.md`. Don't define top-level methods or constants.
266
+
267
+ ## Semantic versioning & CHANGELOG
268
+
269
+ Follow [SemVer](https://semver.org): `MAJOR.MINOR.PATCH`.
270
+
271
+ - **PATCH** (`0.1.0 → 0.1.1`): backward-compatible bug fixes.
272
+ - **MINOR** (`0.1.0 → 0.2.0`): backward-compatible new features.
273
+ - **MAJOR** (`0.x → 1.0`, `1.x → 2.0`): breaking changes.
274
+ - `0.y.z` means "unstable" — minor bumps may break. Cut `1.0.0` when the API is committed.
275
+
276
+ Maintain a [Keep a Changelog](https://keepachangelog.com) `CHANGELOG.md` — human-curated, newest first, grouped by Added/Changed/Deprecated/Removed/Fixed/Security.
277
+
278
+ ```markdown
279
+ # Changelog
280
+
281
+ ## [Unreleased]
282
+
283
+ ## [0.2.0] - 2026-06-09
284
+ ### Added
285
+ - `Rubino::Client#stream` for incremental responses.
286
+ ### Deprecated
287
+ - `Client#fetch_all`; use `#stream`. Removed in 1.0.
288
+
289
+ ## [0.1.0] - 2026-05-01
290
+ ### Added
291
+ - Initial release.
292
+ ```
293
+
294
+ DON'T auto-generate the changelog from raw commit subjects — curate it for humans who need to decide whether to upgrade.
295
+
296
+ ## README
297
+
298
+ A gem README should let a reader install and succeed in 60 seconds, in this order:
299
+
300
+ 1. One-sentence what-and-why.
301
+ 2. Installation: `bundle add rubino` (modern) or the `gem "rubino"` Gemfile line + `gem install rubino`.
302
+ 3. Usage — a copy-pasteable minimal example that actually runs.
303
+ 4. Configuration options.
304
+ 5. Compatibility (supported Ruby/Rails versions).
305
+ 6. Development & contributing, License.
306
+
307
+ Show real code, not API tables. Keep the top example small.
308
+
309
+ ## Testing a gem
310
+
311
+ Use RSpec (or Minitest — see `references/testing.md`). Test the *public* API against the namespace, not file internals (Zeitwerk autoloads).
312
+
313
+ ```ruby
314
+ # spec/rubino_spec.rb
315
+ RSpec.describe Rubino do
316
+ it "has a version" do
317
+ expect(Rubino::VERSION).to match(/\A\d+\.\d+\.\d+/)
318
+ end
319
+
320
+ it "eager loads without naming errors" do
321
+ expect { Rubino::Loader.eager_load }.not_to raise_error
322
+ end
323
+ end
324
+ ```
325
+
326
+ ### Multi-version testing with Appraisal
327
+
328
+ When your gem must support several versions of a dependency (e.g. Rails 7.1 and 8.0), use the **appraisal** gem to run the suite against each.
329
+
330
+ ```ruby
331
+ # Appraisals
332
+ appraise "rails-7.1" do
333
+ gem "rails", "~> 7.1.0"
334
+ end
335
+ appraise "rails-8.0" do
336
+ gem "rails", "~> 8.0.0"
337
+ end
338
+ ```
339
+
340
+ ```bash
341
+ bundle exec appraisal install # generates gemfiles/*.gemfile + locks
342
+ bundle exec appraisal rspec # runs the suite under every gemfile
343
+ bundle exec appraisal rails-8.0 rspec # just one
344
+ ```
345
+
346
+ ### CI matrix across Ruby versions
347
+
348
+ ```yaml
349
+ # .github/workflows/main.yml
350
+ name: CI
351
+ on: [push, pull_request]
352
+ jobs:
353
+ test:
354
+ runs-on: ubuntu-latest
355
+ strategy:
356
+ fail-fast: false
357
+ matrix:
358
+ ruby: ["3.2", "3.3", "3.4"]
359
+ gemfile: ["gemfiles/rails_7.1.gemfile", "gemfiles/rails_8.0.gemfile"]
360
+ env:
361
+ BUNDLE_GEMFILE: ${{ matrix.gemfile }}
362
+ steps:
363
+ - uses: actions/checkout@v4
364
+ - uses: ruby/setup-ruby@v1
365
+ with:
366
+ ruby-version: ${{ matrix.ruby }}
367
+ bundler-cache: true # bundle install + cache gems
368
+ - run: bundle exec rspec
369
+ - run: bundle exec rubocop
370
+ ```
371
+
372
+ Match the Ruby floor in the matrix to `required_ruby_version`.
373
+
374
+ ## Building & releasing
375
+
376
+ Bundler's `gem` tasks (loaded by `Bundler::GemHelper.install_tasks` in the Rakefile) drive the release:
377
+
378
+ ```bash
379
+ rake build # builds pkg/rubino-0.2.0.gem from the gemspec
380
+ rake install # builds + installs locally for smoke-testing
381
+ rake release # tags vX.Y.Z, pushes the tag, and gem push to rubygems.org
382
+ ```
383
+
384
+ `rake release` derives the version from `Rubino::VERSION`, so the release flow is: bump `version.rb` → update `CHANGELOG.md` → commit → `rake release`. It refuses to release with uncommitted changes.
385
+
386
+ ### Credentials, API key & MFA
387
+
388
+ - `gem push` reads `~/.gem/credentials` (`chmod 0600`). Get a key with `gem signin` or from rubygems.org → Settings → API keys, scoped to *push only*.
389
+ - Enable account-level **MFA** and set `rubygems_mfa_required = "true"` in metadata (above) so pushes require MFA even if a key leaks.
390
+
391
+ ### Trusted publishing / OIDC from CI (preferred)
392
+
393
+ Don't store a long-lived API key in CI secrets. Configure **trusted publishing** on rubygems.org (per-gem, bound to your GitHub repo + workflow), then publish keylessly via OIDC:
394
+
395
+ ```yaml
396
+ release:
397
+ runs-on: ubuntu-latest
398
+ if: startsWith(github.ref, 'refs/tags/v')
399
+ permissions:
400
+ id-token: write # required for OIDC
401
+ contents: write
402
+ steps:
403
+ - uses: actions/checkout@v4
404
+ - uses: ruby/setup-ruby@v1
405
+ with: { ruby-version: "3.4", bundler-cache: true }
406
+ - uses: rubygems/release-gem@v1 # exchanges OIDC token, runs gem push
407
+ ```
408
+
409
+ This issues a short-lived credential per run — no secret to leak or rotate.
410
+
411
+ ### Yanking
412
+
413
+ A pushed version is immutable — you cannot overwrite it. To pull a broken/insecure release:
414
+
415
+ ```bash
416
+ gem yank rubino -v 0.2.0 # removes it as an install candidate
417
+ ```
418
+
419
+ Yanking does NOT free the version number — you cannot re-push `0.2.0`; ship `0.2.1`. Yank only for serious breakage (security, unusable). Prefer a fast follow-up release for ordinary bugs so existing pins keep resolving.
420
+
421
+ ## Backward-compat & deprecation policy
422
+
423
+ Within a major version, don't break the public API. To remove/rename something, deprecate first, remove in the next major.
424
+
425
+ ```ruby
426
+ # RIGHT: warn, delegate, document the removal version
427
+ def fetch_all(*args, **kwargs)
428
+ warn "[DEPRECATION] Rubino::Client#fetch_all is deprecated and will be " \
429
+ "removed in 1.0. Use #stream instead.", uplevel: 1
430
+ stream(*args, **kwargs).to_a
431
+ end
432
+ ```
433
+
434
+ ```ruby
435
+ # WRONG: silently change behavior or delete the method in a minor release
436
+ ```
437
+
438
+ - Use `Kernel#warn ... uplevel: 1` so the warning points at the *caller's* line.
439
+ - Record every deprecation under `### Deprecated` in the CHANGELOG with the planned removal version.
440
+ - Treat anything documented in the README/public methods as API. Mark genuinely-internal classes (`@api private` in docs, or a `Rubino::Internal` namespace) so users know what's safe to break.
441
+ - For richer deprecation (Rails-style), `ActiveSupport::Deprecation` provides per-gem deprecator objects. See `references/errors-and-types.md` for warnings/deprecation mechanics.
442
+
443
+ ## Quick checklist
444
+
445
+ - Scaffold with `bundle gem <name>` (`--test`, `--ci`, `--linter`); don't hand-roll.
446
+ - `version.rb` contains only `VERSION`; the gemspec `require_relative`s it.
447
+ - `spec.files` from `git ls-files`; never glob the whole directory.
448
+ - Set `required_ruby_version`, metadata URIs, and `rubygems_mfa_required = "true"`.
449
+ - Use `~>` pessimistic constraints in the gemspec; never exact-pin a library dep.
450
+ - Runtime deps via `add_dependency`; dev/test deps via `add_development_dependency`.
451
+ - Let Zeitwerk autoload; match file names to constants; register acronym inflections.
452
+ - User executables in `exe/` + `spec.bindir`; dev helpers in `bin/`; Thor as a thin CLI adapter.
453
+ - One top-level module; no top-level constants/methods; no core monkey-patching.
454
+ - Locate bundled data with `File.expand_path(..., __dir__)`, never CWD-relative.
455
+ - SemVer strictly; curate a Keep a Changelog `CHANGELOG.md`.
456
+ - CI matrix over supported Rubies; Appraisal for multi-version deps; eager-load in CI.
457
+ - Release via `rake release`; prefer OIDC trusted publishing over stored API keys.
458
+ - Versions are immutable — `gem yank` for emergencies; bump for fixes.
459
+ - Deprecate (warn + `uplevel: 1`) before removing; remove only on a major bump.