rubino-agent 0.5.0 → 0.5.1

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 (250) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/.rubocop_todo.yml +1 -0
  4. data/CHANGELOG.md +317 -0
  5. data/README.md +56 -7
  6. data/Rakefile +17 -0
  7. data/docs/agents.md +40 -25
  8. data/docs/architecture.md +2 -9
  9. data/docs/commands.md +18 -6
  10. data/docs/configuration.md +154 -7
  11. data/docs/mcp.md +3 -3
  12. data/docs/memory.md +3 -3
  13. data/docs/security.md +1 -1
  14. data/docs/tools.md +45 -49
  15. data/ext/landlock/extconf.rb +78 -0
  16. data/ext/landlock/landlock.c +253 -0
  17. data/lib/rubino/agent/action_claim_guard.rb +61 -29
  18. data/lib/rubino/agent/definition.rb +3 -19
  19. data/lib/rubino/agent/iteration_budget.rb +1 -1
  20. data/lib/rubino/agent/loop.rb +188 -22
  21. data/lib/rubino/agent/prompts/build.txt +36 -5
  22. data/lib/rubino/agent/prompts/general.txt +8 -3
  23. data/lib/rubino/agent/runner.rb +179 -10
  24. data/lib/rubino/agent/tool_executor.rb +205 -20
  25. data/lib/rubino/agent/truncation_continuation.rb +7 -4
  26. data/lib/rubino/api/operations/approvals/decide_operation.rb +0 -4
  27. data/lib/rubino/api/operations/clarifications/decide_operation.rb +0 -4
  28. data/lib/rubino/api/operations/cron_jobs/create_operation.rb +0 -4
  29. data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +0 -4
  30. data/lib/rubino/api/operations/cron_jobs/list_operation.rb +0 -4
  31. data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +1 -5
  32. data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +1 -5
  33. data/lib/rubino/api/operations/cron_jobs/show_operation.rb +0 -4
  34. data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +0 -4
  35. data/lib/rubino/api/operations/cron_jobs/update_operation.rb +0 -4
  36. data/lib/rubino/api/operations/files/read_operation.rb +1 -5
  37. data/lib/rubino/api/operations/files/upload_operation.rb +0 -4
  38. data/lib/rubino/api/operations/health_operation.rb +1 -5
  39. data/lib/rubino/api/operations/memory/delete_operation.rb +0 -4
  40. data/lib/rubino/api/operations/memory/index_operation.rb +0 -4
  41. data/lib/rubino/api/operations/memory/stats_operation.rb +0 -4
  42. data/lib/rubino/api/operations/metrics_operation.rb +1 -1
  43. data/lib/rubino/api/operations/mode/show_operation.rb +0 -4
  44. data/lib/rubino/api/operations/mode/update_operation.rb +0 -4
  45. data/lib/rubino/api/operations/models/list_operation.rb +0 -4
  46. data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +0 -4
  47. data/lib/rubino/api/operations/oauth/connections/list_operation.rb +0 -4
  48. data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +0 -4
  49. data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +0 -4
  50. data/lib/rubino/api/operations/oauth/providers/list_operation.rb +0 -4
  51. data/lib/rubino/api/operations/runs/create_operation.rb +0 -4
  52. data/lib/rubino/api/operations/runs/events_operation.rb +0 -4
  53. data/lib/rubino/api/operations/runs/stop_operation.rb +0 -4
  54. data/lib/rubino/api/operations/sessions/create_operation.rb +0 -4
  55. data/lib/rubino/api/operations/sessions/delete_operation.rb +0 -4
  56. data/lib/rubino/api/operations/sessions/index_operation.rb +0 -4
  57. data/lib/rubino/api/operations/sessions/retry_operation.rb +0 -4
  58. data/lib/rubino/api/operations/sessions/show_operation.rb +0 -4
  59. data/lib/rubino/api/operations/sessions/undo_operation.rb +0 -4
  60. data/lib/rubino/api/operations/skills/list_operation.rb +0 -4
  61. data/lib/rubino/api/operations/skills/toggle_operation.rb +0 -4
  62. data/lib/rubino/api/operations/tasks/index_operation.rb +0 -4
  63. data/lib/rubino/api/operations/tasks/show_operation.rb +0 -4
  64. data/lib/rubino/api/operations/tasks/stop_operation.rb +0 -4
  65. data/lib/rubino/api/router.rb +2 -2
  66. data/lib/rubino/attachments/policy.rb +8 -0
  67. data/lib/rubino/attachments/preamble.rb +16 -8
  68. data/lib/rubino/cli/chat/completion_builder.rb +2 -2
  69. data/lib/rubino/cli/chat/session_resolver.rb +100 -30
  70. data/lib/rubino/cli/chat_command.rb +607 -113
  71. data/lib/rubino/cli/commands.rb +93 -1
  72. data/lib/rubino/cli/config_command.rb +54 -7
  73. data/lib/rubino/cli/doctor_command.rb +73 -20
  74. data/lib/rubino/cli/jobs_command.rb +38 -11
  75. data/lib/rubino/cli/memory_command.rb +29 -9
  76. data/lib/rubino/cli/onboarding_wizard.rb +6 -1
  77. data/lib/rubino/cli/server_command.rb +43 -1
  78. data/lib/rubino/cli/session_command.rb +129 -29
  79. data/lib/rubino/cli/setup_command.rb +166 -4
  80. data/lib/rubino/cli/skills_command.rb +21 -0
  81. data/lib/rubino/commands/built_ins.rb +2 -2
  82. data/lib/rubino/commands/executor.rb +16 -11
  83. data/lib/rubino/commands/handlers/agents.rb +199 -30
  84. data/lib/rubino/commands/handlers/config.rb +4 -0
  85. data/lib/rubino/commands/handlers/display.rb +50 -0
  86. data/lib/rubino/commands/handlers/help.rb +2 -9
  87. data/lib/rubino/commands/handlers/mcp.rb +7 -32
  88. data/lib/rubino/commands/handlers/memory.rb +10 -35
  89. data/lib/rubino/commands/handlers/sessions.rb +64 -50
  90. data/lib/rubino/commands/handlers/skills.rb +47 -28
  91. data/lib/rubino/commands/handlers/status.rb +56 -6
  92. data/lib/rubino/compression/compression_result.rb +35 -0
  93. data/lib/rubino/compression/compressor.rb +109 -0
  94. data/lib/rubino/compression/content_router.rb +240 -0
  95. data/lib/rubino/compression/diff_compressor.rb +252 -0
  96. data/lib/rubino/compression/javascript_code_skeleton.rb +15 -0
  97. data/lib/rubino/compression/json_compressor.rb +274 -0
  98. data/lib/rubino/compression/line_skeleton.rb +92 -0
  99. data/lib/rubino/compression/log_compressor.rb +299 -0
  100. data/lib/rubino/compression/python_code_skeleton.rb +122 -0
  101. data/lib/rubino/compression/ruby_code_skeleton.rb +80 -0
  102. data/lib/rubino/compression/tree_sitter_code_skeleton.rb +118 -0
  103. data/lib/rubino/compression/tsx_code_skeleton.rb +15 -0
  104. data/lib/rubino/compression/typescript_code_skeleton.rb +15 -0
  105. data/lib/rubino/config/configuration.rb +70 -86
  106. data/lib/rubino/config/defaults.rb +229 -8
  107. data/lib/rubino/config/loader.rb +9 -1
  108. data/lib/rubino/config/reasoning_prefs.rb +23 -0
  109. data/lib/rubino/config/validator.rb +50 -7
  110. data/lib/rubino/context/compressor.rb +1 -1
  111. data/lib/rubino/context/file_discovery.rb +0 -8
  112. data/lib/rubino/context/message_boundary.rb +2 -7
  113. data/lib/rubino/context/project_languages.rb +0 -7
  114. data/lib/rubino/context/prompt_assembler.rb +7 -2
  115. data/lib/rubino/context/summary_builder.rb +34 -25
  116. data/lib/rubino/context/token_budget.rb +3 -3
  117. data/lib/rubino/database/migrations/001_create_initial_schema.rb +1 -1
  118. data/lib/rubino/database/migrator.rb +0 -26
  119. data/lib/rubino/files/workspace.rb +2 -2
  120. data/lib/rubino/interaction/events.rb +0 -3
  121. data/lib/rubino/interaction/input_queue.rb +11 -0
  122. data/lib/rubino/interaction/lifecycle.rb +144 -25
  123. data/lib/rubino/interaction/polishing.rb +8 -0
  124. data/lib/rubino/interaction/probe.rb +1 -1
  125. data/lib/rubino/jobs/cron_job_repository.rb +0 -4
  126. data/lib/rubino/jobs/handlers/distill_skill_job.rb +3 -13
  127. data/lib/rubino/jobs/queue.rb +70 -5
  128. data/lib/rubino/jobs/worker.rb +1 -1
  129. data/lib/rubino/llm/adapter_factory.rb +1 -1
  130. data/lib/rubino/llm/auxiliary_client.rb +63 -3
  131. data/lib/rubino/llm/cache_breakpoint_middleware.rb +194 -0
  132. data/lib/rubino/llm/credential_check.rb +61 -4
  133. data/lib/rubino/llm/error_classifier.rb +142 -121
  134. data/lib/rubino/llm/fake_provider.rb +3 -3
  135. data/lib/rubino/llm/inline_think_filter.rb +34 -3
  136. data/lib/rubino/llm/reasoning_manager.rb +3 -26
  137. data/lib/rubino/llm/request.rb +0 -16
  138. data/lib/rubino/llm/ruby_llm_adapter.rb +233 -25
  139. data/lib/rubino/llm/scenario_loader.rb +10 -17
  140. data/lib/rubino/llm/scenarios/glued-table-prose.yml +36 -0
  141. data/lib/rubino/llm/scenarios/growing-table.yml +49 -0
  142. data/lib/rubino/llm/scenarios/narrow-terminal-table.yml +47 -0
  143. data/lib/rubino/llm/scenarios/streamed-table.yml +55 -0
  144. data/lib/rubino/llm/scenarios/table-then-prose.yml +34 -0
  145. data/lib/rubino/llm/scenarios/too-wide-table.yml +47 -0
  146. data/lib/rubino/llm/scenarios/wide-table.yml +1 -1
  147. data/lib/rubino/llm/thinking_support.rb +17 -12
  148. data/lib/rubino/llm/tool_bridge.rb +101 -37
  149. data/lib/rubino/mcp/manager.rb +53 -9
  150. data/lib/rubino/mcp/mcp_tool_wrapper.rb +24 -0
  151. data/lib/rubino/memory/backends/sqlite.rb +43 -35
  152. data/lib/rubino/memory/backends.rb +3 -3
  153. data/lib/rubino/memory/deduplicator.rb +22 -0
  154. data/lib/rubino/memory/flusher.rb +35 -1
  155. data/lib/rubino/memory/salience_gate.rb +26 -0
  156. data/lib/rubino/memory/sqlite_extraction_prompt.rb +5 -1
  157. data/lib/rubino/memory/store.rb +29 -29
  158. data/lib/rubino/memory/threat_scanner.rb +8 -0
  159. data/lib/rubino/memory.rb +47 -0
  160. data/lib/rubino/oauth/provider.rb +0 -5
  161. data/lib/rubino/run/event_store.rb +1 -6
  162. data/lib/rubino/run/repository.rb +0 -14
  163. data/lib/rubino/security/approval_policy.rb +116 -30
  164. data/lib/rubino/security/command_normalizer.rb +36 -0
  165. data/lib/rubino/security/dangerous_patterns.rb +17 -4
  166. data/lib/rubino/security/hardline_guard.rb +4 -3
  167. data/lib/rubino/security/readonly_commands.rb +299 -15
  168. data/lib/rubino/security/redactor.rb +272 -0
  169. data/lib/rubino/security/sandbox.rb +460 -0
  170. data/lib/rubino/security/secret_detector.rb +110 -0
  171. data/lib/rubino/security/secret_path.rb +136 -7
  172. data/lib/rubino/session/lock.rb +91 -0
  173. data/lib/rubino/session/message.rb +38 -3
  174. data/lib/rubino/session/picker.rb +95 -0
  175. data/lib/rubino/session/repository.rb +57 -40
  176. data/lib/rubino/session/store.rb +0 -11
  177. data/lib/rubino/skills/registry.rb +14 -5
  178. data/lib/rubino/skills/skill.rb +31 -10
  179. data/lib/rubino/skills/skill_tool.rb +3 -18
  180. data/lib/rubino/skills/state_repository.rb +0 -4
  181. data/lib/rubino/tools/background_tasks.rb +179 -40
  182. data/lib/rubino/tools/base.rb +87 -73
  183. data/lib/rubino/tools/edit_tool.rb +50 -20
  184. data/lib/rubino/tools/fuzzy_match.rb +212 -0
  185. data/lib/rubino/tools/glob_tool.rb +5 -1
  186. data/lib/rubino/tools/grep_tool.rb +17 -51
  187. data/lib/rubino/tools/multi_edit_tool.rb +32 -19
  188. data/lib/rubino/tools/patch_tool.rb +51 -10
  189. data/lib/rubino/tools/probe_tool.rb +0 -20
  190. data/lib/rubino/tools/question_tool.rb +54 -2
  191. data/lib/rubino/tools/read_attachment_tool.rb +21 -11
  192. data/lib/rubino/tools/read_tool.rb +131 -25
  193. data/lib/rubino/tools/read_tracker.rb +36 -0
  194. data/lib/rubino/tools/registry.rb +63 -44
  195. data/lib/rubino/tools/result.rb +43 -12
  196. data/lib/rubino/tools/retrieve_output_tool.rb +70 -0
  197. data/lib/rubino/tools/ruby_tool.rb +0 -0
  198. data/lib/rubino/tools/shell_kill_tool.rb +6 -2
  199. data/lib/rubino/tools/shell_output_tool.rb +7 -1
  200. data/lib/rubino/tools/shell_registry.rb +169 -15
  201. data/lib/rubino/tools/shell_tail_tool.rb +6 -1
  202. data/lib/rubino/tools/shell_tool.rb +483 -53
  203. data/lib/rubino/tools/steer_tool.rb +2 -21
  204. data/lib/rubino/tools/subagent_probe.rb +1 -1
  205. data/lib/rubino/tools/summarize_file_tool.rb +6 -0
  206. data/lib/rubino/tools/task_result_tool.rb +8 -2
  207. data/lib/rubino/tools/task_stop_tool.rb +5 -6
  208. data/lib/rubino/tools/task_tool.rb +200 -103
  209. data/lib/rubino/tools/vision_tool.rb +32 -4
  210. data/lib/rubino/tools/webfetch_tool.rb +145 -0
  211. data/lib/rubino/tools/write_tool.rb +1 -1
  212. data/lib/rubino/ui/agent_menu.rb +179 -0
  213. data/lib/rubino/ui/api.rb +2 -2
  214. data/lib/rubino/ui/base.rb +2 -2
  215. data/lib/rubino/ui/bottom_composer.rb +1112 -140
  216. data/lib/rubino/ui/cli.rb +898 -262
  217. data/lib/rubino/ui/completion_menu.rb +24 -43
  218. data/lib/rubino/ui/composer/input_line.rb +131 -0
  219. data/lib/rubino/ui/composer/subagent_panel.rb +35 -0
  220. data/lib/rubino/ui/headless_trace.rb +1 -1
  221. data/lib/rubino/ui/input_history.rb +90 -5
  222. data/lib/rubino/ui/live_region.rb +12 -0
  223. data/lib/rubino/ui/markdown_renderer.rb +103 -41
  224. data/lib/rubino/ui/menu_view.rb +117 -0
  225. data/lib/rubino/ui/null.rb +1 -1
  226. data/lib/rubino/ui/paste_store.rb +33 -1
  227. data/lib/rubino/ui/printer_base.rb +135 -8
  228. data/lib/rubino/ui/streaming_markdown.rb +89 -0
  229. data/lib/rubino/ui/subagent_cards.rb +126 -25
  230. data/lib/rubino/util/atomic_file.rb +12 -0
  231. data/lib/rubino/util/duration.rb +8 -5
  232. data/lib/rubino/util/output.rb +55 -10
  233. data/lib/rubino/version.rb +7 -1
  234. data/lib/rubino/workspace.rb +65 -2
  235. data/lib/rubino.rb +29 -22
  236. data/rubino-agent.gemspec +27 -1
  237. metadata +78 -20
  238. data/docs/plugins.md +0 -195
  239. data/lib/rubino/interaction/state.rb +0 -56
  240. data/lib/rubino/memory/backends/default.rb +0 -101
  241. data/lib/rubino/memory/extractor.rb +0 -85
  242. data/lib/rubino/memory/retriever.rb +0 -50
  243. data/lib/rubino/plugins/registry.rb +0 -75
  244. data/lib/rubino/plugins.rb +0 -86
  245. data/lib/rubino/tools/answer_child_tool.rb +0 -83
  246. data/lib/rubino/tools/ask_parent_tool.rb +0 -232
  247. data/lib/rubino/tools/git_tool.rb +0 -71
  248. data/lib/rubino/tools/github_tool.rb +0 -233
  249. data/lib/rubino/tools/test_tool.rb +0 -454
  250. data/lib/rubino/ui/subagent_view.rb +0 -280
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a3a48a6f7deb104446c624354a271ff1a1671c9b3ea846cee1144b3f55538db
4
- data.tar.gz: cbc407cd78db827d75f150f61045b2cce759b266816e32fdba874c30eb3647c4
3
+ metadata.gz: c1debe685b923c625e0dc4dcf95da3c9fc12fcd6c73bdff71e35164279e62b06
4
+ data.tar.gz: 5451e122fc13bfdd4ffeba0e680cad9fb6b976dfabe8f9dcfd894e5215ac9688
5
5
  SHA512:
