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,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module Commands
5
+ # Discovers and manages custom slash commands from configured paths.
6
+ class Loader
7
+ COMMAND_GLOB = "*.md"
8
+
9
+ def initialize(config: nil)
10
+ @config = config || Rubino.configuration
11
+ @commands = {}
12
+ @discovered = false
13
+ end
14
+
15
+ # Discovers all available commands
16
+ def discover!
17
+ @commands.clear
18
+ command_paths.each do |dir|
19
+ expanded = self.class.resolve_path(dir)
20
+ next unless File.directory?(expanded)
21
+
22
+ Dir.glob(File.join(expanded, COMMAND_GLOB)).each do |path|
23
+ cmd = Command.new(path: path)
24
+ @commands[cmd.name] = cmd
25
+ end
26
+ end
27
+ @discovered = true
28
+ @commands
29
+ end
30
+
31
+ # Returns all discovered commands
32
+ def all
33
+ discover! unless @discovered
34
+ @commands.values
35
+ end
36
+
37
+ # Finds a command by name (without the leading /)
38
+ def find(name)
39
+ discover! unless @discovered
40
+ @commands[name.to_s.sub(%r{\A/}, "")]
41
+ end
42
+
43
+ # Returns true if input starts with a slash command
44
+ def slash_command?(input)
45
+ input.strip.start_with?("/")
46
+ end
47
+
48
+ # Parses a slash command input into [command_name, arguments]
49
+ def parse(input)
50
+ stripped = input.strip
51
+ return nil unless stripped.start_with?("/")
52
+
53
+ parts = stripped[1..].split(/\s+/, 2)
54
+ command_name = parts[0]
55
+ arguments = parts[1] || ""
56
+ [command_name, arguments]
57
+ end
58
+
59
+ # Returns command names for autocomplete
60
+ def names
61
+ all.map { |c| "/#{c.name}" }
62
+ end
63
+
64
+ private
65
+
66
+ def command_paths
67
+ @config.dig("commands", "paths") || Config::Defaults.to_hash.dig("commands", "paths")
68
+ end
69
+
70
+ # Default search paths, with the home sentinel resolved to a real dir.
71
+ # Used by the loader and the "/commands" empty-state copy so both report
72
+ # the directories actually searched (RUBINO_HOME-aware).
73
+ def self.default_command_paths
74
+ Array(Config::Defaults.to_hash.dig("commands", "paths")).map { |p| resolve_path(p) }
75
+ end
76
+
77
+ # Resolves a configured commands path to an absolute directory, expanding
78
+ # the <RUBINO_HOME>/commands sentinel against the resolved home
79
+ # (RUBINO_HOME -> else ~/.rubino) instead of a literal ~/.rubino (#38).
80
+ def self.resolve_path(dir)
81
+ if dir.to_s.start_with?(Config::Defaults::HOME_COMMANDS_PATH)
82
+ suffix = dir.to_s.sub(Config::Defaults::HOME_COMMANDS_PATH, "")
83
+ File.join(Config::Loader.default_home_path, "commands#{suffix}")
84
+ else
85
+ File.expand_path(dir)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,455 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubino
4
+ module Config
5
+ # Central configuration object providing typed accessors for all config sections.
6
+ # Wraps the raw hash loaded by Config::Loader with convenient method access.
7
+ class Configuration
8
+ attr_reader :raw
9
+
10
+ def initialize(raw: nil, home_path: nil)
11
+ @home_path = home_path
12
+ @raw = raw || load_from_file
13
+ end
14
+
15
+ # -- Model section --
16
+ def model_default
17
+ dig("model", "default")
18
+ end
19
+
20
+ def model_provider
21
+ dig("model", "provider")
22
+ end
23
+
24
+ def model_context_length
25
+ dig("model", "context_length")
26
+ end
27
+
28
+ def model_temperature
29
+ dig("model", "temperature")
30
+ end
31
+
32
+ # -- Database section --
33
+ # Resolves the sqlite path. The DEFAULT (sentinel) follows the resolved
34
+ # home so RUBINO_HOME relocates the DB alongside config/.env/skills,
35
+ # avoiding the split brain where config went to the isolated home but the
36
+ # DB to the real ~/.rubino (issue #96). An EXPLICIT database.path in
37
+ # config.yml wins and is expanded verbatim.
38
+ def database_path
39
+ path = dig("database", "path")
40
+ if path == Defaults::DEFAULT_DATABASE_PATH
41
+ File.join(resolved_home, "rubino.sqlite3")
42
+ else
43
+ File.expand_path(path)
44
+ end
45
+ end
46
+
47
+ # -- Paths section --
48
+ def paths_home
49
+ dig("paths", "home")
50
+ end
51
+
52
+ # -- UI section --
53
+ def ui_adapter
54
+ dig("ui", "adapter")
55
+ end
56
+
57
+ def ui_verbose?
58
+ dig("ui", "verbose") == true
59
+ end
60
+
61
+ # -- Display section --
62
+ def display_streaming?
63
+ dig("display", "streaming") == true
64
+ end
65
+
66
+ # The status bar under the chat input (display.statusbar, default true).
67
+ # Only an explicit false disables it.
68
+ def display_statusbar?
69
+ dig("display", "statusbar") != false
70
+ end
71
+
72
+ # Transcript preview budget for tool output
73
+ # (display.tool_output_preview_lines): head lines shown before the
74
+ # "… +N lines (full output → context)" marker. 0 = no collapse (full
75
+ # dump). Display-only — the model-facing output is untouched.
76
+ def display_tool_output_preview_lines
77
+ value = dig("display", "tool_output_preview_lines")
78
+ value.nil? ? 3 : value.to_i
79
+ end
80
+
81
+ # Cap on the chat input's visual rows (display.input_max_rows). Falls
82
+ # back to the composer default for nil/zero/garbage so a bad value can
83
+ # never collapse or unbound the input block.
84
+ def display_input_max_rows
85
+ value = dig("display", "input_max_rows").to_i
86
+ value.positive? ? value : UI::BottomComposer::MAX_INPUT_ROWS
87
+ end
88
+
89
+ # -- Paste section (UI::PasteStore: the file-backed paste pipeline) --
90
+ # A paste with MORE than this many lines collapses to a
91
+ # "[Pasted text #N +M lines]" placeholder in the composer (expanded to
92
+ # the full body at send). Falls back for nil/zero/garbage.
93
+ def paste_collapse_lines
94
+ value = dig("paste", "collapse_lines").to_i
95
+ value.positive? ? value : UI::PasteStore::DEFAULT_COLLAPSE_LINES
96
+ end
97
+
98
+ # A paste estimated above this many tokens (chars/4, the same rule
99
+ # compaction uses) overflows to <home>/sessions/<id>/paste_N.txt and the
100
+ # message carries a read-tool pointer instead of the content.
101
+ def paste_file_threshold_tokens
102
+ value = dig("paste", "file_threshold_tokens").to_i
103
+ value.positive? ? value : UI::PasteStore::DEFAULT_THRESHOLD_TOKENS
104
+ end
105
+
106
+ # -- Notifications section (UI::Notifier: attention bell + hook) --
107
+ # enabled/bell are on unless explicitly false; command is nil unless a
108
+ # non-empty string is set; min_turn_seconds falls back to the default.
109
+ def notifications_enabled?
110
+ dig("notifications", "enabled") != false
111
+ end
112
+
113
+ def notifications_bell?
114
+ dig("notifications", "bell") != false
115
+ end
116
+
117
+ def notifications_command
118
+ value = dig("notifications", "command").to_s
119
+ value.empty? ? nil : value
120
+ end
121
+
122
+ def notifications_min_turn_seconds
123
+ value = dig("notifications", "min_turn_seconds")
124
+ (value.nil? ? Defaults.dig("notifications", "min_turn_seconds") : value).to_f
125
+ end
126
+
127
+ # -- Streaming section --
128
+ def streaming_enabled?
129
+ dig("streaming", "enabled") == true
130
+ end
131
+
132
+ # -- Agent section --
133
+ def agent_max_turns
134
+ dig("agent", "max_turns")
135
+ end
136
+
137
+ # Iteration/time caps fall back to the built-in defaults when the config
138
+ # value is nil/missing (e.g. `config set agent.max_tool_iterations nil`,
139
+ # whose writer coerces "nil" -> nil). A bare nil here would crash every
140
+ # turn in IterationBudget's numeric comparisons (#139).
141
+ def agent_max_tool_iterations
142
+ dig("agent", "max_tool_iterations") || Defaults.dig("agent", "max_tool_iterations")
143
+ end
144
+
145
+ def agent_max_turn_seconds
146
+ dig("agent", "max_turn_seconds") || Defaults.dig("agent", "max_turn_seconds")
147
+ end
148
+
149
+ def agent_api_max_retries
150
+ dig("agent", "api_max_retries")
151
+ end
152
+
153
+ def agent_disabled_toolsets
154
+ dig("agent", "disabled_toolsets") || []
155
+ end
156
+
157
+ # -- Tasks / nested-subagent caps --
158
+ # Maximum nesting depth for the `task` delegation tree. depth 0 is a
159
+ # human/top-level-spawned child; the cap bounds how deep a chain of
160
+ # subagents-spawning-subagents may go. Default 2 ⇒ human→child→grandchild.
161
+ # Falls back to the built-in default when missing/nil so the numeric caps
162
+ # in BackgroundTask#reserve never crash on a bare nil.
163
+ def tasks_max_depth
164
+ dig("tasks", "max_depth") || Defaults.dig("tasks", "max_depth")
165
+ end
166
+
167
+ # Maximum number of LIVE direct children a single node (the human/top-level
168
+ # or one subagent) may have at once. Default 3.
169
+ def tasks_max_children_per_node
170
+ dig("tasks", "max_children_per_node") || Defaults.dig("tasks", "max_children_per_node")
171
+ end
172
+
173
+ # Hard global ceiling on the total number of LIVE subagents across the whole
174
+ # tree, so depth × fan-out cannot blow past the process's thread/cost budget.
175
+ # Default 8.
176
+ def tasks_max_concurrent_total
177
+ dig("tasks", "max_concurrent_total") || Defaults.dig("tasks", "max_concurrent_total")
178
+ end
179
+
180
+ # Per-child budget for BILLED live probes (`probe(live:true)`). Over budget,
181
+ # the model is steered to the FREE live:false snapshot. Free snapshots are
182
+ # unlimited. Default 5.
183
+ def tasks_max_live_probes_per_child
184
+ dig("tasks", "max_live_probes_per_child") || Defaults.dig("tasks", "max_live_probes_per_child")
185
+ end
186
+
187
+ # Bound (seconds) a BLOCKING ask_parent waits for an answer before the child
188
+ # self-heals and proceeds with its best judgement (S5a). Reuses the
189
+ # approval-gate timeout convention — a sane upper bound, never "forever" —
190
+ # so an abandoned ask never parks the child's thread indefinitely. Default 900.
191
+ def tasks_ask_parent_timeout
192
+ dig("tasks", "ask_parent_timeout") || Defaults.dig("tasks", "ask_parent_timeout")
193
+ end
194
+
195
+ # -- Prompts section --
196
+ # The customer-facing preamble prepended to every assembled system
197
+ # prompt. nil/empty disables the layer.
198
+ def prompts_preamble
199
+ value = dig("prompts", "preamble")
200
+ return nil if value.nil?
201
+
202
+ text = value.to_s.strip
203
+ text.empty? ? nil : text
204
+ end
205
+
206
+ def prompts_environment_enabled?
207
+ # Default to on when the key is absent — env injection is the cheap
208
+ # win we don't want a forgetful config.yml to disable accidentally.
209
+ value = dig("prompts", "environment", "enabled")
210
+ value.nil? || value == true
211
+ end
212
+
213
+ def prompts_environment_extra_utilities
214
+ Array(dig("prompts", "environment", "extra_utilities")).map(&:to_s)
215
+ end
216
+
217
+ # Returns the override string for a given role name, or nil if the
218
+ # built-in default prompt should be used.
219
+ def prompts_override_for(role)
220
+ value = dig("prompts", "overrides", role.to_s)
221
+ return nil if value.nil?
222
+
223
+ text = value.to_s.strip
224
+ text.empty? ? nil : text
225
+ end
226
+
227
+ # -- Run lifecycle section --
228
+ # Returns Float seconds (or nil to disable). EventsOperation uses this
229
+ # to bound how long a "running" row can go without producing a new
230
+ # event before the watchdog promotes it to failed.
231
+ def run_idle_event_timeout
232
+ raw = dig("run", "idle_event_timeout")
233
+ return nil if raw.nil?
234
+
235
+ raw.to_f
236
+ end
237
+
238
+ # -- Compression section --
239
+ def compression_enabled?
240
+ dig("compression", "enabled") == true
241
+ end
242
+
243
+ def compression_threshold
244
+ dig("compression", "threshold")
245
+ end
246
+
247
+ def compression_gateway_threshold
248
+ dig("compression", "gateway_threshold")
249
+ end
250
+
251
+ def compression_target_ratio
252
+ dig("compression", "target_ratio")
253
+ end
254
+
255
+ def compression_protect_first_n
256
+ dig("compression", "protect_first_n")
257
+ end
258
+
259
+ def compression_protect_last_n
260
+ dig("compression", "protect_last_n")
261
+ end
262
+
263
+ def compression_max_summary_tokens
264
+ dig("compression", "max_summary_tokens")
265
+ end
266
+
267
+ def compression_preserve_tool_pairs?
268
+ dig("compression", "preserve_tool_pairs") == true
269
+ end
270
+
271
+ # -- Memory section --
272
+ def memory_enabled?
273
+ dig("memory", "enabled") == true
274
+ end
275
+
276
+ def memory_auto_extract?
277
+ dig("memory", "auto_extract") == true
278
+ end
279
+
280
+ def memory_char_limit
281
+ dig("memory", "memory_char_limit")
282
+ end
283
+
284
+ # Post-turn skill distillation. Defaults to true (skills feature on +
285
+ # distill key absent ⇒ distill on), mirroring memory_auto_extract? as the
286
+ # gate for an aux-spending background job. Turning skills off disables it
287
+ # too, since there is no point distilling skills that won't be loaded.
288
+ def skills_auto_distill?
289
+ return false unless dig("skills", "enabled") != false
290
+
291
+ value = dig("skills", "auto_distill")
292
+ value.nil? || value == true
293
+ end
294
+
295
+ def memory_user_char_limit
296
+ dig("memory", "user_char_limit")
297
+ end
298
+
299
+ # Ingest/store budget for the live memory set, decoupled from the
300
+ # injection budget (`memory_char_limit`). `nil` => unbounded ingest.
301
+ def memory_ingest_char_limit
302
+ dig("memory", "ingest_char_limit")
303
+ end
304
+
305
+ # -- Jobs section --
306
+ def jobs_mode
307
+ dig("jobs", "mode")
308
+ end
309
+
310
+ def jobs_poll_interval
311
+ dig("jobs", "poll_interval")
312
+ end
313
+
314
+ def jobs_max_attempts
315
+ dig("jobs", "max_attempts")
316
+ end
317
+
318
+ # -- Tools section --
319
+ def tool_enabled?(name)
320
+ dig("tools", name.to_s) == true
321
+ end
322
+
323
+ def tool_output_max_bytes
324
+ dig("tool_output", "max_bytes")
325
+ end
326
+
327
+ def tool_output_max_lines
328
+ dig("tool_output", "max_lines")
329
+ end
330
+
331
+ # -- Security section --
332
+ def approvals_mode
333
+ dig("approvals", "mode")
334
+ end
335
+
336
+ # Seconds a run blocks on a human approval/clarification before the gate
337
+ # gives up and AUTO-DENIES (freeing the worker thread). nil = wait
338
+ # indefinitely (interruptible only by an explicit stop). Used by
339
+ # ApprovalGate as its default await deadline so an abandoned approval
340
+ # never parks a server worker for the whole window (W1).
341
+ def approvals_wait_timeout
342
+ raw = dig("approvals", "wait_timeout_seconds")
343
+ return nil if raw.nil?
344
+
345
+ raw.to_f
346
+ end
347
+
348
+ # Auto-allow provably read-only shell commands (ls, cat, grep, git log,
349
+ # ...) without an approval prompt. Default ON (key absent = on); the
350
+ # hardline floor and permissions:deny still precede it.
351
+ def auto_allow_readonly?
352
+ dig("approvals", "auto_allow_readonly") != false
353
+ end
354
+
355
+ # Extra command names / leading-token prefixes merged into the built-in
356
+ # read-only set (Security::ReadonlyCommands::SAFE_COMMANDS).
357
+ def approvals_readonly_commands
358
+ dig("approvals", "readonly_commands") || []
359
+ end
360
+
361
+ # When true, a `shell` tool call must always be confirmed in manual mode
362
+ # even if the tool's own risk level wouldn't otherwise require it. Default
363
+ # true (key absent = on) so shell-by-default stays gated behind a human.
364
+ def require_confirmation_for_shell?
365
+ dig("security", "require_confirmation_for_shell") != false
366
+ end
367
+
368
+ # Effective shell prompt policy: :confirm_all (every not-otherwise-allowed
369
+ # shell command prompts — today's default) or :dangerous_only (safe shell
370
+ # commands run unprompted; only DangerousPatterns matches prompt).
371
+ #
372
+ # Resolution / coercion (documented in defaults.rb):
373
+ # - if security.confirm_policy is set explicitly, it WINS (over the
374
+ # legacy alias);
375
+ # - otherwise it is DERIVED from require_confirmation_for_shell
376
+ # (true -> :confirm_all, false -> :dangerous_only),
377
+ # so any deployment that only ever set the old alias keeps its behavior.
378
+ # An unrecognized value falls back to the derived alias result.
379
+ def confirm_policy
380
+ raw = dig("security", "confirm_policy")
381
+ return raw.to_sym if %w[confirm_all dangerous_only].include?(raw.to_s)
382
+
383
+ require_confirmation_for_shell? ? :confirm_all : :dangerous_only
384
+ end
385
+
386
+ def security_command_allowlist
387
+ dig("security", "command_allowlist") || []
388
+ end
389
+
390
+ # -- Providers section --
391
+ def provider_config(name)
392
+ dig("providers", name.to_s) || {}
393
+ end
394
+
395
+ # -- Auxiliary section --
396
+ def auxiliary_compression_config
397
+ dig("auxiliary", "compression") || {}
398
+ end
399
+
400
+ def auxiliary_vision_config
401
+ dig("auxiliary", "vision") || {}
402
+ end
403
+
404
+ # Generic accessor for auxiliary task config blocks. Returns {} when
405
+ # the task isn't defined, so callers can chain .dig safely.
406
+ def auxiliary_config(task)
407
+ dig("auxiliary", task.to_s) || {}
408
+ end
409
+
410
+ # Returns true when the primary model can ingest images directly. Honours
411
+ # an explicit `model.supports_vision` override; otherwise falls back to
412
+ # ContentBuilder's name-pattern heuristic. Used by VisionTool to decide
413
+ # whether to expose itself (no point delegating if the primary can see).
414
+ def model_supports_vision?
415
+ raw = dig("model", "supports_vision")
416
+ return raw == true unless raw.nil?
417
+
418
+ LLM::ContentBuilder.supports_vision?(model_default.to_s)
419
+ end
420
+
421
+ # -- Generic access --
422
+ def dig(*keys)
423
+ @raw.dig(*keys)
424
+ end
425
+
426
+ def set(*keys, value)
427
+ hash = @raw
428
+ keys[0..-2].each do |key|
429
+ hash[key] ||= {}
430
+ hash = hash[key]
431
+ end
432
+ hash[keys.last] = value
433
+ end
434
+
435
+ def reload!
436
+ @raw = load_from_file
437
+ end
438
+
439
+ private
440
+
441
+ # The home this config is bound to: the explicit home_path passed at
442
+ # construction, else the same resolver the Loader uses (RUBINO_HOME →
443
+ # ~/.rubino). Read here (not at construction) so RUBINO_HOME just
444
+ # needs to be set before database_path is first read.
445
+ def resolved_home
446
+ @home_path || Loader.default_home_path
447
+ end
448
+
449
+ def load_from_file
450
+ loader = Loader.new(home_path: @home_path)
451
+ loader.load
452
+ end
453
+ end
454
+ end
455
+ end