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/docs/api/v1.md ADDED
@@ -0,0 +1,414 @@
1
+ # HTTP API v1
2
+
3
+ Base URL: `http://<host>:<port>/v1` (default port 4820)
4
+ Auth: `Authorization: Bearer <RUBINO_API_KEY>` on every request, EXCEPT `GET /v1/health` and `GET /v1/metrics`.
5
+ Content type: `application/json` unless noted.
6
+
7
+ Every endpoint listed below is covered by an end-to-end contract spec in `spec/rubino/api/contract/`. If a behavior here disagrees with the contract suite, the contract suite is the source of truth.
8
+
9
+ ## Conventions
10
+
11
+ | | |
12
+ |---|---|
13
+ | **IDs** | UUIDv4 strings (`SecureRandom.uuid`). |
14
+ | **Timestamps** | ISO 8601 UTC with the `Z` suffix. |
15
+ | **Error envelope** | `{"error": {"code": "string", "message": "string", "details": {...}?}}` — identical shape on every error code. `details` is present only when the error carries one (notably 422). |
16
+ | **Status codes** | 200/201/202/204 success · 401 auth · 404 missing resource or route · 409 conflict · 422 validation · 500 internal · 502 upstream. |
17
+ | **SSE** | streams use `id: <seq>\nevent: <type>\ndata: <json-payload>\n\n`. Supports `Last-Event-ID` for replay. |
18
+ | **Bearer** | scheme is case-insensitive (`Bearer` and `bearer` both accepted). Wrong/missing token → 401 envelope. |
19
+ | **Routing 404** | unknown route returns 404 in the standard envelope with `code: "not_found"`. |
20
+
21
+ ---
22
+
23
+ ## Health & metrics (unauthenticated)
24
+
25
+ ### `GET /v1/health` → 200 | 503
26
+ ```json
27
+ {
28
+ "status": "ok",
29
+ "version": "0.3.0",
30
+ "deps": {
31
+ "db": { "status": "ok" },
32
+ "scheduler": { "status": "ok", "scheduled_jobs": 0 }
33
+ }
34
+ }
35
+ ```
36
+ Returns 503 with `status: "degraded"` when any dep reports non-ok.
37
+
38
+ ### `GET /v1/metrics` → 200 `text/plain; version=0.0.4`
39
+ Prometheus text exposition. Registered metrics include `http_requests_total`, `http_request_duration_seconds`, `cron_fires_total`, `webhook_deliveries_total`, `oauth_token_exchanges_total`, `runs_total`, `runs_completed_total`, `skills_loaded_total`, `skills_created_total`. The two skill counters measure adoption vs. creation — see **[docs/skills.md](../skills.md#observability)**.
40
+
41
+ ---
42
+
43
+ ## Sessions
44
+
45
+ ### `GET /v1/sessions` → 200
46
+ Session index, most recent first. Query: `limit` (default 20, max 100), `q` (full-text search over message content; hits are deduped to sessions).
47
+ ```json
48
+ {
49
+ "sessions": [
50
+ { "id": "uuid", "title": "string|null", "status": "string",
51
+ "created_at": "ts", "updated_at": "ts",
52
+ "message_count": 12, "token_count": 3456 }
53
+ ]
54
+ }
55
+ ```
56
+
57
+ ### `POST /v1/sessions` → 201
58
+ ```json
59
+ // req — all fields optional
60
+ { "title": "string?", "parent_id": "uuid?", "instructions": "string?" }
61
+ // res
62
+ { "id": "uuid", "title": "string|null", "parent_id": "uuid|null", "created_at": "ts" }
63
+ ```
64
+
65
+ ### `GET /v1/sessions/:id` → 200 | 404
66
+ ```json
67
+ {
68
+ "id": "uuid",
69
+ "title": "string|null",
70
+ "instructions": null,
71
+ "status": "string",
72
+ "created_at": "ts",
73
+ "messages": [
74
+ { "id": "uuid", "role": "user|assistant|tool", "content": "string", "created_at": "ts" }
75
+ ]
76
+ }
77
+ ```
78
+
79
+ ### `DELETE /v1/sessions/:id` → 204 | 404
80
+ Cascade-deletes messages, runs, events, summaries, tool calls.
81
+
82
+ ### `POST /v1/sessions/:id/retry` → 202 | 404 | 409
83
+ Deletes the last user message (and everything after it), enqueues a fresh run with the same input.
84
+ ```json
85
+ { "run_id": "uuid", "session_id": "uuid", "status": "running" }
86
+ ```
87
+ 409 when the session has no user message to retry.
88
+
89
+ ### `POST /v1/sessions/:id/undo` → 200 | 404 | 409
90
+ Removes the last user message and everything after it (no re-run).
91
+ ```json
92
+ { "removed_messages": 3 }
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Runs
98
+
99
+ ### `POST /v1/sessions/:id/runs` → 201 | 404 | 422
100
+ ```json
101
+ // req
102
+ {
103
+ "input": "string", // required
104
+ "attachments": ["string"]?,
105
+ "skills": ["string"]?,
106
+ "model": "string|null",
107
+ "provider": "string|null"
108
+ }
109
+ // res
110
+ { "id": "uuid", "session_id": "uuid", "status": "running", "created_at": "ts" }
111
+ ```
112
+ The run is dispatched to a background Executor immediately; tail `/v1/runs/:id/events` for state.
113
+
114
+ ### `POST /v1/runs/:id/stop` → 200 | 404
115
+ Cooperative stop — flips the `stop_requested` flag; the executor exits between turns.
116
+ ```json
117
+ { "id": "uuid", "status": "stop_requested" }
118
+ ```
119
+
120
+ ### `GET /v1/runs/:id/events` → 200 (SSE) | 404
121
+ Replays persisted events first (honoring `Last-Event-ID`), then polls for new ones until the run reaches a terminal status (`completed`, `failed`, `stopped`) or disappears. Headers:
122
+ ```
123
+ content-type: text/event-stream
124
+ cache-control: no-cache
125
+ x-accel-buffering: no
126
+ ```
127
+ Event types that can appear on the stream: `message.completed`, `tool.started`, `tool.progress`, `tool.completed`, `artifact.created`, `input.injected`, `skill.loaded`, `subagent.spawned`, `subagent.completed`, `subagent.failed`, `approval.required` (`{approval_id, question, tool, command, choices, ...}`), `approval.decided` (`{approval_id, decision}`), `approval.expired` (`{approval_id}`), `clarify.required` (`{clarify_id, question}`), `run.completed`, `run.failed`, `run.stopped`.
128
+
129
+ `approval.decided` follows the `approval.required` frame once a decision lands (posted via the approvals endpoint), carrying the chosen `decision`; `approval.expired` fires instead if the gate's await deadline elapses first, after which the server treats the call as a safe deny. `run.stopped` is emitted on the clean-stop branch (a stop request that the run honored) — it is both this distinct event AND the terminal `stopped` status.
130
+
131
+ Unknown event types should be ignored rather than treated as errors, so a future addition never breaks a client.
132
+
133
+ The `id:` sequence is a global monotonic counter: ids keep climbing across runs, so they are NOT per-run ordinals. The stream is already scoped to one run (`for_run`), so use the `id:` only for `Last-Event-ID` replay, not to infer how many events a run produced.
134
+
135
+ API runs are **non-streaming**: the full answer arrives in the final `run.completed` frame. No `message.delta` is ever emitted — the agent loop disables token streaming for every API run (the always-registered `question` tool makes each run an interactive turn) — and there is no `reasoning.delta` event anywhere in the system. Do not wait for incremental text deltas.
136
+
137
+ ---
138
+
139
+ ## Approvals & clarifications
140
+
141
+ Both endpoints unblock an in-process gate registered when the run started. Decisions sent to a run with no live gate return 409.
142
+
143
+ ### `POST /v1/runs/:run_id/approvals/:approval_id` → 200 | 404 | 409 | 422
144
+ ```json
145
+ // req
146
+ { "decision": "once|session|always|always_prefix|always_command|deny|deny_always" }
147
+ // res
148
+ { "approval_id": "string", "decision": "string" }
149
+ ```
150
+ The enum is closed — anything else fails validation with 422. The approve values are kept in sync with `UI::API::APPROVE_DECISIONS`, plus the two explicit deny forms:
151
+
152
+ | decision | effect |
153
+ |---|---|
154
+ | `once` | approve this call only |
155
+ | `session` | approve this scope for the rest of the session (in-memory) |
156
+ | `always_prefix` | approve + persist an allow rule for the command's prefix (offered only when a prefix rule is derivable) |
157
+ | `always_command` | approve + persist an allow rule for this exact command |
158
+ | `always` | back-compat alias for `always_command` (older clients post it) |
159
+ | `deny` | deny this call once — nothing persisted, re-prompts next time |
160
+ | `deny_always` | deny + persist a `permissions:deny` rule, auto-denied across sessions |
161
+
162
+ The `approval.required` SSE frame carries a `choices` array listing the decisions offered for THAT request (e.g. `session` only when session caching applies, `always_prefix` only with a derivable prefix); clients should render `choices` rather than hardcoding the enum.
163
+
164
+ ### `POST /v1/runs/:run_id/clarifications/:clarify_id` → 200 | 404 | 409 | 422
165
+ ```json
166
+ // req
167
+ { "response": "string" }
168
+ // res
169
+ { "clarify_id": "string", "accepted": true }
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Skills
175
+
176
+ ### `GET /v1/skills` → 200
177
+ ```json
178
+ [{ "name": "string", "description": "string", "enabled": true }]
179
+ ```
180
+
181
+ ### `PUT /v1/skills/:name` → 200 | 404 | 422
182
+ ```json
183
+ // req
184
+ { "enabled": true }
185
+ // res
186
+ { "name": "string", "enabled": true }
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Models
192
+
193
+ ### `GET /v1/models` → 200
194
+ ```json
195
+ [{ "id": "provider/model", "provider": "string|null", "context_window": 128000 }]
196
+ ```
197
+ Source defaults to `RubyLLM.models.all`; some fields may be `null` for models that don't expose them.
198
+
199
+ ---
200
+
201
+ ## Mode
202
+
203
+ Process-level toggle that gates tool availability + approval policy. Lives in the Ruby heap (see `Rubino::Modes`); a restart resets to `default`. Valid values: `"default"`, `"plan"`, `"yolo"`.
204
+
205
+ | mode | effect |
206
+ |---|---|
207
+ | `default` | all tools registered, approval rules from config |
208
+ | `plan` | read-only tools only (no edits/shell/git) |
209
+ | `yolo` | all tools, approval policy bypassed |
210
+
211
+ ### `GET /v1/mode` → 200
212
+ ```json
213
+ {
214
+ "mode": "default",
215
+ "description": "all tools, approvals from config",
216
+ "available": [
217
+ { "mode": "default", "description": "..." },
218
+ { "mode": "plan", "description": "..." },
219
+ { "mode": "yolo", "description": "..." }
220
+ ]
221
+ }
222
+ ```
223
+ The minimum guaranteed shape is `{ "mode": "default"|"plan"|"yolo" }`; `description` and `available` are exposed so clients can render a picker without hardcoding the catalogue.
224
+
225
+ ### `PUT /v1/mode` → 200 | 422
226
+ ```json
227
+ // req
228
+ { "mode": "default"|"plan"|"yolo" }
229
+ // res
230
+ { "mode": "plan", "previous": "default", "description": "..." }
231
+ ```
232
+ Emits the same `mode_changed` UI event the CLI fires on `/mode plan`, so any in-flight SSE stream notices. An invalid `mode` (missing, wrong type, or not in the enum) returns the canonical error envelope:
233
+ ```json
234
+ { "error": { "code": "validation", "message": "invalid request body", "details": { "errors": { "mode": ["..."] } } } }
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Files
240
+
241
+ Workspace is sandboxed under `config.paths.home`. Path traversal raises `Workspace::PathTraversal < ValidationError` → 422.
242
+
243
+ ### `GET /v1/files?path=relative/path` → 200 | 404 | 422
244
+ Raw bytes as `application/octet-stream`. 422 when `path` query is missing/empty or escapes the sandbox. 404 when no file is at that path.
245
+
246
+ ### `POST /v1/files` → 201 | 422
247
+ Multipart `multipart/form-data` with a `file` part. 422 when the content-type is not multipart, or the `file` field is missing.
248
+ ```json
249
+ { "id": "uuid", "filename": "string", "size": 12345 }
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Cron jobs
255
+
256
+ In-process `rufus-scheduler` singleton. The scheduler does not survive multi-process scale-out (every worker would run every tick).
257
+
258
+ ### `POST /v1/jobs` → 201 | 422
259
+ ```json
260
+ // req
261
+ {
262
+ "name": "string", // required
263
+ "schedule": "cron string", // required
264
+ "prompt": "string", // required
265
+ "skills": ["string"]?,
266
+ "model": "string|null",
267
+ "provider": "string|null",
268
+ "deliver": "local|webhook" // optional, default "local"
269
+ }
270
+ // res — full serialized job
271
+ { "id": "uuid", "name": "string", "schedule": "string", "prompt": "string",
272
+ "skills": [], "model": null, "provider": null, "deliver": "local",
273
+ "enabled": true, "last_run_at": null, "last_run_id": null,
274
+ "created_at": "ts", "updated_at": "ts" }
275
+ ```
276
+
277
+ ### `GET /v1/jobs?include_disabled=true|false` → 200
278
+ Array of serialized jobs. `include_disabled=false` filters out paused.
279
+
280
+ ### `GET /v1/jobs/:id` → 200 | 404
281
+ ### `PATCH /v1/jobs/:id` → 200 | 404 | 422
282
+ Partial update. Same fields as create plus `enabled`. `skills` is an Array (returns as `skills`, never `skills_json` — the JSON column is internal).
283
+
284
+ ### `DELETE /v1/jobs/:id` → 204 | 404
285
+ Removes the row AND unschedules the rufus handle.
286
+
287
+ ### `POST /v1/jobs/:id/pause` → 200
288
+ Disables + unschedules. Returns the serialized job.
289
+
290
+ ### `POST /v1/jobs/:id/resume` → 200
291
+ Enables + reschedules.
292
+
293
+ ### `POST /v1/jobs/:id/trigger` → 202 | 404
294
+ Fires one run immediately, returns a run reference.
295
+ ```json
296
+ { "job_id": "uuid", "run_id": "uuid", "session_id": "uuid" }
297
+ ```
298
+
299
+ Webhook delivery: when `deliver: "webhook"`, on run completion rubino POSTs to `RUBINO_WEBHOOK_URL` with `{ job_id, run_id, status, session_id }`.
300
+
301
+ ---
302
+
303
+ ## Memory
304
+
305
+ The persistent memory store (same backend the CLI's `rubino memory` uses — see [`docs/memory.md`](../memory.md)).
306
+
307
+ ### `GET /v1/memory` → 200
308
+ Query: `limit` (default 50, max 200), `offset`, `q` (substring filter on content).
309
+ ```json
310
+ {
311
+ "memory": [
312
+ { "id": "uuid", "kind": "string", "content": "string",
313
+ "created_at": "ts", "updated_at": "ts" }
314
+ ]
315
+ }
316
+ ```
317
+
318
+ ### `GET /v1/memory/stats` → 200
319
+ ```json
320
+ { "backend": "sqlite", "count": 42 }
321
+ ```
322
+
323
+ ### `DELETE /v1/memory/:id` → 204 | 404
324
+
325
+ ---
326
+
327
+ ## Tasks
328
+
329
+ Background subagent runs spawned by the `task` tool (in-process registry; entries do not survive a restart).
330
+
331
+ ### `GET /v1/tasks` → 200
332
+ ```json
333
+ {
334
+ "tasks": [
335
+ { "id": "string", "subagent": "string", "prompt": "string",
336
+ "status": "running|completed|failed|cancelled",
337
+ "started_at": "ts|null", "elapsed_seconds": 1.234,
338
+ "result_summary": "string|null" }
339
+ ]
340
+ }
341
+ ```
342
+
343
+ ### `GET /v1/tasks/:id` → 200 | 404
344
+ The summary shape above plus `finished_at`, the full `result`, and `error`.
345
+
346
+ ### `POST /v1/tasks/:id/stop` → 202 | 404 | 409
347
+ Cooperative cancel of a running task (descendant ask-gates are cancelled too). Returns the task detail; 409 when the task already finished.
348
+
349
+ ---
350
+
351
+ ## OAuth
352
+
353
+ See [`docs/oauth-providers.md`](../oauth-providers.md) for the full PKCE flow, encryption key requirements, and per-provider setup. The HTTP surface:
354
+
355
+ ### `GET /v1/oauth/providers` → 200
356
+ Lists providers registered at boot via `OAuth::Registry.load_from_config!`. Empty when no `oauth.providers.*` section in config carries both `client_id` and `client_secret`.
357
+ ```json
358
+ [{ "id": "github", "display_name": "Github", "scopes": ["repo", "user:email"] }]
359
+ ```
360
+
361
+ ### `POST /v1/oauth/providers/:id/connect` → 200 | 404 | 422
362
+ Builds a PKCE authorize request. The client MUST persist `state` and `code_verifier` between connect and callback — rubino is stateless on the OAuth flow.
363
+ ```json
364
+ // req
365
+ { "redirect_uri": "https://your-client/oauth/callback", "scopes": ["string"]? }
366
+ // res
367
+ {
368
+ "authorize_url": "https://provider.example/authorize?...",
369
+ "state": "string",
370
+ "code_verifier": "string",
371
+ "provider": "github"
372
+ }
373
+ ```
374
+
375
+ ### `POST /v1/oauth/providers/:id/callback` → 201 | 404 | 422 | 502
376
+ The client posts the code it received at `redirect_uri`, plus the `state`/`code_verifier` it kept from connect, plus the `expected_state` it stored (constant-time compared against `state`).
377
+ ```json
378
+ // req — every field required
379
+ {
380
+ "code": "string",
381
+ "state": "string",
382
+ "expected_state": "string",
383
+ "code_verifier": "string",
384
+ "redirect_uri": "string"
385
+ }
386
+ // res 201 — serialized connection, tokens stripped
387
+ {
388
+ "id": "uuid",
389
+ "provider": "github",
390
+ "account_id": "string",
391
+ "account_email": "string|null",
392
+ "expires_at": "ts|null",
393
+ "scopes": ["string"],
394
+ "metadata": { },
395
+ "created_at": "ts",
396
+ "updated_at": "ts"
397
+ }
398
+ ```
399
+ 422 when `state != expected_state`. 502 when token exchange against the provider raises (counted under `oauth_token_exchanges_total{outcome="error"}`).
400
+
401
+ ### `GET /v1/oauth/connections` → 200
402
+ Array of serialized connections. Tokens are NEVER returned over the wire.
403
+
404
+ ### `DELETE /v1/oauth/connections/:id` → 204 | 404
405
+ Removes the row (encrypted tokens included). Does not attempt provider-side revocation.
406
+
407
+ ---
408
+
409
+ ## Operational notes
410
+
411
+ - **Auth bypass** — only `/v1/health` and `/v1/metrics` (see `Auth::SKIP_PATHS`). Every other route requires a valid bearer.
412
+ - **Token redaction** — the structured logger masks `access_token`, `refresh_token`, `id_token`, `client_secret`, `api_key`, `password`, `secret`, `bearer`, `authorization`, `http_authorization` (case-insensitive, recursive) before serialization.
413
+ - **Single process** — the scheduler, gate registry, and event polling all live in the Ruby heap. Scaling to multiple workers would require pulling these out (Redis, Postgres LISTEN, etc.).
414
+ - **Encryption key** — `RUBINO_ENCRYPTION_KEY` (32-byte base64) is required to use any OAuth route. Boot fails fast without it.
@@ -0,0 +1,177 @@
1
+ # Architecture
2
+
3
+ ## Overview
4
+
5
+ rubino is a lightweight agent that runs on a PC or inside a VM. It follows a
6
+ layered architecture with strict separation of concerns:
7
+
8
+ ```
9
+ Presentation Layer → CLI, JSON API Server
10
+ Orchestration Layer → Agent Router, Interaction Lifecycle
11
+ Core Layer → Agent Loop, Context, Memory, Jobs, Tools
12
+ Infrastructure Layer → LLM Adapter, Database, MCP, OAuth
13
+ ```
14
+
15
+ ## Key Design Principles
16
+
17
+ 1. **All output goes through UI** — No `puts`/`print` in core modules
18
+ 2. **LLM is isolated** — Only `LLM::RubyLLMAdapter` talks to ruby_llm
19
+ 3. **SQLite is the single database** — Sessions, memory, jobs, events
20
+ 4. **Event-driven** — Core emits events, UI/plugins subscribe
21
+ 5. **Plugin hooks** — 38 declared extension points for customization (design surface; few are wired today)
22
+ 6. **Config is not architecture** — Configuration describes what; architecture decides how
23
+
24
+ ## Module Map
25
+
26
+ ### `agent/`
27
+ Multiple agent types and @mention routing exist as a design surface; the
28
+ rubino runs a single agent by default and multi-agent routing is dormant.
29
+ - `AgentRegistry` — Defines all agent types (build, plan, explore, general, utility)
30
+ - `Router` — Routes input to appropriate agent via @mention
31
+ - `Definition` — Agent type with model, tools, permissions, MCP scoping
32
+ - `Runner` — Top-level orchestrator for a user interaction
33
+ - `Loop` — Core LLM call + tool execution cycle
34
+ - `IterationBudget` — Prevents runaway loops
35
+ - `ToolExecutor` — Executes tools with approval and result formatting
36
+
37
+ ### `interaction/`
38
+ - `Lifecycle` — Full turn lifecycle: input → memory → context → model → tools → persist → jobs
39
+ - `State` — State machine (idle → calling_model → executing_tools → finished)
40
+ - `EventBus` — Pub/sub for decoupling core from UI
41
+ - `Events` — All typed event constants
42
+
43
+ ### `context/`
44
+ - `PromptAssembler` — Builds the full prompt from all sources
45
+ - `TokenBudget` — Calculates token usage and decides when to compact (`needs_compaction?`)
46
+ - `Compressor` — Orchestrates compaction (flush memory → split → summarize → lineage)
47
+ - `MessageBoundary` — Splits messages into head/middle/tail
48
+ - `SummaryBuilder` — Generates structured summaries via LLM
49
+ - `ToolPairSanitizer` — Keeps tool_call/result pairs intact
50
+ - `FileDiscovery` — Finds project context files (.rubino.md, AGENTS.md, etc.)
51
+
52
+ ### `memory/`
53
+ - `Store` — CRUD for memories (7 kinds: user_profile, preference, fact, etc.)
54
+ - `Retriever` — Loads relevant memories for prompt inclusion
55
+ - `Extractor` — Pattern-based extraction from conversations
56
+ - `Deduplicator` — Jaccard similarity deduplication
57
+ - `Flusher` — Pre-compaction memory flush
58
+
59
+ ### `session/`
60
+ - `Repository` — Session CRUD with prefix-matching find
61
+ - `Store` — Message persistence
62
+ - `Message` — Value object with to_context / to_row
63
+
64
+ Forking is not a dedicated class: a new session inherits history via the
65
+ API's `parent_session_id` path.
66
+
67
+ ### `jobs/`
68
+ - `Queue` — SQLite-backed job queue with priority and scheduling
69
+ - `Runner` — Executes jobs, records runs
70
+ - `Worker` — Polling loop for background processing
71
+ - `Registry` — Maps job types to handler classes
72
+ - Handlers: ExtractMemory, SummarizeSession, CompactSession, CleanupSessions, DistillSkill
73
+
74
+ ### `tools/`
75
+ - `Base` — Abstract tool interface (name, description, input_schema, risk_level, call)
76
+ - `Registry` — Singleton registry with enable/disable
77
+ - `Result` — Structured result (success/error/denied)
78
+ - The built-in tools (authoritative, drift-checked count and list in [tools.md](tools.md)) + custom tool loader + formatter integration
79
+ - `CustomToolLoader` — DSL for user-defined tools
80
+
81
+ ### `llm/`
82
+ - `RubyLLMAdapter` — Wraps ruby_llm (chat, stream, structured output)
83
+ - `ProviderResolver` — Auto-detects provider from model name
84
+ - `ModelRegistry` — Known models with context windows
85
+ - `ContentBuilder` — Multipart content for vision (text + images)
86
+
87
+ ### `mcp/`
88
+ Experimental — booted at chat startup when `mcp.servers` is configured
89
+ (see [mcp.md](mcp.md)).
90
+ - `Manager` — Manages multiple MCP client connections
91
+ - `MCPToolWrapper` — Wraps MCP tools into Tools::Base interface
92
+
93
+ ### `security/`
94
+ - `ApprovalPolicy` — Decides allow/ask/deny per tool call
95
+ - `PatternMatcher` — Wildcard pattern matching for permissions
96
+ - `DoomLoopDetector` — Detects repeated identical tool calls
97
+ - `CommandAllowlist` — Pre-approved shell commands
98
+
99
+ ### `plugins/`
100
+ - `Registry` — Central hook registry; the hook set (38 points) is declared in
101
+ `plugins.rb` as a design surface, with few hooks wired today
102
+ - Loaded from `.rubino/plugins/`
103
+
104
+ ### `skills/`
105
+ - `Skill` — Parsed SKILL.md with YAML frontmatter
106
+ - `Registry` — Discovery from configured paths
107
+ - `SkillTool` — Tool for on-demand skill loading
108
+
109
+ ### `commands/`
110
+ - `Command` — Parsed command.md with template rendering
111
+ - `Loader` — Discovery from configured paths
112
+ - `Executor` — Handles slash commands and built-ins
113
+
114
+ ### `api/`
115
+ - `Server` — Rack + Puma boot
116
+ - `Router` — pattern-based dispatcher
117
+ - `Middleware::{Auth,ErrorHandler,JsonParser}` — Bearer auth, typed-error mapping, JSON body parsing
118
+ - `Operations::*` — request handlers (sessions, runs, approvals, clarifications, skills, models, files, cron jobs, oauth)
119
+
120
+ ### `oauth/`
121
+ - `Provider` (+ `Github`, `Google`) — provider abstraction with PKCE auth flow
122
+ - `Registry` — process-wide registry hydrated from config
123
+ - `ConnectionRepository` — encrypted token persistence (AES-256-GCM via `TokenEncryptor`)
124
+
125
+ ### `config/`
126
+ - `Loader` — Basic YAML loader
127
+ - `EnhancedLoader` — Multi-layer precedence with substitutions
128
+ - `RemoteConfig` — Enterprise remote config fetching
129
+ - `Configuration` — Typed accessors for all config sections
130
+ - `Writer` — Persists config changes
131
+ - `Defaults` — All default values
132
+
133
+ ### `database/`
134
+ - `Connection` — SQLite + WAL mode via Sequel
135
+ - `Migrator` — Versioned migrations
136
+
137
+ ### `ui/`
138
+ - `Base` — Abstract interface (info, error, stream, table, ask, confirm, etc.)
139
+ - `CLI` — TTY-based terminal output
140
+ - `Null` — Silent adapter for testing
141
+ - `API` — Structured event collector
142
+
143
+ ## Data Flow
144
+
145
+ ```
146
+ User Input
147
+
148
+ ├─→ Commands::Executor (if /command)
149
+ │ └─→ Render template → feed to agent
150
+
151
+ ├─→ Agent::Router (if @mention)
152
+ │ └─→ Select agent definition
153
+
154
+ └─→ Interaction::Lifecycle
155
+
156
+ ├─ Persist user message
157
+ ├─ Load memory (Retriever)
158
+ ├─ Extract images (ContentBuilder)
159
+ ├─ Build context (PromptAssembler)
160
+ ├─ Check token budget (TokenBudget)
161
+ ├─ Compact if needed (Compressor)
162
+
163
+ ├─ Agent::Loop
164
+ │ ├─ Call LLM (RubyLLMAdapter)
165
+ │ ├─ Stream to UI
166
+ │ ├─ If tool_calls:
167
+ │ │ ├─ Check permissions (ApprovalPolicy)
168
+ │ │ ├─ Check doom loop (DoomLoopDetector)
169
+ │ │ ├─ Execute tool (ToolExecutor)
170
+ │ │ ├─ Run plugin hooks
171
+ │ │ └─ Loop back to LLM
172
+ │ └─ Final text response
173
+
174
+ ├─ Persist session
175
+ ├─ Enqueue jobs (extract memory, summarize)
176
+ └─ Emit events → UI + SSE clients
177
+ ```