6
- metadata.gz: 3342a4c8b1856691788ac9625b81eb4058870a0eb5a75946e99e6570ce40a05a7b44475a798863ac0fc92c9fcb505cf6caf27fcedc10a8e1bd9653ba18edf440
7
- data.tar.gz: e54077b30f385942dd3591fb7e6c4e7afc0f301e5fea9d3111732c91ce56e63a4c7d192704ebd74ea97ee4a04437784d73a8d398bf02ce6143cdd69b2227a841
6
+ metadata.gz: bf657914d128053ffa39d7911a5c2c12e491ff45b1907e8bc78a694b8f8d7540a1e8d1182ee5831670af17e5e64b16148202d987ebc9e2c75604a88f78148d36
7
+ data.tar.gz: eefe6fbbcd977bff1cf8b7a189fdaf73daee9ca6b12ca55b82876a99c55ede529dc78ba3f5146f8b34a7e6e6b6b38dab454c9d6cdcf3baca8e754187f49c6829
data/.rubocop.yml CHANGED
@@ -27,6 +27,12 @@ AllCops:
27
27
  # Test fixtures are sample input documents (e.g. a .rb code sample for the
28
28
  # plain-text converter), not project source -- they must not be linted.
29
29
  - "spec/fixtures/**/*"
30
+ # Eval-harness fixtures are deliberately tiny/imperfect sample projects the
31
+ # agent edits at eval time (INPUT, not source); results/ is generated output.
32
+ # The eval/.rubocop.yml excludes these for an in-eval run; mirror it here so
33
+ # the whole-repo lint from the root is clean too.
34
+ - "eval/fixtures/**/*"
35
+ - "eval/results/**/*"
30
36
 
