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,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rubino/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rubino-agent"
7
+ spec.version = Rubino::VERSION
8
+ spec.authors = ["Jhon Rojas"]
9
+ spec.email = ["jhon@example.com"]
10
+
11
+ spec.summary = "A lightweight Ruby coding and automation agent with persistent memory, sessions, and context compaction"
12
+ spec.description = "A standalone, self-contained coding and automation agent built on ruby_llm. " \
13
+ "Provides an agent loop, persistent memory, SQLite sessions, context compaction, " \
14
+ "a job system, a tool registry, and an extensible UI layer."
15
+ spec.homepage = "https://github.com/Jhonnyr97/rubino-agent"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = ">= 3.1.0"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = spec.homepage
21
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
22
+ spec.metadata["rubygems_mfa_required"] = "true"
23
+
24
+ spec.files = Dir.chdir(__dir__) do
25
+ # Use git if available, otherwise glob
26
+ if system("git rev-parse --git-dir > /dev/null 2>&1")
27
+ `git ls-files -z`.split("\x0").reject do |f|
28
+ (File.expand_path(f) == __FILE__) ||
29
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
30
+ end
31
+ else
32
+ Dir.glob("{lib,exe}/**/*").reject { |f| File.directory?(f) } +
33
+ %w[Gemfile Rakefile README.md CHANGELOG.md]
34
+ end
35
+ end
36
+
37
+ spec.bindir = "exe"
38
+ spec.executables = ["rubino"]
39
+ spec.require_paths = ["lib"]
40
+
41
+ # Core dependencies
42
+ spec.add_dependency "dry-configurable", "~> 1.0"
43
+ spec.add_dependency "dry-schema", "~> 1.13"
44
+ spec.add_dependency "faraday", "~> 2.9"
45
+ spec.add_dependency "faraday-retry", "~> 2.2"
46
+ spec.add_dependency "oauth2", "~> 2.0"
47
+ spec.add_dependency "puma", "~> 6.4"
48
+ spec.add_dependency "rack", "~> 3.1"
49
+ spec.add_dependency "ruby_llm", "~> 1.0"
50
+ spec.add_dependency "ruby_llm-mcp", "~> 1.0"
51
+ spec.add_dependency "rufus-scheduler", "~> 3.9"
52
+ spec.add_dependency "sequel", "~> 5.0"
53
+ spec.add_dependency "sqlite3", "~> 2.0"
54
+ spec.add_dependency "thor", "~> 1.3"
55
+ spec.add_dependency "zeitwerk", "~> 2.6"
56
+
57
+ # CLI UI dependencies
58
+ spec.add_dependency "kramdown", "~> 2.5"
59
+ spec.add_dependency "kramdown-parser-gfm", "~> 1.1"
60
+ spec.add_dependency "pastel", "~> 0.8"
61
+ spec.add_dependency "tty-box", "~> 0.7"
62
+ spec.add_dependency "tty-prompt", "~> 0.23"
63
+ spec.add_dependency "tty-spinner", "~> 0.9"
64
+ spec.add_dependency "tty-table", "~> 0.12"
65
+ spec.add_dependency "unicode-display_width", "~> 2.6"
66
+
67
+ # Reline used to ship with Ruby, but it was removed from default gems
68
+ # in Ruby 4.0 and is now a regular gem. UI::LineInput depends on it for
69
+ # the interactive prompt (history, completion, multi-line editing).
70
+ spec.add_dependency "reline", "~> 0.5"
71
+
72
+ # `csv` left the default gems in Ruby 3.4. The in-repo document converter
73
+ # (Rubino::Documents) uses it for the CORE csv->Markdown format, so it is a
74
+ # hard runtime dependency (the converter still falls back to a built-in
75
+ # splitter if it is ever absent, but we ship it so csv always works).
76
+ spec.add_dependency "csv", "~> 3.2"
77
+
78
+ # Optional document-conversion extraction gems (Rubino::Documents, #6). These
79
+ # are NOT hard runtime dependencies: each converter `require`s its gem lazily
80
+ # inside begin/rescue LoadError and reports itself unavailable when the gem is
81
+ # absent, so the module loads and runs with none of them installed (callers
82
+ # then fall back to the shell-extraction hint). They are declared as
83
+ # development dependencies so CI/specs can exercise the gem-backed converters;
84
+ # an end user installs only the formats they need (e.g. `gem install roo`).
85
+ # All MIT-licensed. html/xml use kramdown/rexml which are already present.
86
+ #
87
+ # NOTE: `ruby_powerpoint` is deliberately NOT in the dev bundle -- it pins
88
+ # `rubyzip ~> 1.0`, which is irreconcilable with `docx`/`roo` (rubyzip ~> 2.x)
89
+ # in a single Gemfile. The Pptx converter is therefore exercised by its
90
+ # degradation path and unit-level shaping (a stubbed gem interface) rather
91
+ # than the live gem; an end user who needs pptx installs ruby_powerpoint into
92
+ # their own (compatible) environment. This is exactly the optional-require
93
+ # design: a missing/absent gem never breaks the module.
94
+ spec.add_development_dependency "docx", "~> 0.8"
95
+ spec.add_development_dependency "pdf-reader", "~> 2.12"
96
+ spec.add_development_dependency "roo", "~> 2.10"
97
+
98
+ # Development dependencies
99
+ spec.add_development_dependency "rack-test", "~> 2.1"
100
+ spec.add_development_dependency "rspec", "~> 3.12"
101
+ spec.add_development_dependency "rubocop", "~> 1.60"
102
+ spec.add_development_dependency "rubocop-rspec", "~> 3.0"
103
+ end
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: ruby-expert
3
+ description: Deep Ruby & Rails expertise — idioms, OO design, metaprogramming, errors/types, concurrency, Rails, testing, performance, security, tooling, gem authoring. Load when writing, reviewing, debugging, or designing Ruby or Rails code, or when a Ruby/Rails decision needs an authoritative answer.
4
+ ---
5
+
6
+ # Ruby expert
7
+
8
+ Authoritative, current (Ruby 3.2–3.4, Rails 7.1–8.x) knowledge for writing,
9
+ reviewing, and designing idiomatic Ruby. This file is the router: it carries the
10
+ non-negotiable defaults, then points you at one bundled reference for deep,
11
+ task-specific guidance. **Load the matching reference before answering a
12
+ non-trivial question in that area** — don't work from memory when a reference
13
+ covers it.
14
+
15
+ ## Non-negotiable defaults
16
+
17
+ Apply these unless the surrounding project clearly does otherwise (existing
18
+ project conventions always win over these defaults):
19
+
20
+ - **Match the codebase first.** Read neighbouring files and mirror their naming,
21
+ structure, and idioms before introducing your own. Consistency beats personal
22
+ preference.
23
+ - **`# frozen_string_literal: true`** as the first line of every Ruby source file.
24
+ - **Naming:** `snake_case` for methods/variables, `CamelCase` for classes/modules,
25
+ `SCREAMING_SNAKE_CASE` for constants, `?` suffix for predicates, `!` suffix only
26
+ for the dangerous/mutating variant that has a safe sibling.
27
+ - **Keyword arguments** for anything with more than one or two parameters, or any
28
+ boolean/optional flag — never a positional boolean.
29
+ - **`rescue StandardError`**, never a bare `rescue` or `rescue Exception`. Never
30
+ rescue just to swallow — handle, re-raise, or don't rescue.
31
+ - **Two-space indentation, no tabs.** Guard clauses over deep nesting. Prefer the
32
+ smallest method that reads clearly.
33
+ - **Tests are part of "done."** New behavior ships with a spec; a bug fix ships
34
+ with the regression test that would have caught it.
35
+ - **Run the linter.** Honor the project's `.rubocop.yml` / `standard`; don't fight
36
+ it or disable cops without a reason in a comment.
37
+ - **Security is not optional.** Never interpolate untrusted input into SQL, shell,
38
+ `eval`, `send`, or deserialization. See `references/security.md`.
39
+
40
+ ## Which reference to load
41
+
42
+ | Load `references/…` | When the task involves |
43
+ | --- | --- |
44
+ | `language-idioms.md` | Day-to-day Ruby: collections/Enumerable, pattern matching, blocks/procs/lambdas, keyword args, `Data`/`Struct`, hash idioms, nil handling, numbers/money |
45
+ | `datetime-and-encoding.md` | Dates/times/time zones (the `Time.now` vs `Time.zone.now` footgun, DST, parsing, monotonic clock) and string encoding (UTF-8, `force_encoding` vs `encode`, scrubbing bad bytes) |
46
+ | `metaprogramming.md` | `define_method`, `method_missing`, `send`, hooks, `class_eval`, refinements, building DSLs, introspection |
47
+ | `oo-design.md` | Class/module design, SOLID, composition vs inheritance, service/value/query/policy objects, Result objects, dependency injection, refactoring a god object |
48
+ | `errors-and-types.md` | Exception design, `rescue`/`retry`/`ensure`, custom errors, cause chaining, and RBS/Sorbet type checking |
49
+ | `concurrency.md` | Threads, mutexes/queues, the GVL, fibers, the `async` gem, Ractors, processes/fork — choosing a concurrency model |
50
+ | `rails.md` | Anything Rails: Active Record, migrations, controllers, routing, concerns, jobs, Hotwire, caching, Rails secure defaults |
51
+ | `testing.md` | RSpec or Minitest, FactoryBot, TDD, doubles/mocks, WebMock/VCR, request vs system specs, fixing flaky tests |
52
+ | `performance.md` | Profiling, memory/allocations, GC, YJIT, fixing a slow path or high-memory process |
53
+ | `security.md` | Injection (SQL/command/eval), mass assignment, deserialization, XSS/CSRF, authz/IDOR, secrets, Brakeman, dependency audit, ReDoS |
54
+ | `tooling.md` | Bundler, version managers (rbenv/asdf/mise/rv), the `debug` gem/pry, RuboCop/standard, Rake, CI, LSP |
55
+ | `gem-authoring.md` | Building or releasing a gem: gemspec, Zeitwerk, versioning/CHANGELOG, `rake release`, shipping assets |
56
+
57
+ When a task spans areas (e.g. "make this Rails query fast and safe"), load each
58
+ relevant reference. Each reference ends with a `## Quick checklist` you can scan
59
+ for the rules without re-reading the whole file.
60
+
61
+ ## How to apply
62
+
63
+ 1. Identify the area(s) the task touches and load the matching reference(s) above.
64
+ 2. Inspect the actual project (Gemfile, `.rubocop.yml`, existing code) — its
65
+ conventions override the generic defaults here.
66
+ 3. Write the change, then verify it: run the tests and the linter before calling
67
+ it done.
@@ -0,0 +1,357 @@
1
+ # Concurrency & parallelism
2
+
3
+ How to pick and use a concurrency model in modern Ruby (MRI/CRuby 3.2–3.4). The single most important fact: **MRI has a Global VM Lock (GVL), so threads give you concurrency for IO-bound work but NOT parallelism for CPU-bound work.** Choose the model from the workload, not from habit.
4
+
5
+ ## The GVL/GIL — what it does and does not do
6
+
7
+ The GVL (historically "GIL") ensures only **one thread executes Ruby bytecode at a time** in a single MRI process.
8
+
9
+ - It **does** prevent true parallel execution of pure-Ruby CPU work. Two threads spinning on math run no faster than one.
10
+ - It is **released** during blocking IO (sockets, file reads, `sleep`, many DB driver calls) and during some C-extension sections. So while one thread waits on the network, another runs Ruby. This is why threads help IO-bound workloads.
11
+ - It does **NOT** make your code thread-safe. The GVL can switch threads between bytecode instructions, so `count += 1` (read, add, write) can interleave and lose updates. You still need locks. See "Thread-safety hazards" below.
12
+
13
+ ```ruby
14
+ # CPU-bound: threads do NOT help on MRI (GVL serializes the work)
15
+ threads = 4.times.map { Thread.new { fib(35) } } # ~same wall time as serial
16
+ threads.each(&:join)
17
+
18
+ # IO-bound: threads DO help (GVL released during the HTTP wait)
19
+ urls.map { |u| Thread.new { Net::HTTP.get(URI(u)) } }.each(&:join)
20
+ ```
21
+
22
+ JRuby and TruffleRuby have **no GVL** — threads run truly parallel there, so CPU-bound threading works on those runtimes.
23
+
24
+ ## Thread basics
25
+
26
+ ```ruby
27
+ t = Thread.new(arg) do |x| # pass args explicitly; do NOT close over a loop var
28
+ do_work(x)
29
+ end
30
+ result = t.value # join + return the block's value (re-raises if it failed)
31
+ t.join # wait without caring about return value
32
+ t.join(5) # returns nil on timeout, else the thread
33
+ ```
34
+
35
+ DO pass loop variables as block args; DON'T capture them by closure:
36
+
37
+ ```ruby
38
+ # WRONG: all threads may see the final value of `i`
39
+ (0..9).each { |i| Thread.new { puts i } }
40
+
41
+ # RIGHT: bind per-thread via block argument
42
+ (0..9).each { |i| Thread.new(i) { |n| puts n } }
43
+ ```
44
+
45
+ **Thread-local / fiber-local storage.** `Thread#[]` is actually *fiber*-local (scoped to the current fiber). Use `Thread#thread_variable_get/set` for true per-thread state.
46
+
47
+ ```ruby
48
+ Thread.current[:tag] = "fiber-local" # fiber-scoped (surprising name)
49
+ Thread.current.thread_variable_set(:id, 7) # genuinely thread-scoped
50
+ ```
51
+
52
+ ### Exceptions in threads
53
+
54
+ An unhandled exception in a thread is stored and **re-raised when you call `join`/`value`**. If you never join, the exception is silently swallowed.
55
+
56
+ ```ruby
57
+ t = Thread.new { raise "boom" }
58
+ sleep 0.1 # main thread keeps running; nothing printed yet
59
+ t.join # NOW it raises "boom"
60
+ ```
61
+
62
+ DON'T flip `Thread.abort_on_exception = true` globally in libraries (it crashes the whole process on any thread error). For dev visibility prefer `Thread.report_on_exception = true` (default since 2.5) which logs but doesn't abort. Better: always `join` or wrap work in `begin/rescue` and push errors to a `Queue`.
63
+
64
+ ## Synchronization primitives
65
+
66
+ ### Mutex
67
+
68
+ ```ruby
69
+ mutex = Mutex.new
70
+ mutex.synchronize { @balance += amount } # critical section
71
+ ```
72
+
73
+ DON'T re-lock the same `Mutex` from the same thread (e.g. recursive call) — it raises `ThreadError` (deadlock). Use `Monitor` for reentrancy.
74
+
75
+ ### Monitor (reentrant mutex + condition vars)
76
+
77
+ ```ruby
78
+ require "monitor"
79
+ class Counter
80
+ include MonitorMixin # adds #synchronize to instances
81
+ def initialize = (super; @n = 0)
82
+ def incr = synchronize { @n += 1 } # reentrant: safe to call other synchronized methods
83
+ end
84
+ ```
85
+
86
+ ### ConditionVariable — wait/signal
87
+
88
+ Use when a thread must wait for a condition another thread sets. Always re-check the predicate in a loop (guard against spurious wakeups).
89
+
90
+ ```ruby
91
+ mutex, cond = Mutex.new, ConditionVariable.new
92
+ ready = false
93
+
94
+ # consumer
95
+ mutex.synchronize { cond.wait(mutex) until ready; consume }
96
+ # producer
97
+ mutex.synchronize { ready = true; cond.signal } # or broadcast for all waiters
98
+ ```
99
+
100
+ ### Queue / SizedQueue — the preferred producer-consumer tool
101
+
102
+ `Thread::Queue` is thread-safe and blocking out of the box. Prefer it over hand-rolled Mutex+ConditionVariable.
103
+
104
+ ```ruby
105
+ q = Thread::Queue.new # unbounded
106
+ sq = Thread::SizedQueue.new(100) # bounded -> applies backpressure on push
107
+
108
+ # producers
109
+ producers = files.map { |f| Thread.new { sq.push(parse(f)) } }
110
+
111
+ # consumers
112
+ workers = 4.times.map do
113
+ Thread.new do
114
+ while (item = sq.pop) # pop blocks until an item is available
115
+ handle(item)
116
+ end
117
+ end
118
+ end
119
+
120
+ producers.each(&:join)
121
+ workers.size.times { sq.push(nil) } # poison pills to stop consumers
122
+ workers.each(&:join)
123
+ # Alt: sq.close then `while (i = sq.pop); ...` exits when closed+drained
124
+ ```
125
+
126
+ `SizedQueue` is the idiomatic way to bound memory and rate: `push` blocks when full.
127
+
128
+ ## concurrent-ruby (`Concurrent::*`)
129
+
130
+ Battle-tested toolkit. Reach for it instead of building pools/atomics yourself.
131
+
132
+ ```ruby
133
+ require "concurrent-ruby"
134
+
135
+ # Bounded thread pool (don't spawn unbounded threads)
136
+ pool = Concurrent::FixedThreadPool.new(8)
137
+ pool.post { do_io }
138
+ pool.shutdown; pool.wait_for_termination
139
+
140
+ # Futures (run now, collect later)
141
+ futures = urls.map { |u| Concurrent::Future.execute { Net::HTTP.get(URI(u)) } }
142
+ results = futures.map(&:value) # blocks; check #rejected? / #reason for errors
143
+
144
+ # Thread-safe map (use instead of a plain Hash shared across threads)
145
+ cache = Concurrent::Map.new
146
+ cache.compute_if_absent(key) { expensive(key) } # atomic memoization
147
+
148
+ # Atomic counter (no Mutex needed)
149
+ counter = Concurrent::AtomicFixnum.new(0)
150
+ counter.increment
151
+ ```
152
+
153
+ Other useful types: `Concurrent::Array`/`Concurrent::Hash` (thread-safe wrappers), `Concurrent::Promises` (composable futures), `Concurrent::TimerTask`, `Concurrent::ThreadLocalVar`.
154
+
155
+ DON'T use raw `Concurrent::Future`/`Promise` without checking for rejection — a failed future returns `nil` from `value` and hides the error in `#reason`.
156
+
157
+ ## Thread-safety hazards & patterns
158
+
159
+ ### Shared mutable state
160
+
161
+ Any object mutated by multiple threads needs a lock or a thread-safe type. Plain `Hash`, `Array`, and `+=` are NOT atomic.
162
+
163
+ ```ruby
164
+ # WRONG: lost updates under contention
165
+ @total += n
166
+ @list << item
167
+ @hash[k] = v
168
+
169
+ # RIGHT: guard with a mutex, or use Concurrent::* types
170
+ @mutex.synchronize { @total += n }
171
+ ```
172
+
173
+ ### Check-then-act races
174
+
175
+ `if !exists then create` is two operations; another thread can act in between.
176
+
177
+ ```ruby
178
+ # WRONG (TOCTOU)
179
+ @conn = connect unless @conn
180
+
181
+ # RIGHT: atomic compute-if-absent, or lock the whole check+act
182
+ @mutex.synchronize { @conn ||= connect }
183
+ ```
184
+
185
+ ### Memoization races
186
+
187
+ `@x ||= compute` is fine for **idempotent, side-effect-free** values where a rare double-compute is harmless. It is NOT safe when `compute` has side effects or must run exactly once.
188
+
189
+ ```ruby
190
+ # Risky if compute is expensive/side-effectful: two threads may both run it
191
+ def config = @config ||= load_config
192
+
193
+ # Safe: compute exactly once
194
+ def config
195
+ @mutex.synchronize { @config ||= load_config }
196
+ end
197
+ ```
198
+
199
+ Pattern: **make objects immutable and share those**. Build state on one thread, `freeze` it, hand the frozen object to others. Frozen + no shared mutation = no locks needed.
200
+
201
+ ## Fibers & the Fiber scheduler
202
+
203
+ Fibers are cooperative, lightweight units of execution (no OS thread per fiber). You can have hundreds of thousands. They yield control explicitly.
204
+
205
+ ```ruby
206
+ f = Fiber.new { puts "a"; Fiber.yield; puts "b" }
207
+ f.resume # => "a"
208
+ f.resume # => "b"
209
+ ```
210
+
211
+ Since Ruby 3.0 a **Fiber scheduler** can make blocking IO automatically yield, so thousands of fibers multiplex over a few threads with normal-looking blocking code. You rarely implement the scheduler yourself — use the `async` gem.
212
+
213
+ ```ruby
214
+ Fiber.set_scheduler(MyScheduler.new) # what `Async{}` does for you
215
+ ```
216
+
217
+ ## The `async` gem — high-concurrency IO
218
+
219
+ Idiomatic high-level fiber concurrency. Code reads sequentially but runs concurrently; IO automatically suspends the fiber.
220
+
221
+ ```ruby
222
+ require "async"
223
+ require "async/http/internet"
224
+
225
+ Async do |task|
226
+ internet = Async::HTTP::Internet.new
227
+ results = urls.map do |u|
228
+ task.async { internet.get(u).read } # each runs concurrently
229
+ end.map(&:wait)
230
+ ensure
231
+ internet&.close
232
+ end
233
+ ```
234
+
235
+ Bound concurrency with a **Semaphore**; coordinate completion with a **Barrier**:
236
+
237
+ ```ruby
238
+ require "async/semaphore"
239
+ require "async/barrier"
240
+
241
+ Async do
242
+ barrier = Async::Barrier.new
243
+ semaphore = Async::Semaphore.new(10, parent: barrier) # max 10 in flight
244
+
245
+ urls.each { |u| semaphore.async { fetch(u) } }
246
+ barrier.wait # wait for all spawned tasks
247
+ end
248
+ ```
249
+
250
+ DON'T mix `async`-style fiber concurrency with blocking C-extensions that don't release the GVL or cooperate with the scheduler — they block the whole reactor. Use `async`-aware libraries (e.g. `async-http`, `async-postgres`).
251
+
252
+ ## Ractors — true parallelism on MRI (experimental)
253
+
254
+ Ractors run Ruby code **in parallel** (each has its own GVL) by forbidding shared mutable state. As of 3.2–3.4 they are still **experimental** (emit a warning) and many gems aren't Ractor-safe.
255
+
256
+ - Only **shareable** objects cross Ractor boundaries: immutable/frozen objects, `Integer`, `Symbol`, `true/false/nil`, deeply-frozen structures, classes/modules. Check with `Ractor.shareable?(obj)`.
257
+ - Communicate by **message passing**, not shared memory: `send`/`receive` (copy, mailbox) and `yield`/`take` (push/pull).
258
+
259
+ ```ruby
260
+ r = Ractor.new do
261
+ msg = Ractor.receive # block until a message arrives
262
+ msg * 2
263
+ end
264
+ r.send(21)
265
+ r.take # => 42 (the block's value)
266
+
267
+ # Parallel map across CPUs
268
+ ractors = inputs.map { |x| Ractor.new(x) { |v| heavy_cpu(v) } }
269
+ results = ractors.map(&:take)
270
+ ```
271
+
272
+ Limits to know: non-shareable globals/constants raise `IsolationError`; many stdlib/gems aren't Ractor-safe; debugging is harder; the warning is emitted on first use. Treat Ractors as promising for isolated CPU-bound fan-out, not as a drop-in thread replacement yet.
273
+
274
+ ## Processes & fork — real parallelism, the safe default for CPU work on MRI
275
+
276
+ Separate processes each have their own GVL, so they run in parallel. The cost is no shared memory (communicate via pipes/IPC) and OS process overhead.
277
+
278
+ ```ruby
279
+ pid = Process.fork do
280
+ # child: independent memory (copy-on-write of parent's pages)
281
+ result = heavy_cpu
282
+ # return value is NOT visible to parent — must use IPC
283
+ end
284
+ Process.wait(pid) # reap the child; avoid zombies
285
+ ```
286
+
287
+ Pass results back over an `IO.pipe` (or use a higher-level tool):
288
+
289
+ ```ruby
290
+ reader, writer = IO.pipe
291
+ pid = fork { reader.close; writer.write(Marshal.dump(compute)); writer.close }
292
+ writer.close
293
+ data = Marshal.load(reader.read); reader.close
294
+ Process.wait(pid)
295
+ ```
296
+
297
+ `fork` is **not available on Windows** and is fragile with threads (only the forking thread survives in the child) and with open connections/file handles — reconnect DBs/clients in the child.
298
+
299
+ ### `parallel` gem — fork made easy
300
+
301
+ ```ruby
302
+ require "parallel"
303
+ # CPU-bound: spread across cores with processes
304
+ Parallel.map(items, in_processes: 8) { |i| heavy_cpu(i) }
305
+ # IO-bound: lighter-weight threads
306
+ Parallel.map(items, in_threads: 16) { |i| fetch(i) }
307
+ ```
308
+
309
+ `in_processes` serializes args/results via Marshal across the fork boundary — objects must be Marshalable, and side effects in children don't propagate back.
310
+
311
+ ## Choosing a model
312
+
313
+ | Workload | Use |
314
+ |---|---|
315
+ | IO-bound, moderate concurrency | Threads + `SizedQueue`, or `Concurrent::FixedThreadPool` |
316
+ | IO-bound, very high concurrency (10k+ sockets) | Fibers via the `async` gem |
317
+ | CPU-bound on MRI | Processes (`fork` / `parallel` gem) or Ractors (if isolatable) |
318
+ | CPU-bound, want shared memory + parallelism | JRuby or TruffleRuby (no GVL) |
319
+
320
+ Rules of thumb:
321
+ - IO-bound -> **threads / async / fibers**.
322
+ - CPU-bound -> **processes / Ractors / JRuby / TruffleRuby**.
323
+ - Always **bound** concurrency (fixed pool, sized queue, semaphore). Unbounded `Thread.new` per item exhausts memory and OS threads.
324
+
325
+ Rails background jobs (ActiveJob/Sidekiq) are a separate, higher-level concern — see references/rails.md. Profiling concurrency for performance — see references/performance.md.
326
+
327
+ ## Timeouts — `Timeout` caveats
328
+
329
+ `Timeout.timeout` raises an exception in the target thread at an **arbitrary point**, which can interrupt the middle of a critical section, leave locks held, or corrupt state. Treat it as a last resort.
330
+
331
+ ```ruby
332
+ # RISKY: can fire mid-operation, leaving inconsistent state / undefined behavior
333
+ Timeout.timeout(5) { do_complex_thing }
334
+
335
+ # PREFER: native/library timeouts that abort cleanly at safe points
336
+ Net::HTTP.start(host, open_timeout: 5, read_timeout: 5) { ... }
337
+ db.connect(connect_timeout: 5)
338
+ socket.read_nonblock(...) # with IO.select for the deadline
339
+ ```
340
+
341
+ If you must use `Timeout`, keep the block tiny, avoid holding mutexes inside it, and never wrap operations that mutate shared state without cleanup.
342
+
343
+ ## Quick checklist
344
+
345
+ - MRI GVL: threads help **IO-bound**, never CPU-bound. CPU-bound -> processes/Ractors/JRuby/TruffleRuby.
346
+ - The GVL does **not** make code thread-safe; `+=`, `<<`, `Hash[]=` are not atomic.
347
+ - Always `join`/`value` threads (or push errors to a `Queue`) — unjoined exceptions vanish.
348
+ - Pass loop vars as block args to `Thread.new(x) { |x| }`; don't capture by closure.
349
+ - Prefer `Thread::Queue`/`SizedQueue` for producer-consumer; use `SizedQueue` for backpressure.
350
+ - Use `Concurrent::*` (FixedThreadPool, Future, Map, AtomicFixnum) over hand-rolled primitives.
351
+ - Guard check-then-act and side-effecting memoization with a `Mutex`/`Monitor`; `||=` only for idempotent values.
352
+ - Prefer immutable, `frozen` objects shared across threads to avoid locks entirely.
353
+ - Bound concurrency: fixed pools, sized queues, `Async::Semaphore`. Never unbounded `Thread.new`.
354
+ - High-concurrency IO -> `async` gem (`Async{}`, semaphores, barriers).
355
+ - Ractors are experimental: only shareable/frozen objects cross boundaries; communicate by message passing.
356
+ - `fork`: no Windows, fragile with threads/connections; reconnect clients in the child; reap with `Process.wait`.
357
+ - Avoid `Timeout.timeout` for stateful work — prefer library/socket-level timeouts.