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
data/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # rubino
2
+
3
+ A coding & automation **agent** — small, self-contained, and built to run *where the work is*: directly on your machine or inside a VM. You drop it onto a box and it works there, reachable over a CLI and an HTTP API. It is not a heavy framework; it's a lightweight agent with persistent memory, sessions, and context compaction. Built on [ruby_llm](https://github.com/crmne/ruby_llm).
4
+
5
+ ## Why rubino
6
+
7
+ - **Runs where the work is** — a single gem on the machine (or VM) that holds the code, not a remote service you pipe files to.
8
+ - **Persistent memory** — a tiny SQLite "Zep"-style fact store that learns about you and the project across sessions.
9
+ - **Context compaction** — automatic compression with session lineage when the conversation outgrows the window.
10
+ - **CLI *and* HTTP API** — an interactive terminal session for humans, a bearer-protected JSON + SSE API for programs.
11
+ - **Real tools, gated** — read/write/edit, shell, ruby, git/github, grep/glob, a structured test runner, vision, and more, behind an approval model with a non-bypassable hardline floor.
12
+ - **Built on ruby_llm** — provider-agnostic: MiniMax, OpenAI, Anthropic, Gemini, or an OpenAI-compatible gateway.
13
+
14
+ ## Install
15
+
16
+ One line, Linux and macOS (x86_64 / arm64). Installs a compatible Ruby, then the gem — all in user space, no sudo:
17
+
18
+ ```bash
19
+ curl -fsSL https://raw.githubusercontent.com/Jhonnyr97/rubino-agent/main/install.sh | bash
20
+ ```
21
+
22
+ On **Linux** the installer fetches a precompiled Ruby via [`rv`](https://github.com/spinel-coop/rv). On **macOS**, if [Homebrew](https://brew.sh) is present it asks whether to use Homebrew (`brew install ruby`) or `rv`; without Homebrew it uses `rv` directly. Skip the prompt with `RUBINO_INSTALL_METHOD=brew` or `=rv`.
23
+
24
+ > **Review before you pipe.** Piping a script into your shell runs whatever it contains. Read it first:
25
+ > ```bash
26
+ > curl -fsSL https://raw.githubusercontent.com/Jhonnyr97/rubino-agent/main/install.sh -o install.sh
27
+ > less install.sh && bash install.sh
28
+ > ```
29
+
30
+ The installer is idempotent — safe to re-run — and prints the exact `PATH` line for the `rubino` executable plus the next step.
31
+
32
+ **Manual install** (if you'd rather not pipe, or already manage Ruby yourself):
33
+
34
+ ```bash
35
+ # With rv (https://rv.dev):
36
+ curl -LsSf https://rv.dev/install | sh
37
+ rv ruby install 3.3.3
38
+ rv run --ruby 3.3.3 gem install rubino-agent
39
+
40
+ # Or with any Ruby >= 3.1 already on your PATH:
41
+ gem install rubino-agent
42
+ ```
43
+
44
+ ## Quick Start
45
+
46
+ ```bash
47
+ rubino setup # guided first-run: pick a provider, paste a key
48
+ rubino chat # start chatting; ask "what does this project do?"
49
+ ```
50
+
51
+ `rubino setup` runs an interactive wizard that picks a provider/model and stores your API key — no hand-editing of YAML to get a first answer. If you skip the wizard, a bare `rubino chat` from a fresh home launches it for you before the first message.
52
+
53
+ New here? Read **[docs/getting-started.md](docs/getting-started.md)** — install → setup → first working message.
54
+
55
+ In development:
56
+
57
+ ```bash
58
+ git clone https://github.com/Jhonnyr97/rubino-agent.git
59
+ cd rubino-agent
60
+ bundle install
61
+ bundle exec rubino setup
62
+ bundle exec rubino chat
63
+ ```
64
+
65
+ ## Requirements
66
+
67
+ - Ruby >= 3.1
68
+ - SQLite3
69
+ - An LLM provider API key (MiniMax, OpenAI, Anthropic, or Google) — or any OpenAI-compatible gateway.
70
+
71
+ ## Essential commands
72
+
73
+ | Command | What it does |
74
+ |---|---|
75
+ | `rubino setup` | Guided first-run: provider/model/key, config + database |
76
+ | `rubino chat` | Interactive session (bare `chat` auto-resumes your last session) |
77
+ | `rubino chat --new` | Start a fresh session instead of resuming |
78
+ | `rubino prompt "..."` | One-shot, non-interactive (alias for `chat -q`) |
79
+ | `rubino server` | Start the JSON API + SSE server |
80
+ | `rubino doctor` | Check config, credentials, and database health |
81
+ | `rubino tools` | List tools and their enabled/disabled state |
82
+ | `rubino memory list` | Inspect stored memories (uses the active backend) |
83
+ | `rubino version` | Print the version |
84
+ | `rubino update` | Update to the latest published version via RubyGems |
85
+
86
+ `rubino update` runs `gem update rubino-agent` under the active interpreter (multi-Ruby safe); for a source/dev checkout it points you back at the installer instead. On interactive boot rubino shows a single dim line when a newer version is available (`▸ rubino vX.Y available — run \`rubino update\``). The check is cached and refreshed out-of-band (once / 24h, short timeout), so it never slows startup and is silent offline. Set `RUBINO_NO_UPDATE_CHECK=1` to disable it (it is also off when not a TTY or under `CI`). It prints nothing until the gem is actually published. See **[docs/commands.md](docs/commands.md)**.
87
+
88
+ Inside a chat, type `/help` for the slash commands (`/status`, `/sessions`, `/memory`, `/agents`, `/skills`, `/mode`, `/commands`, `/new`, …). The full reference is **[docs/commands.md](docs/commands.md)**.
89
+
90
+ ## Configuration
91
+
92
+ Configuration lives in `~/.rubino/config.yml` (created by `rubino setup`); secrets go in `~/.rubino/.env`. Both follow `RUBINO_HOME` if set. A representative slice (defaults shown):
93
+
94
+ ```yaml
95
+ model:
96
+ default: "openai/gpt-4.1" # the shipped default — see the note below
97
+ provider: "auto" # auto | openai | anthropic | bedrock | gemini | minimax | gateway
98
+ temperature: 0.3
99
+
100
+ agent:
101
+ max_turns: 90
102
+ max_tool_iterations: 8
103
+
104
+ memory:
105
+ enabled: true
106
+ backend: "sqlite" # tiny-Zep FTS5 + graph-lite recall (default)
107
+ auto_extract: true
108
+
109
+ compression:
110
+ enabled: true
111
+ threshold: 0.50
112
+
113
+ jobs:
114
+ mode: "inline" # inline | manual | worker
115
+
116
+ tools:
117
+ workspace_strict: true # sandbox write/edit/delete to the workspace
118
+ git: true
119
+ shell: true # ON by default; every command is still approval-gated
120
+ ruby: true
121
+ web: false # gates BOTH webfetch and websearch
122
+ memory: true
123
+ ```
124
+
125
+ > **Heads-up on the default model.** The shipped `model.default` is `openai/gpt-4.1`, which ruby_llm's registry resolves to **OpenRouter** — so a first run with no OpenAI/OpenRouter key fails fast with guidance instead of hanging. Run `rubino setup` (the wizard defaults to OpenAI gpt-4.1) or set your provider/key explicitly. See **[docs/models-and-keys.md](docs/models-and-keys.md)**.
126
+
127
+ Full reference (every key, env vars, precedence): **[docs/configuration.md](docs/configuration.md)**.
128
+
129
+ ## Documentation
130
+
131
+ - **[Getting started](docs/getting-started.md)** — install → setup → first message
132
+ - **[Models & keys](docs/models-and-keys.md)** — which provider/model/key, per-provider setup blocks
133
+ - **[Commands](docs/commands.md)** — CLI subcommands + slash-command reference
134
+ - **[Configuration](docs/configuration.md)** — full config + env vars + precedence
135
+ - **[Tools](docs/tools.md)** — the built-in tool set and approval behavior
136
+ - **[Skills](docs/skills.md)** — reusable instruction packs, the 3-level disclosure, and `SKILL_LOADED` observability
137
+ - **[Memory](docs/memory.md)** — the SQLite tiny-Zep backend
138
+ - **[Security](docs/security.md)** — approval model, hardline floor, TLS
139
+ - **[Troubleshooting](docs/troubleshooting.md)** — keyed on the exact error strings
140
+ - **[HTTP API](docs/api/v1.md)** · **[Jobs & cron](docs/jobs.md)** · **[OAuth providers](docs/oauth-providers.md)** · **[Architecture](docs/architecture.md)**
141
+ - **[Contributing](CONTRIBUTING.md)** · **[Changelog](CHANGELOG.md)**
142
+
143
+ ## Built-in tools
144
+
145
+ The agent ships **33 built-in tools**: `read`, `summarize_file`, `write`, `edit`, `multi_edit`, `grep`, `glob`, `git`, `github`, `shell`, `shell_output`, `shell_tail`, `shell_input`, `shell_kill`, `ruby`, `run_tests`, `apply_patch`, `webfetch`, `websearch`, `question`, `todowrite`, `memory`, `session_search`, `attach_file`, `vision`, `skill`, `task`, `task_result`, `task_stop`, `ask_parent`, `steer`, `probe`, `answer_child`. Each is gated by a `tools.<key>` config flag (opt-out) and the approval model. See **[docs/tools.md](docs/tools.md)**.
146
+
147
+ ## Skills
148
+
149
+ Skills are reusable instruction packs (a `SKILL.md` plus optional bundled reference files) that the agent pulls into context only when relevant — it sees a short index of available skills up front (Level 1), loads a skill's full body on demand (Level 2, which emits the `SKILL_LOADED` signal), and reads bundled references when needed (Level 3). They live in `.rubino/skills` / `~/.rubino/skills`, are gated by `tools.skill`, and expose usage/creation metrics. See **[docs/skills.md](docs/skills.md)**.
150
+
151
+ ## Fake LLM provider
152
+
153
+ rubino ships a built-in **fake LLM provider** for tests, demos, and integration harnesses. Unlike a mocked adapter, the fake provider plugs into the *real* `Agent::Loop`, the real `ToolExecutor`, the real approvals/clarifications pipeline, and the real SSE stream — scenarios fake only what an LLM would produce (content / thinking chunks and `tool_call` requests). Downstream consumers hit the same surface they would against OpenAI/Anthropic, at zero token cost.
154
+
155
+ ```yaml
156
+ model:
157
+ default: "fake/happy-path"
158
+
159
+ providers:
160
+ fake:
161
+ scenarios_dir: "~/.rubino/scenarios" # optional; defaults to built-in
162
+ ```
163
+
164
+ Any `model_id` starting with `fake` is auto-routed to the fake provider. Because it can short-circuit tool decisions, it is **disabled by default** in `server` and `chat` — set `RUBINO_ALLOW_FAKE=1` to opt in. Production deployments must never set this.
165
+
166
+ ## HTTP API
167
+
168
+ Start the bearer-protected JSON API server:
169
+
170
+ ```bash
171
+ export RUBINO_API_KEY="$(openssl rand -hex 32)"
172
+ export RUBINO_ENCRYPTION_KEY="$(openssl rand -base64 32)" # required for OAuth routes
173
+ rubino server --port 4820
174
+ ```
175
+
176
+ Every request carries `Authorization: Bearer <RUBINO_API_KEY>` except `GET /v1/health` and `GET /v1/metrics`. The server binds `127.0.0.1` by default — pass `--host 0.0.0.0` (or set `RUBINO_API_HOST`) to expose it, and only do so behind TLS or a trusted segment. For a remote HTTP client the API can serve over a self-signed cert the client pins (`RUBINO_TLS=1`; read it with `rubino tls-cert`).
177
+
178
+ Full request/response shapes, the error envelope, and SSE replay are in **[docs/api/v1.md](docs/api/v1.md)**.
179
+
180
+ ## Planned / on the roadmap
181
+
182
+ These are designed-in but not fully wired yet — don't depend on them in production:
183
+
184
+ - **MCP Support** — connect to Model Context Protocol servers via [ruby_llm-mcp](https://github.com/patvice/ruby_llm-mcp) ([docs/mcp.md](docs/mcp.md)).
185
+ - **Multi-Agent** — Build / Plan / Explore agents with `@mention` routing ([docs/agents.md](docs/agents.md)).
186
+ - **Plugin Hooks** — event hooks for extending behavior ([docs/plugins.md](docs/plugins.md)).
187
+
188
+ ## Development
189
+
190
+ ```bash
191
+ bundle install
192
+ bundle exec rspec # run tests
193
+ bundle exec rubino doctor # verify setup
194
+ ```
195
+
196
+ See **[CONTRIBUTING.md](CONTRIBUTING.md)** for the full dev/test/release flow.
197
+
198
+ ## License
199
+
200
+ [MIT](LICENSE).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/docs/agents.md ADDED
@@ -0,0 +1,190 @@
1
+ # Agents & Subagents
2
+
3
+ rubino has two distinct multi-agent surfaces. Only the first one ships today:
4
+
5
+ 1. **Background subagents** (✅ shipping) — the agent delegates bounded sub-tasks
6
+ to isolated subagent runs via its `task` tool, and you supervise them with
7
+ `/agents` and `/reply`. This is the surface you will actually use.
8
+ 2. **Primary-agent switching** (⏳ not yet wired) — Tab-cycling between primary
9
+ agents and `@mention` routing. The machinery exists (`Agent::Router`,
10
+ `Agent::Definition`, `AgentRegistry`) but no call site passes an agent
11
+ definition yet, so the default (build) agent handles every turn. See
12
+ [the last section](#planned-primary-agent-switching--mentions-not-yet-wired).
13
+
14
+ ---
15
+
16
+ ## Background subagents (what ships)
17
+
18
+ ### How they start
19
+
20
+ The MODEL spawns subagents with the `task` tool — you don't start them by hand;
21
+ you ask for something parallelizable ("audit these 4 files in parallel") and the
22
+ agent delegates. By default a `task` call runs in the **background**: it returns
23
+ immediately with a task id (`sa_…`) and the subagent works on its own thread
24
+ while the parent keeps going. When it finishes, the parent is notified with a
25
+ `[background-task] <id> completed` message folded into its turn; the parent can
26
+ also poll with `task_result(<id>)` or cancel with `task_stop(<id>)`.
27
+ `background: false` runs the child inline instead (the parent blocks); it goes
28
+ through the same nesting caps and ownership stamping as a background spawn.
29
+
30
+ Each subagent is **isolated**: it gets a fresh session seeded with ONLY the
31
+ prompt string — the parent transcript never leaks into the child, so the parent
32
+ must put every needed file path / error / detail into the prompt.
33
+
34
+ Built-in subagents the model can delegate to:
35
+
36
+ | Subagent | Access | Description |
37
+ |---|---|---|
38
+ | **explore** | Read-only tools | Fast codebase search and navigation (max 20 turns) |
39
+ | **general** | Full tools | Complex multi-step tasks (max 50 turns) |
40
+
41
+ Background subagents live only in the current process (nothing is persisted —
42
+ they die with the CLI/server process).
43
+
44
+ ### Nesting and caps
45
+
46
+ Subagents keep the `task` tool, so a subagent CAN spawn its own subagents.
47
+ The tree is bounded in one place (`Tools::BackgroundTasks#reserve`) by three
48
+ config caps; when one is hit, the spawn is refused with a reason-specific
49
+ message instead of fanning out unbounded work:
50
+
51
+ | Config key | Default | Meaning |
52
+ |---|---|---|
53
+ | `tasks.max_depth` | `2` | Max nesting depth (human → child → grandchild) |
54
+ | `tasks.max_children_per_node` | `3` | Max live children per parent |
55
+ | `tasks.max_concurrent_total` | `8` | Max live subagents across the whole tree |
56
+
57
+ ### Statuses
58
+
59
+ `/agents` and the live cards show each child's state:
60
+
61
+ | Glyph | Status | Meaning | You act via |
62
+ |---|---|---|---|
63
+ | `●` | `running` | Working (last activity shown) | — |
64
+ | `●` | `needs_approval` | A child tool needs your approval | `/agents <id>` |
65
+ | `⛔` | `blocked_on_human` | Asked a question only YOU can answer (`ask_parent` escalated to the human) | `/reply <id> <answer>` |
66
+ | `◷` | `blocked_on_parent` | Asked its agent-parent a question — the PARENT MODEL answers (`answer_child`); not your job unless you choose to step in with `/reply` | (optional) `/reply <id>` |
67
+ | `◌` | `stopping` | Stop requested; unwinding at its next checkpoint | — |
68
+ | `✓` | `done` | Finished; result available | `/agents <id>` |
69
+ | `✗` | `failed` | Errored; error available | `/agents <id>` |
70
+ | `⊘` | `stopped` | Cancelled by you (`--stop`); blocked descendants unwound; tools that completed before the stop may have left side effects | `/agents <id>` |
71
+
72
+ A `⛔ N subagent waiting on you` marker persists until you `/reply`.
73
+
74
+ ### Supervising from the CLI: `/agents` and `/reply`
75
+
76
+ ```
77
+ /agents # list background subagents (status, tools run, activity)
78
+ /agents <id> # drill in: live watch while running, result/error when done
79
+ /agents <id> --stop # cancel a running subagent (blocked descendants unwind too)
80
+ /agents <id> steer "note" # park a note folded into the child's context at its next turn
81
+ /agents <id> probe "question" # ephemeral read-only peek — nothing is saved to the child
82
+ /reply <id> <answer> # answer a child blocked on an ask_parent question
83
+ /reply # bare: list the subagents currently blocked on you
84
+ ```
85
+
86
+ `/tasks` is an alias for `/agents`. Stopping a node cancels its descendants'
87
+ ask-gates too, so a blocking question anywhere in the subtree unwinds at once.
88
+
89
+ **steer** is a persistent course-correction: the note enters the child's context
90
+ at its next turn boundary and changes its trajectory.
91
+ **probe** is ephemeral: a read-only side-inference over a snapshot of the
92
+ child's transcript; the answer is shown to you and discarded — nothing is
93
+ appended to the child's history.
94
+
95
+ ### Parent↔child channels (model-driven)
96
+
97
+ The same three verbs are MODEL-callable tools, so an agent-parent can supervise
98
+ its own children the way you supervise yours. All are gated by `tools.task` and
99
+ **ownership-scoped at call time** — a caller can only touch its own direct
100
+ children (see [tools.md](tools.md) for parameters):
101
+
102
+ - **`steer(task_id, note)`** — park a persistent note on one of your running
103
+ children; it folds into the child's context at its next turn.
104
+ - **`probe(task_id, question, live:)`** — check on a child without disturbing it.
105
+ `live: false` (default) is a FREE registry snapshot (status, tool count, last
106
+ activity, recent lines); `live: true` is a billed one-shot model peek over the
107
+ child's transcript, budgeted per child (`tasks.max_live_probes_per_child`,
108
+ default 5).
109
+ - **`ask_parent(question, blocking:)`** — the child→parent escalation (only
110
+ available to subagents). `blocking: false` (default) keeps the child working
111
+ and folds the answer in later; `blocking: true` parks the child until answered,
112
+ bounded by `tasks.ask_parent_timeout` (default 900s — on expiry the child
113
+ proceeds with its best judgement instead of hanging).
114
+ Routing depends on who spawned the child: an agent-parent gets the question as
115
+ a note and answers with `answer_child` (child shows `◷ blocked_on_parent`); a
116
+ human-spawned child escalates straight to you (`⛔ blocked_on_human`, answered
117
+ via `/reply`). A parent that cannot answer from its own context escalates by
118
+ calling its OWN `ask_parent` — questions bubble up the tree to the human.
119
+ - **`answer_child(task_id, answer)`** — the agent-parent's `/reply`: delivers
120
+ the answer into the asking child's context (unblocks a blocking ask, folds in
121
+ for a non-blocking one).
122
+
123
+ ### Approvals inside a background child
124
+
125
+ When a background child's tool needs human approval, the child parks and the
126
+ entry flips to `needs_approval` with the question/command shown on its card;
127
+ resolve it via `/agents <id>`. In `yolo` mode the usual approval-skip rules
128
+ apply (hardline floor still enforced — see [security.md](security.md)).
129
+
130
+ ---
131
+
132
+ ## Built-in agent definitions
133
+
134
+ These definitions exist in `Agent::AgentRegistry` today. The two *subagents*
135
+ are live as `task` targets; the two *primary* agents are only reachable as the
136
+ default (`build`) or via the plan **mode** (`/mode plan`), not via agent
137
+ switching; the *utility* agents are internal.
138
+
139
+ | Agent | Type | Access | Description |
140
+ |-------|------|--------|-------------|
141
+ | **build** | primary | Full tools | Default development agent. Handles every turn today. |
142
+ | **plan** | primary | Read-only | Analysis/planning definition (the shipping read-only surface is `/mode plan`). |
143
+ | **explore** | subagent | Read-only | Fast codebase search and navigation (`task` target). |
144
+ | **general** | subagent | Full tools | Complex multi-step tasks (`task` target). |
145
+ | **compaction** | utility | None | Internal: compresses context. Hidden. |
146
+ | **title** | utility | None | Internal: generates session titles. Hidden. |
147
+
148
+ ### Custom agents (via code)
149
+
150
+ `AgentRegistry#register` accepts custom definitions programmatically:
151
+
152
+ ```ruby
153
+ Rubino.agent_registry.register(
154
+ Rubino::Agent::Definition.new(
155
+ name: "security",
156
+ type: :subagent,
157
+ description: "Security-focused code review",
158
+ system_prompt: "You are a security expert…",
159
+ tools: %w[read grep glob],
160
+ permissions: { "shell *" => "deny", "write *" => "deny" }
161
+ )
162
+ )
163
+ ```
164
+
165
+ A registered `:subagent` definition immediately becomes a valid `task` target
166
+ (it is advertised in the `task` tool's description). Each definition can carry
167
+ its own model, system prompt, tool list (`:all`, `:read_only`, or names),
168
+ pattern-based permission overrides (merged over the global rules by
169
+ `ApprovalPolicy`), MCP-server scoping, and a `max_turns` budget.
170
+
171
+ > **Note:** the `agents:` key in `config.yml` is reserved but **not yet read**
172
+ > by the registry — declaring custom agents in config has no effect today.
173
+
174
+ ---
175
+
176
+ ## Planned: primary-agent switching & @mentions (not yet wired)
177
+
178
+ > **Status:** the machinery exists — `Agent::Router` (@mention detection,
179
+ > Tab-cycling, default routing) and the `agent_definition:` plumbing through the
180
+ > runner — but **no call site passes an agent definition**, so Tab and
181
+ > `@explore`/`@plan`/`@general` mentions currently do nothing. Use the
182
+ > background-subagent surface above for real work.
183
+
184
+ The intended design: press **Tab** to cycle through primary agents, or route a
185
+ single message with an `@mention`:
186
+
187
+ ```
188
+ you > @explore Where is the database connection configured?
189
+ you > @plan How should we restructure the auth module?
190
+ ```