31
37
  # --- House style: strings ----------------------------------------------------
32
38
 
data/.rubocop_todo.yml CHANGED
@@ -538,6 +538,7 @@ RSpec/DescribeClass:
538
538
  - 'spec/rubino/skills/skills_spec.rb'
539
539
  - 'spec/rubino/tools/edit_read_gate_spec.rb'
540
540
  - 'spec/rubino/tools/shell_background_spec.rb'
541
+ - 'spec/rubino/tools/shell_background_completion_spec.rb'
541
542
  - 'spec/rubino/tools/shell_input_spec.rb'
542
543
  - 'spec/rubino/tools/tool_fixes_spec.rb'
543
544
  - 'spec/rubino/ui/bottom_composer_approval_handoff_pty_spec.rb'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,322 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.1] - 2026-06-25
4
+
5
+ ### Added
6
+
7
+ - **Tool-output compression (deterministic, off by default).** A no-LLM content
8
+ router at the single `Agent::ToolExecutor` seam compresses high-volume tool
9
+ output before it reaches the model: test/build/lint logs are reduced to their
10
+ failures + summary (≈97% fewer tokens on a failing suite, every failure kept),
11
+ and a whole-file source read can be returned as a skeleton (signatures kept,
12
+ large bodies elided behind a `read offset:/limit:` pointer). Diffs, grep/search
13
+ results, JSON, and short output pass through **byte-identical**. Reversibility
14
+ reuses the existing spill: the full original is written to
15
+ `tool-results/<call_id>.txt` and the compressed output points the model there —
16
+ no separate store/tool. When enabled, `read` and `shell` expose a `compress`
17
+ parameter (default true) so the model can opt a single call out and get the
18
+ verbatim output. Master switch `tool_output_compression.enabled` (default
19
+ `false`); `rubino setup` offers to turn it on. See
20
+ [configuration.md](docs/configuration.md#tool_output_compression).
21
+ - **Multi-language code compression.** The whole-file source-skeleton compressor
22
+ now covers more than Ruby. `tool_output_compression.code.languages` (default
23
+ `["ruby"]`) selects which languages get skeletonised: Ruby (built-in Prism
24
+ parser), Python (stdlib `ast` via your `python3` — a no-op if `python3` isn't
25
+ on PATH), and JavaScript / TypeScript / TSX (via the optional
26
+ `tree_sitter_language_pack` gem — a no-op until it's installed). A read in an
27
+ unlisted language passes through verbatim. `rubino setup` adds a language
28
+ picker and, if you choose JS/TS, offers to install the parser gem.
29
+ - **Agent-attach view.** At the idle prompt, `↓` opens the subagent picker and
30
+ `Enter` now **attaches** to the highlighted background subagent: the screen
31
+ switches to that agent's OWN full timeline (its tool calls and what it said,
32
+ replayed from its session) and the input prompt becomes scoped — `sa_xxxx ❯`.
33
+ While attached, typed text steers the running child (or answers it when it's
34
+ blocked on you); `←` on the empty prompt (or the picker's `◂ main` row) returns
35
+ to the main timeline, and the picker doubles as a switcher between agents. This
36
+ replaces the bounded registry snapshot the picker's Enter used to show with the
37
+ agent's real conversation, and makes the global `/agents <id> steer/probe` and
38
+ `/reply <id>` forms redundant while attached. The attached view **live-tails**
39
+ the child's stream (tool rows and streaming prose) exactly like the main agent
40
+ instead of freezing on a snapshot, and `/back` / `/detach` return to the main
41
+ agent regardless of composer-draft state (#82, #85, #87).
42
+ - **`api.allow_public_bind` gate.** Because the API server can execute shell
43
+ tools, binding it to a non-loopback address (`--host 0.0.0.0`,
44
+ `RUBINO_API_HOST`) now **refuses to boot** unless `api.allow_public_bind: true`
45
+ is set in `config.yml`; when opted in, the server prints a one-time exposure
46
+ warning. Loopback binds are unaffected (#577).
47
+ - **MCP tool transparency + parallel startup.** An MCP tool's display label now
48
+ carries its source — the live tool card and the approval card both show
49
+ `<bare> (mcp:<server>)`, so you can tell at a glance that an out-of-process
50
+ server is running (the model-facing tool name is unchanged) (#582). MCP
51
+ servers also now connect **in parallel** at boot, so one hanging server no
52
+ longer serializes startup (#576).
53
+ - **Read-only meta-commands run immediately while a turn is active.** A small
54
+ set of non-mutating slash commands (`/agents`, `/tasks`, `/stop`, `/status`,
55
+ `/jobs`, `/help`, `/commands`, `/dirs`) now execute **immediately** mid-turn
56
+ instead of queuing — so you can drill into a sub-agent, stop the run, or check
57
+ status without interrupting. State-mutating commands (`/model`, `/clear`,
58
+ `/new`, `/config`, `/mode`, …) show a transient `⚠ <cmd> is not available
59
+ during an active turn — press Esc to interrupt first` notice; plain text still
60
+ queues, and `Esc` interrupts.
61
+ - **Interactive CLI session picker.** A bare `rubino sessions` on a TTY opens an
62
+ interactive picker (id, title, message count, dir, age; arrow-key highlight,
63
+ type-to-filter, `Esc` cancels) and `Enter` resumes the chosen session. On a
64
+ pipe / non-TTY it prints a script-safe list; `sessions list` stays list-only.
65
+ The picker is cwd-scoped by default; `--all` unscopes it.
66
+ - **`/sessions rename <id|title> <new title>`.** Rename a session from the REPL
67
+ (#45).
68
+ - **Aux-LLM session titles.** When `auxiliary.title` names a concrete backend,
69
+ new sessions get an LLM-generated, length-capped summary title; the
70
+ deterministic derivation stays the default and the fallback (#45).
71
+ - **Streaming GFM table rendering (#89).** A markdown table now renders as a
72
+ live, correctly-fitted table as it streams — a sliding window of recent rows
73
+ grows in place — instead of leaking raw `| col | col |` pipes that only snap
74
+ into a table once the message completes.
75
+
76
+ ### Changed
77
+
78
+ - **Provider auto-routing.** With `model.provider: "auto"` (the default), the
79
+ concrete provider is derived from the model id (`openai/*` → OpenAI); the
80
+ setup wizard / auto-detect write an explicit provider when a non-OpenAI
81
+ backend is chosen.
82
+ - **Credential check uses provider-specific env vars.** The credential check
83
+ and key resolution now read the env var for the configured provider
84
+ (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `BEDROCK_API_KEY`,
85
+ `MINIMAX_API_KEY`, and `<PROVIDER>_API_KEY` for anything else, e.g.
86
+ `DEEPSEEK_API_KEY`). A non-OpenAI provider no longer silently falls back to
87
+ `OPENAI_API_KEY` (only providers explicitly marked `openai_compatible` /
88
+ `anthropic_compatible` fall back to `OPENAI_API_KEY` / `ANTHROPIC_API_KEY`).
89
+ - **`security.confirm_policy` default is `dangerous_only`.** Safe shell commands
90
+ run unprompted; only commands matching a dangerous pattern prompt. Set
91
+ `confirm_policy: confirm_all` to restore prompt-on-everything. The
92
+ non-bypassable hardline floor and `permissions: deny` always run first
93
+ regardless of policy.
94
+ - **Removed the built-in `run_tests` and `github` tools.** Running tests and
95
+ GitHub/git operations now go through the generic `shell` tool (with its
96
+ hardened git arg parsing), matching the field norm and shrinking the tool
97
+ surface.
98
+ - **Blocked-tool results are now typed errors.** When a tool call is blocked
99
+ (denied by approval, sandbox, or policy), its result is returned to the model
100
+ as a typed error with explicit anti-confabulation wording, so the model is told
101
+ the action did NOT happen instead of being free to assume success (#583).
102
+ - **Single status bar during a turn.** The animated facet activity row is folded
103
+ into the model/ctx footer (one bar, not two); the "esc to interrupt" hint shows
104
+ exactly once, and a mid-stream **waiting indicator** resurfaces beneath the
105
+ in-flight tail after a short window of model/transport silence and drops away
106
+ the instant tokens resume (#21, #56b — `/status` now also shows the workspace
107
+ cwd line).
108
+ - **FIFO approval queue for concurrent subagents.** When multiple subagents need
109
+ approval at once, one modal shows at a time with an "(N more queued)"
110
+ indicator that dequeues on resolve, and async-completion notices no longer
111
+ print over an active modal. Subagent approvals also **escalate to the parent's
112
+ approval card at any nesting depth**, so a nested child no longer fail-closes
113
+ with a noninteractive block (#86).
114
+ - **Slash commands dispatch while attached to a subagent** (`/stop <id>`,
115
+ `/agents`, `/status`, …) instead of being steered into the child as text;
116
+ `/skills list` / `/skills ls` show the skills list rather than trying to
117
+ activate a skill named `list`; `/think off` hides the reasoning aside for
118
+ always-thinking models unless an explicit `/reasoning` is set; `/config <key>`
119
+ resolves the short labels `/status` advertises (`reasoning`, `effort`,
120
+ `think`) (#62, #66, #87).
121
+ - **`/new` returns instantly.** The end-of-session memory flush is enqueued as a
122
+ background job instead of running a synchronous aux-LLM extract, so starting a
123
+ new session no longer freezes the prompt for 2–3s.
124
+ - **Headless one-shot drains only its own jobs.** `rubino -q` now emits and
125
+ flushes the JSON result envelope before draining, and scopes the post-turn job
126
+ drain to the run's own session, so a one-shot returns immediately even with a
127
+ background job backlog.
128
+ - **Subagent cards are distinguishable + carry the task id.** Concurrent
129
+ subagent cards label by a dimension drawn from the task prompt (rather than the
130
+ bare agent type), background "done" markers carry the task id, and the live
131
+ elapsed counter shows seconds (`1m05s`) so it visibly advances (#44, #570).
132
+ - **Pastes coalesce into a single placeholder**, input history is recalled and
133
+ persisted, `Enter` accepts the highlighted dropdown candidate, and
134
+ `task_result` running-polls no longer flood the transcript (#524, #525).
135
+ - **System-prompt grounding for control + tools.** The cap / continuation /
136
+ summary control is framed as trusted `[harness control]` so MiniMax-M3 stops
137
+ treating it as prompt-injection (#75); the background-shell lifecycle is primed
138
+ so the model uses `shell_output` / `shell_kill` correctly; the verification
139
+ step is scoped to never modify the environment and to stop honestly.
140
+ - **Memory-flush best-effort boundary** made airtight (#471), so a failure
141
+ flushing memory at shutdown can't take down the run.
142
+
143
+ ### Removed
144
+
145
+ - **Child→parent `ask_parent` / `answer_child` tools.** Subagents are
146
+ non-blocking background workers and can no longer pause mid-task to ask their
147
+ parent (or the human) a question; instead they make sensible default calls and
148
+ surface open decisions in their result. The two model-facing tools that
149
+ implemented that channel — `ask_parent` (the child→parent escalation) and
150
+ `answer_child` (the parent's reply) — are gone. The parent→child `steer` /
151
+ `probe` tools and the human approval gate (`/reply` for a child parked on an
152
+ approval) are unchanged. `tasks.ask_parent_timeout` is now vestigial.
153
+ - **`streaming.cursor` config key.** It was dead config (assigned, never read)
154
+ and is no longer accepted — remove it from any `config.yml`.
155
+ - **`security.require_confirmation_for_shell` config key.** Replaced by
156
+ `security.confirm_policy` (`dangerous_only` | `confirm_all`); the old key is no
157
+ longer honored.
158
+
159
+ ### Security
160
+
161
+ - **Hermes-style secret handling (#506).** Adopts the Hermes secret model across
162
+ the agent: the structured `read` tool blocks `.env` and credential files
163
+ outright, and secret **values** are redacted in the output of `read`, `grep`,
164
+ `shell` (including the live stream seam, not just the final buffer, #507),
165
+ `summarize`, and `read_attachment` (#511/#512). A `security.redact_secrets`
166
+ toggle (default **on**) controls redaction. The earlier per-read secret-file
167
+ approval gate was removed in favour of this block-list + redaction model
168
+ (#480). Over-broad redaction was then narrowed: the `ENV_ASSIGN` pattern is
169
+ anchored so `AUTHORS` / `SECRETARY` pass through while `API_KEY` / `AUTH_TOKEN`
170
+ still redact, the Telegram-token pattern is pinned to its canonical shape, and
171
+ fully-masked secrets carry an explicit marker rather than a bare `***`
172
+ (#67, #516).
173
+ - **Secrets are no longer persisted to memory (#99).** A `Security::SecretDetector`
174
+ is wired into the memory write path (it refuses an explicit save and the
175
+ auto-extract persist path) and into the redactor, catching prefixed key
176
+ shapes, prefix-less AWS secret keys, and a high-entropy heuristic — previously
177
+ an `sk-proj-…` key could be saved verbatim and re-injected into every future
178
+ system prompt.
179
+ - **Removed the dedicated `git` tool (RCE bypass).** Git now runs through the
180
+ hardened `shell` with strict arg parsing that rejects exec vectors
181
+ (`--ext-diff`, `-c`, textconv, …) plus a `GIT_HARDENED_ENV`, instead of a tool
182
+ that could be steered into arbitrary command execution (#536/#553).
183
+ - **Dangerous write/exec flag-forms prompt under the default gate (#61).**
184
+ `git -c` / `--output`, `sed -i`, `sort -o`, `find -delete` / `-exec`,
185
+ `tar --to-command`, `tee`, interpreter `-c` / `-e` / `--eval`, etc. no longer
186
+ auto-run under `dangerous_only`, while bare interpreters and read-only forms
187
+ still auto-run. A shared `Security::CommandNormalizer` also closes
188
+ line-continuation evasion (e.g. `rm -r\<newline>f` no longer slips past the
189
+ danger/approval layer).
190
+ - **Extended HOME credential read-block.** Reading credential stores under HOME
191
+ is blocked and a base64-decode-pipe-to-shell (`echo … | base64 -d | sh`) is
192
+ flagged dangerous (#519); the denylist now covers `.ssh`, `.aws`, `.netrc`,
193
+ `.git-credentials`, `.kube`, `.docker`, `.gnupg`, `.azure`, and `.config/gh`
194
+ (#537). A write through a **dangling in-workspace symlink** can no longer
195
+ escape the sandbox — the link target is resolved before the create-new-file
196
+ fallback (#62).
197
+ - **Tighten the `ruby_llm` floor to `>= 1.16` (#508).** The adapter wires native
198
+ providers through ruby_llm's generic `<provider>_api_base=` setters
199
+ (deepseek/mistral/etc., #482), which only exist from ruby_llm 1.16.0. The
200
+ gemspec previously allowed `~> 1.0`, so a fresh `gem install` could resolve
201
+ ruby_llm 1.15 and crash at runtime with `NoMethodError`. The dependency is now
202
+ `>= 1.16, < 2.0`.
203
+ - **Secret masking on `config set`.** `rubino config set` now masks the echoed
204
+ value when the key looks secret (`api_key`, `token`, `password`, `secret`,
205
+ `authorization`, …) and when the value itself contains inline credentials
206
+ (`key=value`, `Bearer …`, URL userinfo, `curl -u`, `mysql -p…`), so keys are
207
+ not printed in the clear to the terminal/scrollback.
208
+ - **Sanitized untrusted text rendered to the terminal (CWE-150).** Text that
209
+ originates from the model, tools, or filenames (subagent cards, `/`-palette and
210
+ `@`-picker menu labels, and the remaining CLI aside sinks — probe, reasoning,
211
+ open-fence, branch title) is now defanged of ANSI/OSC escape sequences before
212
+ it is written, closing an escape-injection class (#563/#564/#565–#568).
213
+ - **Vision egress hardening.** The `vision` tool now honours
214
+ `attachments.policy.aux_vision_egress` (default `true`): set it to `false` and
215
+ the tool refuses to send an image to an external auxiliary model, returning a
216
+ clean error instead of egressing the bytes (#578). Before any egress it also
217
+ **content-sniffs** the file (magic bytes win over the extension, fail-closed),
218
+ so a mislabelled or non-image file can't be smuggled to the external host
219
+ (#579).
220
+ - **OS sandbox covers more executors.** The OS write-jail (Landlock / Seatbelt)
221
+ now also confines background shells, `ruby`, and `run_tests`, with relaxation
222
+ gated on verified enforcement; a write-jail `EACCES` outside the workspace
223
+ produces an attributable "blocked by write-jail" hint (#74).
224
+
225
+ ### Fixed
226
+
227
+ - **MiniMax-M3 pre-tool-call "freeze".** Thinking/reasoning now defaults ON for
228
+ every provider (it was deliberately off for MiniMax-family ids). On the
229
+ anthropic-compatible path rubino now sends `thinking: {type: enabled,
230
+ budget_tokens: …}` and streams the model's reasoning deltas — so the multi-
231
+ second window where M3 reasons toward a tool-call is filled with visible
232
+ streamed reasoning instead of dead air (the symptom that read as the agent
233
+ "freezing" when it spawned subagents). Matches the reference agent's default
234
+ `reasoning_effort: medium`. A backend that rejects the budget is caught and
235
+ retried once without it (#75), so default-on is safe; set
236
+ `providers.<name>.supports_thinking: false` to opt out.
237
+ - **MCP `degraded` server state.** `/mcp` and `rubino doctor` now distinguish a
238
+ reachable server (`●`) from a **degraded** one (`⚠` — the process is alive but
239
+ a protocol call such as `tools/list` failed), instead of reporting it as plain
240
+ reachable (#575).
241
+ - **Session-title length cap.** A renamed session title is now length-capped at
242
+ rename and truncated on render, so an over-long title can't disrupt status /
243
+ session-list layout (#581).
244
+ - **Streaming fidelity.** A streaming turn no longer re-executes or re-surfaces
245
+ tool calls it already ran (no double "started" line or duplicate final tool)
246
+ (#53), and a split think/fence sentinel is held across the message-boundary
247
+ flush so reasoning no longer leaks into the body and prose isn't torn apart
248
+ (#43/#54). A committed markdown table glued to trailing prose no longer leaks
249
+ raw pipes, and a too-wide table fits the pane instead of tearing the border.
250
+ - **Subagent / multiplexer UI.** A running `blocked_on_parent` sub stays visible
251
+ in the footer while listed; cap-rejected delegation renders a neutral
252
+ "at capacity" row instead of a phantom failed card; the close-row / replay use
253
+ the per-call subagent name instead of a shared stale one (#35); the agent
254
+ picker opens reliably on `↓` and `←`/`↑` backs out; picking `◂ main` returns to
255
+ main immediately mid-turn; a nested child's menu no longer crashes it; and the
256
+ parent autonomously resumes at idle when background subagents finish while
257
+ detached (#37, #44, #51, #561).
258
+ - **Interrupt handling.** `Esc` at the tool-dispatch boundary raises a clean
259
+ interrupt instead of a malformed continuation that the backend rejects as
260
+ "invalid params"; a stray `Ctrl-C` exits cleanly (130) with no raw `net/http`
261
+ backtrace; and a background thread never dumps a backtrace on death.
262
+ - **Input papercuts.** Backspace (`DEL 0x7f`) deletes instead of inserting a
263
+ space (#522); a single `Ctrl-D` at an idle empty composer no longer hangs, and
264
+ fast input bursts coalesce their redraws (#520). Several composer
265
+ render/input races and resize-while-typing reflows that duplicated the
266
+ in-progress input into the scrollback are fixed, including chained resizes and
267
+ the resize REPAINT path (#481/#485/#486/#499/#500/#501/#503).
268
+ - **`edit` no longer crashes on non-UTF-8 / binary buffers.** Fuzzy-match
269
+ normalization passes invalid-encoding bytes through verbatim (#47), atomic
270
+ writes are binmode'd so binary buffers never transcode (the intermittent
271
+ in-session edit crash on accented files) (#65), and `clean_slice` reinterprets
272
+ binary as UTF-8 rather than calling `.encode` (#58). A failed edit / read /
273
+ write now shows `✗` instead of a green `✓`.
274
+ - **Background jobs and shells.** The job queue drains reliably — stale `running`
275
+ rows are reclaimed after the lease expires (#76) and `ExtractMemoryJob` is
276
+ prioritized over `SummarizeSessionJob` so save→recall doesn't lag (#79);
277
+ finished background shells are retired with their buffer and exit status
278
+ retained, so `shell_output` / `shell_tail` / `shell_kill` stay reachable next
279
+ turn (#78); shell cancel no longer orphans the child process group, and a
280
+ finished background shell auto-wakes the model.
281
+ - **Turn-ledger honesty.** Blocked / errored tools no longer count toward the
282
+ "N tools ran / M edits" ledger, so a turn whose only tool was refused stops
283
+ telling you to review nonexistent changes; the force-summary and closing-summary
284
+ nudges are grounded in the truthful turn ledger so the model can't confabulate
285
+ having done nothing (#36/#84). MiniMax HTTP 429 / quota errors are categorized
286
+ as retryable rate-limit (honouring `Retry-After`) instead of "Invalid request",
287
+ and the anti-confabulation note no longer over-fires on accurate local caveats.
288
+ - **Sessions / resume / doctor.** A per-session `flock` guard stops a concurrent
289
+ `--continue` from forking a moving transcript (#543), replay renders only the
290
+ new tail of a restated final message (#542), `--resume <id>` is validated
291
+ before the boot banner (#521), and `doctor` warns instead of false-green when
292
+ no usable credential exists and no longer implies an unverified key is
293
+ validated (#541/#546).
294
+ - **Non-native provider wiring (#482).** Fixed the preflight that falsely
295
+ reported non-native providers (deepseek/mistral/…) as ready; they are now
296
+ wired through the generic `<provider>_api_base=` setters and the run stops
297
+ on an unreachable endpoint instead of failing later. Transient name-resolution
298
+ failures (`EAI_AGAIN`) are retried rather than fatal, and a stream that ends
299
+ without a finish signal is recovered instead of failing the turn.
300
+ - **Parent-death reaps child shells (#478).** When the agent process dies, the
301
+ long-running child shells it spawned are reaped instead of being orphaned,
302
+ using a trap-safe SIGTERM/SIGHUP handler (no `Mutex` inside the signal trap).
303
+ - **Compaction no-op loop (#484).** Stopped a busy-loop on an over-budget
304
+ session that has too few messages to compact. The `doom_loop.threshold`
305
+ default is also no longer rejected by its own validator (#60).
306
+ - **Memory polish indicator no longer flashes every turn (#59).** The polish
307
+ worker starts only when a row was actually enqueued, the indicator composes
308
+ alongside the ctx bar instead of replacing it, and a verbatim repeat
309
+ short-circuits to the existing row at the write seam.
310
+ - **`/exit` and exit codes.** `/exit` routes through the quit-guard, and an
311
+ interactive session exits non-zero on an auth/credential error (#154).
312
+ - **CLI DX papercuts.** Fixed the bare-`rubino "prompt"` one-shot path, help-
313
+ session clutter, a bare-prompt did-you-mean edge case, and a `read_attachment`
314
+ hint that suggested markitdown for raster images instead of OCR.
315
+ - **Input hardening.** Fixed a raw SQLite3 exception on session input with
316
+ hostile/NUL bytes (#498) and cleaned up `Errno` error messages on the failure
317
+ paths; tightened mcp args validation and assorted low-severity
318
+ config/sessions/resume/CLI papercuts.
319
+
3
320
  ## [0.5.0] - 2026-06-15
4
321
 
5
322
  ### Added
data/README.md CHANGED
@@ -5,12 +5,62 @@ A coding & automation **agent** — small, self-contained, and built to run *whe
5
5
  ## Why rubino
6
6
 
7
7
  - **Runs where the work is** — a single gem on the machine (or VM) that holds the code, not a remote service you pipe files to.
8
- - **Persistent memory** — a tiny SQLite "Zep"-style fact store that learns about you and the project across sessions.
8
+ - **Persistent memory** — a tiny SQLite fact store that learns about you and the project across sessions.
9
9
  - **Context compaction** — automatic compression with session lineage when the conversation outgrows the window.
10
10
  - **CLI *and* HTTP API** — an interactive terminal session for humans, a bearer-protected JSON + SSE API for programs.
11
- - **Real tools, gated** — read/write/edit, shell, ruby, git/github, grep/glob, a structured test runner, vision, and more, behind an approval model with a non-bypassable hardline floor.
11
+ - **Real tools, gated** — read/write/edit, shell, ruby, grep/glob, apply_patch, vision, and more (git, GitHub, and tests run through the hardened shell), behind an approval model with a non-bypassable hardline floor.
12
12
  - **Built on ruby_llm** — provider-agnostic: MiniMax, OpenAI, Anthropic, Gemini, or an OpenAI-compatible gateway.
13
13
 
14
+ ## Cache-friendly compaction (measured)
15
+
16
+ A long agent session only stays cheap if the cached prompt prefix survives
17
+ compaction. rubino is built so that when the conversation is compressed into a
18
+ summary, the summary lands *after* the cached head (system + tools + stable
19
+ history) — so the provider's prompt cache keeps **hitting** the head instead of
20
+ re-encoding it cold every time the session is compacted.
21
+
22
+ Measured with the model held fixed (local oMLX `Qwen3.6-35B-A3B`,
23
+ Anthropic-style `cache_control`) on a 25-turn coding session that triggers
24
+ compaction **9 times**:
25
+
26
+ | metric | rubino |
27
+ |---|---|
28
+ | cached prefix retained right after each compaction | **44–94%** (survives — never resets to 0) |
29
+ | cumulative cache-read over the whole session | **88%** |
30
+ | prefix byte-stability across turns | **0.95** |
31
+ | task solved through all 9 compactions | **10/10** hidden tests, 0 wasted work |
32
+
33
+ Holding the model fixed isolates the **engine** — any difference is the
34
+ scaffolding (prompt assembly, where the compaction summary is placed, cache
35
+ breakpoints), not the model. This is a single model and a single scenario:
36
+ indicative of the design, not a leaderboard. The harness lives in a separate
37
+ benchmark project.
38
+
39
+ ## Tool-output compression (measured)
40
+
41
+ Test logs, diffs and large command dumps are mostly noise. rubino can route
42
+ each tool output through a **deterministic (no-ML)** compressor that keeps the
43
+ signal and drops the rest — opt-in (`tool_output_compression`), with a
44
+ byte-identical passthrough for anything already small and a `retrieve_output`
45
+ pointer back to the full text. Token-honest: counts are the **exact**
46
+ `prompt_tokens` reported by the server (local oMLX `Qwen3.6-35B-A3B`), not
47
+ chars/4 estimates.
48
+
49
+ | tool output | reduction | fidelity (verified) |
50
+ |---|---:|---|
51
+ | rspec full suite (21 failures, ~8k lines) | **97%** | all 21 failures + the tally kept |
52
+ | `git log --stat` / `ls -R` | **94%** | boundary/keyword lines kept |
53
+ | large source diff (9 files) | **42%** | all 575 ± lines, 13 hunks, 9 headers |
54
+ | `package-lock.json` diff (60 bumps) | **99%** | file header + summary (body elided) |
55
+ | whole-file Ruby read → skeleton | **27%** | signatures + structure kept |
56
+ | JSON (kubectl / docker / gh, uniform rows) | **40–88%** | error rows + outliers always kept |
57
+ | rubocop (already signal-dense) | 11% | floor — every offense kept |
58
+
59
+ End-to-end A/B on real edit tasks: **12/12 tasks passed with compression ON and
60
+ OFF** — it never broke a task, and every forced-failure run still recovered the
61
+ single failing line out of a long log. Routing is verified (each output goes to
62
+ the right strategy) and small inputs pass through **byte-identical**.
63
+
14
64
  ## Install
15
65
 
16
66
  One line, Linux and macOS (x86_64 / arm64). Installs a compatible Ruby, then the gem — all in user space, no sudo:
@@ -111,7 +161,7 @@ agent:
111
161
 
112
162
  memory:
113
163
  enabled: true
114
- backend: "sqlite" # tiny-Zep FTS5 + graph-lite recall (default)
164
+ backend: "sqlite" # SQLite FTS5 + graph-lite recall (default)
115
165
  auto_extract: true
116
166
 
117
167
  compression:
@@ -126,7 +176,7 @@ tools:
126
176
  git: true
127
177
  shell: true # ON by default; every command is still approval-gated
128
178
  ruby: true
129
- web: false # gates BOTH webfetch and websearch
179
+ web: true # ON by default (keyless DuckDuckGo backend); gates BOTH webfetch and websearch
130
180
  memory: true
131
181
  ```
132
182
 
@@ -142,7 +192,7 @@ Full reference (every key, env vars, precedence): **[docs/configuration.md](docs
142
192
  - **[Configuration](docs/configuration.md)** — full config + env vars + precedence
143
193
  - **[Tools](docs/tools.md)** — the built-in tool set and approval behavior
144
194
  - **[Skills](docs/skills.md)** — reusable instruction packs, the 3-level disclosure, and `SKILL_LOADED` observability
145
- - **[Memory](docs/memory.md)** — the SQLite tiny-Zep backend
195
+ - **[Memory](docs/memory.md)** — the SQLite memory backend
146
196
  - **[Security](docs/security.md)** — approval model, hardline floor, TLS
147
197
  - **[Troubleshooting](docs/troubleshooting.md)** — keyed on the exact error strings
148
198
  - **[HTTP API](docs/api/v1.md)** · **[Jobs & cron](docs/jobs.md)** · **[OAuth providers](docs/oauth-providers.md)** · **[Architecture](docs/architecture.md)**
@@ -150,7 +200,7 @@ Full reference (every key, env vars, precedence): **[docs/configuration.md](docs
150
200
 
151
201
  ## Built-in tools
152
202
 
153
- The agent ships **27 built-in tools** (the set `rubino tools` lists): `read`, `read_attachment`, `summarize_file`, `write`, `edit`, `multi_edit`, `apply_patch`, `grep`, `glob`, `git`, `github`, `shell`, `shell_output`, `shell_tail`, `shell_input`, `shell_kill`, `ruby`, `run_tests`, `web`, `question`, `todowrite`, `memory`, `session_search`, `attach_file`, `vision`, `skill`, `task`. A single `web` tool gates both fetching a URL and searching (config key `tools.web`, off by default). Each tool is gated by a `tools.<key>` config flag (opt-out) and the approval model. See **[docs/tools.md](docs/tools.md)**.
203
+ The agent ships **27 built-in tools** (the set `rubino tools` lists): `read`, `read_attachment`, `summarize_file`, `write`, `edit`, `multi_edit`, `apply_patch`, `grep`, `glob`, `git`, `github`, `shell`, `shell_output`, `shell_tail`, `shell_input`, `shell_kill`, `ruby`, `run_tests`, `web`, `question`, `todowrite`, `memory`, `session_search`, `attach_file`, `vision`, `skill`, `task`. A single `web` tool gates both fetching a URL and searching (config key `tools.web`, on by default via the keyless DuckDuckGo backend; it degrades gracefully when no search backend is reachable). Each tool is gated by a `tools.<key>` config flag (opt-out) and the approval model. See **[docs/tools.md](docs/tools.md)**.
154
204
 
155
205
  ## Skills
156
206
 
@@ -191,7 +241,6 @@ These are designed-in but not fully wired yet — don't depend on them in produc
191
241
 
192
242
  - **MCP Support** — connect to Model Context Protocol servers via [ruby_llm-mcp](https://github.com/patvice/ruby_llm-mcp) ([docs/mcp.md](docs/mcp.md)).
193
243
  - **Multi-Agent** — Build / Plan / Explore agents with `@mention` routing ([docs/agents.md](docs/agents.md)).
194
- - **Plugin Hooks** — event hooks for extending behavior ([docs/plugins.md](docs/plugins.md)).
195
244
 
196
245
  ## Development
197
246
 
data/Rakefile CHANGED
@@ -7,6 +7,23 @@ RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  task default: :spec
9
9
 
10
+ # API documentation. `rake rdoc` regenerates the HTML API docs into doc/rdoc
11
+ # (gitignored); the .github/workflows/docs.yml workflow publishes the same
12
+ # output to GitHub Pages. Guarded so the Rakefile still loads if the `rdoc`
13
+ # default gem is somehow absent.
14
+ begin
15
+ require "rdoc/task"
16
+
17
+ RDoc::Task.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = "doc/rdoc"
19
+ rdoc.main = "README.md"
20
+ rdoc.title = "rubino-agent API documentation"
21
+ rdoc.rdoc_files.include("lib/**/*.rb", "exe/*", "README.md", "CHANGELOG.md", "docs/*.md")
22
+ end
23
+ rescue LoadError
24
+ # `rdoc` unavailable -> the `rake rdoc` task is simply not defined.
25
+ end
26
+
10
27
  # Parallel test execution across CPU cores via the `parallel_tests` gem.
11
28
  #
12
29
  # rake parallel:spec # auto: one worker per core
data/docs/agents.md CHANGED
@@ -68,15 +68,21 @@ message instead of fanning out unbounded work:
68
68
  | Glyph | Status | Meaning | You act via |
69
69
  |---|---|---|---|
70
70
  | `●` | `running` | Working (last activity shown) | — |
71
- | `●` | `needs_approval` | A child tool needs your approval | `/agents <id>` |
72
- | `⛔` | `blocked_on_human` | Asked a question only YOU can answer (`ask_parent` escalated to the human) | `/reply <id> <answer>` |
73
- | `◷` | `blocked_on_parent` | Asked its agent-parent a question the PARENT MODEL answers (`answer_child`); not your job unless you choose to step in with `/reply` | (optional) `/reply <id>` |
71
+ | `●` | `needs_approval` | A child tool needs your approval (or a budget request) | `/agents <id>` or `/reply <id>` |
72
+ | `⛔` | `blocked_on_human` | Vocabulary glyph for a child parked on the human (not raised in normal operation now that subagents are non-blocking) | `/reply <id> <answer>` |
73
+ | `◷` | `blocked_on_parent` | Vocabulary glyph for a child parked on its agent-parent (likewise not raised now that subagents are non-blocking) | (optional) `/reply <id>` |
74
74
  | `◌` | `stopping` | Stop requested; unwinding at its next checkpoint | — |
75
75
  | `✓` | `done` | Finished; result available | `/agents <id>` |
76
76
  | `✗` | `failed` | Errored; error available | `/agents <id>` |
77
- | `⊘` | `stopped` | Cancelled by you (`--stop`); blocked descendants unwound; tools that completed before the stop may have left side effects | `/agents <id>` |
77
+ | `⊘` | `stopped` | Cancelled by you (`--stop`); descendants unwound; tools that completed before the stop may have left side effects | `/agents <id>` |
78
78
 
79
- A `⛔ N subagent waiting on you` marker persists until you `/reply`.
79
+ Subagents are **non-blocking** background workers: they never pause to ask you a
80
+ mid-task question. The one way a child waits on you is an **approval** — its next
81
+ tool needs your go-ahead, so it parks as `needs_approval` and a marker persists
82
+ until you resolve it (via `/agents <id>` or `/reply <id>`). The `⛔
83
+ blocked_on_human` / `◷ blocked_on_parent` glyphs remain in the status vocabulary
84
+ the `/agents` surface can render, but with the child→parent ask channel removed
85
+ they are no longer raised in normal operation.
80
86
 
81
87
  ### Supervising from the CLI: `/agents` and `/reply`
82
88
 
@@ -86,12 +92,31 @@ A `⛔ N subagent waiting on you` marker persists until you `/reply`.
86
92
  /agents <id> --stop # cancel a running subagent (blocked descendants unwind too)
87
93
  /agents <id> steer "note" # park a note folded into the child's context at its next turn
88
94
  /agents <id> probe "question" # ephemeral read-only peek — nothing is saved to the child
89
- /reply <id> <answer> # answer a child blocked on an ask_parent question
95
+ /reply <id> <answer> # answer a child blocked on you (e.g. an approval)
90
96
  /reply # bare: list the subagents currently blocked on you
91
97
  ```
92
98
 
93
99
  `/tasks` is an alias for `/agents`. Stopping a node cancels its descendants'
94
- ask-gates too, so a blocking question anywhere in the subtree unwinds at once.
100
+ approval gates too, so anything parked anywhere in the subtree unwinds at once.
101
+
102
+ #### Attach to a subagent (agent-view)
103
+
104
+ The typed forms above work by id from anywhere, but the fastest way to focus on
105
+ one running child is to **attach**. At the idle prompt press `↓` to open the
106
+ subagent picker, arrow to one, and `Enter`:
107
+
108
+ - the screen switches to that agent's **own full timeline** — its tool calls and
109
+ what it said, replayed from its session (not the bounded activity snapshot the
110
+ picker used to show);
111
+ - the prompt becomes **scoped** to it: `sa_xxxx ❯`;
112
+ - while attached, just **type** to steer the running child (or answer it if it's
113
+ blocked on you) — no id needed; `←` on the empty prompt (or `/detach`) returns
114
+ to the main timeline.
115
+
116
+ So attaching makes `/agents <id> steer/probe` and `/reply <id>` redundant for the
117
+ focused child — they're the same operations, just addressed by id. Attach is a
118
+ between-turns action (it owns the screen): while a parent turn is still streaming
119
+ the picker's `Enter` toasts "attach when the turn ends" — attach once it's idle.
95
120
 
96
121
  **steer** is a persistent course-correction: the note enters the child's context
97
122
  at its next turn boundary and changes its trajectory.
@@ -99,12 +124,15 @@ at its next turn boundary and changes its trajectory.
99
124
  child's transcript; the answer is shown to you and discarded — nothing is
100
125
  appended to the child's history.
101
126
 
102
- ### Parentchild channels (model-driven)
127
+ ### Parentchild channels (model-driven)
103
128
 
104
- The same three verbs are MODEL-callable tools, so an agent-parent can supervise
105
- its own children the way you supervise yours. All are gated by `tools.task` and
106
- **ownership-scoped at call time** a caller can only touch its own direct
107
- children (see [tools.md](tools.md) for parameters):
129
+ Both verbs are MODEL-callable tools, so an agent-parent can supervise its own
130
+ children the way you supervise yours. They are **parent→child only** a
131
+ subagent has no channel to ask its parent a question mid-task (subagents are
132
+ non-blocking; they make sensible default calls and surface open decisions in
133
+ their result instead). Both are gated by `tools.task` and **ownership-scoped at
134
+ call time** — a caller can only touch its own direct children (see
135
+ [tools.md](tools.md) for parameters):
108
136
 
109
137
  - **`steer(task_id, note)`** — park a persistent note on one of your running
110
138
  children; it folds into the child's context at its next turn.
@@ -113,19 +141,6 @@ children (see [tools.md](tools.md) for parameters):
113
141
  activity, recent lines); `live: true` is a billed one-shot model peek over the
114
142
  child's transcript, budgeted per child (`tasks.max_live_probes_per_child`,
115
143
  default 5).
116
- - **`ask_parent(question, blocking:)`** — the child→parent escalation (only
117
- available to subagents). `blocking: false` (default) keeps the child working
118
- and folds the answer in later; `blocking: true` parks the child until answered,
119
- bounded by `tasks.ask_parent_timeout` (default 900s — on expiry the child
120
- proceeds with its best judgement instead of hanging).
121
- Routing depends on who spawned the child: an agent-parent gets the question as
122
- a note and answers with `answer_child` (child shows `◷ blocked_on_parent`); a
123
- human-spawned child escalates straight to you (`⛔ blocked_on_human`, answered
124
- via `/reply`). A parent that cannot answer from its own context escalates by
125
- calling its OWN `ask_parent` — questions bubble up the tree to the human.
126
- - **`answer_child(task_id, answer)`** — the agent-parent's `/reply`: delivers
127
- the answer into the asking child's context (unblocks a blocking ask, folds in
128
- for a non-blocking one).
129
144
 
130
145
  ### Approvals inside a background child
131
146
 
data/docs/architecture.md CHANGED
@@ -17,9 +17,8 @@ Infrastructure Layer → LLM Adapter, Database, MCP, OAuth
17
17
  1. **All output goes through UI** — No `puts`/`print` in core modules
18
18
  2. **LLM is isolated** — Only `LLM::RubyLLMAdapter` talks to ruby_llm
19
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
20
+ 4. **Event-driven** — Core emits events, UI subscribes
21
+ 5. **Config is not architecture** — Configuration describes what; architecture decides how
23
22
 
24
23
  ## Module Map
25
24
 
@@ -96,11 +95,6 @@ Experimental — booted at chat startup when `mcp.servers` is configured
96
95
  - `DoomLoopDetector` — Detects repeated identical tool calls
97
96
  - `CommandAllowlist` — Pre-approved shell commands
98
97
 
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
98
  ### `skills/`
105
99
  - `Skill` — Parsed SKILL.md with YAML frontmatter
106
100
  - `Registry` — Discovery from configured paths
@@ -167,7 +161,6 @@ User Input
167
161
  │ │ ├─ Check permissions (ApprovalPolicy)
168
162
  │ │ ├─ Check doom loop (DoomLoopDetector)
169
163
  │ │ ├─ Execute tool (ToolExecutor)
170
- │ │ ├─ Run plugin hooks
171
164
  │ │ └─ Loop back to LLM
172
165
  │ └─ Final text response
173
166