rubino-agent 0.3.0 → 0.5.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 (196) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +11 -2
  3. data/AGENTS.md +1 -1
  4. data/CHANGELOG.md +172 -5
  5. data/CONTRIBUTING.md +10 -1
  6. data/README.md +14 -5
  7. data/Rakefile +31 -0
  8. data/docs/agents.md +42 -23
  9. data/docs/architecture.md +2 -2
  10. data/docs/commands.md +35 -3
  11. data/docs/configuration.md +20 -23
  12. data/docs/getting-started.md +5 -3
  13. data/docs/security.md +16 -5
  14. data/docs/skills.md +31 -0
  15. data/docs/troubleshooting.md +1 -1
  16. data/exe/rubino +16 -2
  17. data/install.sh +721 -59
  18. data/lib/rubino/active_agent.rb +73 -0
  19. data/lib/rubino/agent/action_claim_guard.rb +881 -0
  20. data/lib/rubino/agent/agent_registry.rb +5 -2
  21. data/lib/rubino/agent/definition.rb +1 -9
  22. data/lib/rubino/agent/fallback_chain.rb +0 -6
  23. data/lib/rubino/agent/iteration_budget.rb +109 -3
  24. data/lib/rubino/agent/loop.rb +476 -20
  25. data/lib/rubino/agent/model_call_runner.rb +81 -3
  26. data/lib/rubino/agent/prompts/build.txt +22 -5
  27. data/lib/rubino/agent/response_validator.rb +8 -0
  28. data/lib/rubino/agent/runner.rb +133 -8
  29. data/lib/rubino/agent/tool_executor.rb +166 -14
  30. data/lib/rubino/agent/truncation_continuation.rb +4 -1
  31. data/lib/rubino/api/server.rb +19 -0
  32. data/lib/rubino/attachments/classify.rb +35 -17
  33. data/lib/rubino/boot/config_guard.rb +71 -0
  34. data/lib/rubino/cli/chat/completion_builder.rb +42 -6
  35. data/lib/rubino/cli/chat/idle_card_host.rb +7 -1
  36. data/lib/rubino/cli/chat/session_resolver.rb +87 -21
  37. data/lib/rubino/cli/chat_command.rb +1189 -50
  38. data/lib/rubino/cli/commands.rb +282 -2
  39. data/lib/rubino/cli/config_command.rb +68 -8
  40. data/lib/rubino/cli/doctor_command.rb +204 -12
  41. data/lib/rubino/cli/jobs_command.rb +12 -0
  42. data/lib/rubino/cli/memory_command.rb +53 -20
  43. data/lib/rubino/cli/onboarding_wizard.rb +79 -6
  44. data/lib/rubino/cli/session_command.rb +172 -18
  45. data/lib/rubino/cli/setup_command.rb +131 -8
  46. data/lib/rubino/cli/skills_command.rb +183 -9
  47. data/lib/rubino/cli/trust_gate.rb +16 -7
  48. data/lib/rubino/commands/built_ins.rb +2 -0
  49. data/lib/rubino/commands/command.rb +12 -2
  50. data/lib/rubino/commands/executor.rb +149 -12
  51. data/lib/rubino/commands/handlers/agent_switch.rb +100 -0
  52. data/lib/rubino/commands/handlers/agents.rb +156 -41
  53. data/lib/rubino/commands/handlers/config.rb +4 -1
  54. data/lib/rubino/commands/handlers/help.rb +113 -14
  55. data/lib/rubino/commands/handlers/memory.rb +15 -5
  56. data/lib/rubino/commands/handlers/sessions.rb +26 -3
  57. data/lib/rubino/commands/handlers/status.rb +9 -4
  58. data/lib/rubino/commands/loader.rb +12 -0
  59. data/lib/rubino/config/configuration.rb +86 -24
  60. data/lib/rubino/config/defaults.rb +140 -33
  61. data/lib/rubino/config/loader.rb +62 -12
  62. data/lib/rubino/config/validator.rb +341 -0
  63. data/lib/rubino/config/writer.rb +123 -31
  64. data/lib/rubino/context/compressor.rb +184 -22
  65. data/lib/rubino/context/environment_inspector.rb +2 -2
  66. data/lib/rubino/context/file_discovery.rb +2 -2
  67. data/lib/rubino/context/message_boundary.rb +27 -1
  68. data/lib/rubino/context/project_languages.rb +90 -0
  69. data/lib/rubino/context/prompt_assembler.rb +105 -22
  70. data/lib/rubino/context/summary_builder.rb +45 -4
  71. data/lib/rubino/context/token_budget.rb +36 -11
  72. data/lib/rubino/context/token_estimate.rb +45 -0
  73. data/lib/rubino/context/tool_result_pruner.rb +81 -0
  74. data/lib/rubino/database/connection.rb +154 -3
  75. data/lib/rubino/database/migrations/001_create_initial_schema.rb +314 -40
  76. data/lib/rubino/database/migrator.rb +98 -5
  77. data/lib/rubino/documents/cap_exceeded.rb +13 -0
  78. data/lib/rubino/documents/converters/csv.rb +4 -3
  79. data/lib/rubino/documents/converters/docx.rb +29 -5
  80. data/lib/rubino/documents/converters/html.rb +5 -1
  81. data/lib/rubino/documents/converters/json.rb +2 -1
  82. data/lib/rubino/documents/converters/pdf.rb +11 -2
  83. data/lib/rubino/documents/converters/plain.rb +2 -1
  84. data/lib/rubino/documents/converters/pptx.rb +11 -2
  85. data/lib/rubino/documents/converters/xlsx.rb +35 -4
  86. data/lib/rubino/documents/converters/xml.rb +2 -1
  87. data/lib/rubino/documents/limits.rb +210 -0
  88. data/lib/rubino/documents.rb +10 -3
  89. data/lib/rubino/errors.rb +36 -5
  90. data/lib/rubino/interaction/cancel_token.rb +19 -3
  91. data/lib/rubino/interaction/events.rb +13 -0
  92. data/lib/rubino/interaction/lifecycle.rb +99 -13
  93. data/lib/rubino/interaction/polishing.rb +176 -0
  94. data/lib/rubino/jobs/cron_job_repository.rb +5 -8
  95. data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +11 -0
  96. data/lib/rubino/jobs/handlers/distill_skill_job.rb +65 -9
  97. data/lib/rubino/jobs/queue.rb +63 -8
  98. data/lib/rubino/jobs/runner.rb +24 -6
  99. data/lib/rubino/jobs/worker.rb +0 -4
  100. data/lib/rubino/llm/adapter_response.rb +47 -4
  101. data/lib/rubino/llm/credential_check.rb +15 -16
  102. data/lib/rubino/llm/error_classifier.rb +89 -1
  103. data/lib/rubino/llm/inline_think_filter.rb +69 -12
  104. data/lib/rubino/llm/request.rb +30 -3
  105. data/lib/rubino/llm/ruby_llm_adapter.rb +394 -46
  106. data/lib/rubino/llm/tool_bridge.rb +113 -9
  107. data/lib/rubino/mcp/manager.rb +18 -1
  108. data/lib/rubino/mcp/mcp_tool_wrapper.rb +14 -3
  109. data/lib/rubino/memory/aux_retry.rb +107 -0
  110. data/lib/rubino/memory/backends/sqlite.rb +73 -44
  111. data/lib/rubino/memory/backends.rb +23 -7
  112. data/lib/rubino/memory/salience_gate.rb +103 -0
  113. data/lib/rubino/memory/sqlite_extraction.rb +70 -0
  114. data/lib/rubino/memory/sqlite_extraction_prompt.rb +11 -0
  115. data/lib/rubino/memory/store.rb +33 -5
  116. data/lib/rubino/memory/threat_scanner.rb +52 -0
  117. data/lib/rubino/output/cost.rb +52 -0
  118. data/lib/rubino/output/headless_block_latch.rb +53 -0
  119. data/lib/rubino/output/result_serializer.rb +222 -0
  120. data/lib/rubino/output/turn_recorder.rb +77 -0
  121. data/lib/rubino/security/approval_policy.rb +227 -32
  122. data/lib/rubino/security/command_allowlist.rb +79 -4
  123. data/lib/rubino/security/doom_loop_detector.rb +21 -2
  124. data/lib/rubino/security/hardline_guard.rb +189 -16
  125. data/lib/rubino/security/pattern_matcher.rb +28 -5
  126. data/lib/rubino/security/prefix_deriver.rb +25 -6
  127. data/lib/rubino/security/readonly_commands.rb +145 -5
  128. data/lib/rubino/security/secret_path.rb +134 -0
  129. data/lib/rubino/security/url_safety.rb +255 -0
  130. data/lib/rubino/session/repository.rb +212 -11
  131. data/lib/rubino/session/store.rb +139 -14
  132. data/lib/rubino/skills/installer.rb +230 -0
  133. data/lib/rubino/skills/prompt_index.rb +2 -2
  134. data/lib/rubino/skills/registry.rb +52 -1
  135. data/lib/rubino/skills/skill.rb +64 -3
  136. data/lib/rubino/skills/skill_tool.rb +16 -5
  137. data/lib/rubino/tools/background_tasks.rb +157 -13
  138. data/lib/rubino/tools/base.rb +204 -3
  139. data/lib/rubino/tools/edit_tool.rb +73 -18
  140. data/lib/rubino/tools/glob_tool.rb +48 -9
  141. data/lib/rubino/tools/grep_tool.rb +103 -9
  142. data/lib/rubino/tools/multi_edit_tool.rb +64 -9
  143. data/lib/rubino/tools/patch_tool.rb +5 -0
  144. data/lib/rubino/tools/read_attachment_tool.rb +3 -1
  145. data/lib/rubino/tools/read_tool.rb +33 -15
  146. data/lib/rubino/tools/read_tracker.rb +153 -35
  147. data/lib/rubino/tools/registry.rb +113 -12
  148. data/lib/rubino/tools/result.rb +9 -1
  149. data/lib/rubino/tools/ruby_tool.rb +0 -0
  150. data/lib/rubino/tools/shell_registry.rb +70 -0
  151. data/lib/rubino/tools/shell_tool.rb +40 -1
  152. data/lib/rubino/tools/summarize_file_tool.rb +6 -0
  153. data/lib/rubino/tools/task_stop_tool.rb +10 -16
  154. data/lib/rubino/tools/task_tool.rb +36 -8
  155. data/lib/rubino/tools/vision_tool.rb +5 -0
  156. data/lib/rubino/tools/webfetch_tool.rb +39 -7
  157. data/lib/rubino/tools/websearch_tool.rb +92 -30
  158. data/lib/rubino/tools/write_tool.rb +23 -4
  159. data/lib/rubino/ui/api.rb +10 -1
  160. data/lib/rubino/ui/base.rb +11 -0
  161. data/lib/rubino/ui/bottom_composer.rb +382 -74
  162. data/lib/rubino/ui/cli.rb +515 -83
  163. data/lib/rubino/ui/completion_menu.rb +11 -7
  164. data/lib/rubino/ui/headless_trace.rb +63 -0
  165. data/lib/rubino/ui/live_region.rb +70 -7
  166. data/lib/rubino/ui/markdown_renderer.rb +142 -7
  167. data/lib/rubino/ui/notifier.rb +0 -2
  168. data/lib/rubino/ui/null.rb +52 -5
  169. data/lib/rubino/ui/paste_store.rb +16 -2
  170. data/lib/rubino/ui/queued_indicators.rb +6 -1
  171. data/lib/rubino/ui/status_bar.rb +61 -7
  172. data/lib/rubino/ui/streaming_markdown.rb +59 -6
  173. data/lib/rubino/ui/subagent_view.rb +29 -4
  174. data/lib/rubino/ui/tool_label.rb +52 -0
  175. data/lib/rubino/update_check.rb +39 -4
  176. data/lib/rubino/util/atomic_file.rb +117 -0
  177. data/lib/rubino/util/ignore_rules.rb +120 -0
  178. data/lib/rubino/util/output.rb +229 -12
  179. data/lib/rubino/util/secrets_mask.rb +70 -7
  180. data/lib/rubino/util/spill_store.rb +153 -0
  181. data/lib/rubino/version.rb +1 -1
  182. data/lib/rubino/workspace.rb +9 -1
  183. data/lib/rubino.rb +191 -7
  184. data/rubino-agent.gemspec +1 -0
  185. data/skills/ruby-expert/SKILL.md +1 -0
  186. metadata +42 -12
  187. data/lib/rubino/agent/router.rb +0 -65
  188. data/lib/rubino/database/migrations/002_create_runs.rb +0 -45
  189. data/lib/rubino/database/migrations/003_create_skill_states.rb +0 -15
  190. data/lib/rubino/database/migrations/004_create_cron_jobs.rb +0 -36
  191. data/lib/rubino/database/migrations/005_create_oauth_connections.rb +0 -27
  192. data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +0 -34
  193. data/lib/rubino/database/migrations/007_create_messages_fts.rb +0 -59
  194. data/lib/rubino/database/migrations/008_create_memory_facts.rb +0 -75
  195. data/lib/rubino/database/migrations/009_create_memory_graph.rb +0 -55
  196. data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db2afff4be97c340cbe86b9fb1b7509f1457abdfe812ac8c376e03421229ada1
4
- data.tar.gz: efc19c01b592b21c70304f82fe9dbc4b76fea60b9dbdd63ed15fe5b5d0548f57
3
+ metadata.gz: 8a3a48a6f7deb104446c624354a271ff1a1671c9b3ea846cee1144b3f55538db
4
+ data.tar.gz: cbc407cd78db827d75f150f61045b2cce759b266816e32fdba874c30eb3647c4
5
5
  SHA512:
6
- metadata.gz: fb700fb3a191d046737ad40b53d69c66a1e2c0cba573e04c6d3cfa803fa94cc2f1c74c09390ebda03424e6b30d19b9d4a948ee1ecfaba44926576b31c92d089a
7
- data.tar.gz: 2eef17e973b05eb0eb5e6c0f7ee79ac8c8c440ea32f2c6ef589c06512319d0b03fac46879a480ee92923bf18167ede7fbd9ae14178ede83e32571058791890f6
6
+ metadata.gz: 3342a4c8b1856691788ac9625b81eb4058870a0eb5a75946e99e6570ce40a05a7b44475a798863ac0fc92c9fcb505cf6caf27fcedc10a8e1bd9653ba18edf440
7
+ data.tar.gz: e54077b30f385942dd3591fb7e6c4e7afc0f301e5fea9d3111732c91ce56e63a4c7d192704ebd74ea97ee4a04437784d73a8d398bf02ce6143cdd69b2227a841
data/.rubocop_todo.yml CHANGED
@@ -256,6 +256,8 @@ Metrics/BlockLength:
256
256
  Metrics/ClassLength:
257
257
  Exclude:
258
258
  - 'lib/rubino/cli/chat_command.rb'
259
+ - 'lib/rubino/commands/executor.rb'
260
+ - 'lib/rubino/llm/ruby_llm_adapter.rb'
259
261
  - 'lib/rubino/ui/bottom_composer.rb'
260
262
  - 'lib/rubino/ui/cli.rb'
261
263
 
@@ -406,7 +408,6 @@ Naming/PredicateMethod:
406
408
  Exclude:
407
409
  - 'lib/rubino/agent/model_call_runner.rb'
408
410
  - 'lib/rubino/agent/response_validator.rb'
409
- - 'lib/rubino/agent/router.rb'
410
411
  - 'lib/rubino/cli/onboarding_wizard.rb'
411
412
  - 'lib/rubino/cli/trust_gate.rb'
412
413
  - 'lib/rubino/memory/backends/sqlite.rb'
@@ -451,6 +452,7 @@ RSpec/AnyInstance:
451
452
  - 'spec/rubino/commands/commands_spec.rb'
452
453
  - 'spec/rubino/context/compressor_spec.rb'
453
454
  - 'spec/rubino/context/prompt_assembler_active_skill_spec.rb'
455
+ - 'spec/rubino/context/prompt_assembler_cache_breakpoints_spec.rb'
454
456
  - 'spec/rubino/context/prompt_assembler_ignore_rules_spec.rb'
455
457
  - 'spec/rubino/context/prompt_assembler_layering_spec.rb'
456
458
  - 'spec/rubino/context/prompt_assembler_memory_snapshot_spec.rb'
@@ -462,8 +464,10 @@ RSpec/BeforeAfterAll:
462
464
  - '**/spec/spec_helper.rb'
463
465
  - '**/spec/rails_helper.rb'
464
466
  - '**/spec/support/**/*.rb'
467
+ - 'spec/rubino/llm/tool_bridge_cache_breakpoint_spec.rb'
465
468
  - 'spec/rubino/security/approval_policy_mode_spec.rb'
466
469
  - 'spec/rubino/tools/registry_mode_spec.rb'
470
+ - 'spec/rubino/tools/registry_situational_gating_spec.rb'
467
471
  - 'spec/rubino/tools/registry_spec.rb'
468
472
 
469
473
  # Offense count: 31
@@ -550,7 +554,11 @@ RSpec/DescribeMethod:
550
554
  - 'spec/rubino/api/middleware/json_body_size_spec.rb'
551
555
  - 'spec/rubino/cli/chat_command_continue_spec.rb'
552
556
  - 'spec/rubino/cli/commands_version_spec.rb'
557
+ - 'spec/rubino/llm/ruby_llm_adapter_cache_spec.rb'
558
+ - 'spec/rubino/llm/tool_bridge_cache_breakpoint_spec.rb'
559
+ - 'spec/rubino/tools/registry_situational_gating_spec.rb'
553
560
  - 'spec/rubino/context/prompt_assembler_active_skill_spec.rb'
561
+ - 'spec/rubino/context/prompt_assembler_cache_breakpoints_spec.rb'
554
562
  - 'spec/rubino/context/prompt_assembler_ignore_rules_spec.rb'
555
563
  - 'spec/rubino/context/prompt_assembler_layering_spec.rb'
556
564
  - 'spec/rubino/context/prompt_assembler_memory_snapshot_spec.rb'
@@ -654,12 +662,13 @@ RSpec/LeakyLocalVariable:
654
662
  Exclude:
655
663
  - 'spec/rubino/no_direct_output_spec.rb'
656
664
 
657
- # Offense count: 7
665
+ # Offense count: 8
658
666
  RSpec/MultipleDescribes:
659
667
  Exclude:
660
668
  - 'spec/rubino/commands/commands_spec.rb'
661
669
  - 'spec/rubino/context/prompt_assembler_layering_spec.rb'
662
670
  - 'spec/rubino/memory/backends_spec.rb'
671
+ - 'spec/rubino/tools/encoding_robustness_spec.rb'
663
672
  - 'spec/rubino/tools/error_code_spec.rb'
664
673
  - 'spec/rubino/tools/tool_fixes_spec.rb'
665
674
  - 'spec/rubino/trust_spec.rb'
data/AGENTS.md CHANGED
@@ -77,7 +77,7 @@ lib/rubino/
77
77
  ## Surfaces this project exposes
78
78
 
79
79
  - **HTTP API** (`/v1/*`) — the canonical interface. See `docs/api/v1.md`.
80
- - **CLI** — `rubino {setup,chat,prompt,server,config,memory,sessions,jobs,tools,doctor,version}`.
80
+ - **CLI** — `rubino {setup,chat,prompt,server,config,memory,sessions,jobs,skills,tools,tls_cert,doctor,version,update}`.
81
81
  - **Library** — `require "rubino"; Rubino.run(...)`.
82
82
 
83
83
  The interactive CLI ships as part of `rubino chat`. Multi-agent routing, MCP, and plugin hooks are designed in but not fully wired yet.
data/CHANGELOG.md CHANGED
@@ -1,6 +1,177 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.5.0] - 2026-06-15
4
+
5
+ ### Added
6
+
7
+ - **One-shot tool-activity trace.** The non-interactive text path (`rubino
8
+ prompt` / `-q` / piped `chat`) now prints a concise per-tool activity trace
9
+ by default — one line per tool completion (`· edit foo.rb`, `· bash npm
10
+ test`) — routed to STDERR so the final answer on STDOUT stays clean
11
+ (`x=$(rubino prompt …)` captures only the answer). `--quiet`/`-Q` silences
12
+ the trace (machine-silent path); `--verbose`/`-v` widens each line's args.
13
+ `--output-format json`/`stream-json` (structured events on stdout) and the
14
+ interactive TUI tool-cards are unchanged. Mirrors the Codex/gemini-cli/Hermes
15
+ stderr-trace norm (Hermes `-q` default / `-Q` quiet).
16
+
17
+ - **Prompt-cache breakpoints (`cache_control`).** The conversation now inserts
18
+ cache breakpoints so the stable prefix (system + tool schemas + prior turns)
19
+ is reused across round-trips, cutting input-token cost/latency.
20
+ - **Situational tool-schema gating.** Tool definitions sent to the model are
21
+ scoped to the situation instead of always shipping the full set, reducing
22
+ prompt size and accidental tool selection.
23
+ - **Primary-agent switching.** Switch the active primary agent inline with
24
+ `/<name>`, the `/agent` command, or `Tab`; `@` remains reserved for file
25
+ references.
26
+ - **Detached post-turn polishing.** A post-turn polishing pass runs detached and
27
+ is cancellable with `Esc`, so it never blocks the next prompt.
28
+ - **Stdin pipe for one-shot.** Piped stdin is consumed as the prompt for
29
+ one-shot runs (`echo … | rubino prompt`), enabling unix-style composition.
30
+ - **Per-round-trip loop accounting.** Round-trips are counted, usage is summed
31
+ across them, and `tool_calls` are persisted on the streaming path.
32
+ - **Machine-readable headless output (`--output-format json | stream-json`, #312).**
33
+ `rubino prompt` / `chat -q` can now emit Claude-Code-aligned JSON for
34
+ CI/automation instead of prose. `--output-format json` (or the `--json` alias)
35
+ prints a single `{type:"result", subtype, is_error, result, session_id,
36
+ exit_reason, num_turns, duration_ms, usage:{input/output/cache_* tokens},
37
+ total_cost_usd, model}` object on stdout at completion; `--output-format
38
+ stream-json` emits JSONL (a `system`/`init` line, then Messages-API-shaped
39
+ `assistant`/`user` step objects, then the same final `result`). In both modes
40
+ ALL JSON goes to stdout and ALL logs/diagnostics/errors to stderr, and markdown
41
+ rendering is suppressed. The fail-closed / exit-code contract is preserved: a
42
+ blocked tool still emits the result with `is_error:true` and a non-zero exit.
43
+ The schema lives in a single shared serializer (`Rubino::Output::ResultSerializer`)
44
+ so it never drifts. `text` (default) is unchanged.
45
+ - **Higher tool-loop budget with an interactive extension prompt (#399).** The
46
+ `max_tool_iterations` default is raised from 8 to 25 so longer agent runs no
47
+ longer hit the cap mid-task. When the cap is reached interactively, the run
48
+ pauses with a budget-extension prompt — **Continue +N** (grant another batch),
49
+ **Summarize** (wrap up with what's done), or **Abort** — instead of failing
50
+ silently; headless runs keep the force-summarize behavior.
51
+ - **TUI: Ctrl-L clear-screen and a resize-while-typing fix (#395 / #401).**
52
+ `Ctrl-L` now clears the screen from the composer. Fixed a bug where resizing
53
+ the terminal while typing reflowed and duplicated the in-progress input into
54
+ the scrollback.
55
+
56
+ ### Security
57
+
58
+ - **Hardened/narrowed the command-allowlist convenience layer (SEC-R2-1/2/3).**
59
+ Closes three default-config / bare-`git` paths that could run arbitrary code
60
+ or write arbitrary files past the headless gate **without `--yolo`**:
61
+ - removed code-loading test/build runners (`bundle exec rspec`, …) from the
62
+ **shipped default** `command_allowlist` — they load and execute arbitrary
63
+ project code by design (`rspec -r FILE`), so they are not safely
64
+ auto-approvable (SEC-R2-3);
65
+ - an allowlisted **git** head is now vetted for GLOBAL flags before the
66
+ subcommand (`git -c alias.x='!cmd' x`, `-c core.sshCommand=…`, `-C dir`,
67
+ `--exec-path`) and for code-loading/mutating subcommands (`apply`, `am`,
68
+ `push`, hooks, …); the "approve git always" path now persists only a
69
+ narrowed `git <read-only verb>`, never bare `git` (SEC-R2-1);
70
+ - any allowlisted head whose argument is itself a program
71
+ (`awk`/`sed`/`perl`/`python`/`ruby`/`node`/`tar`/`tee`/`xargs`/shells) is
72
+ default-denied auto-approval, and write flags on read heads (`sort -o`, …)
73
+ are rejected (SEC-R2-2).
74
+
75
+ An allowlist is a convenience layer, **not** a security boundary (per industry
76
+ practice the OS sandbox is the real floor, tracked separately); this narrows
77
+ it to close the above default-config and bare-`git` RCEs.
78
+
79
+ ### Hardening
80
+
81
+ Four adversarial QA rounds fixed ~45 issues across the agent. Highlights:
82
+
83
+ - **Security.** Hardline-floor canonicalization; OOXML zip-bomb total-archive
84
+ cap; CWE-150 argument sanitization; threat-scanner; tightened
85
+ command-allowlist (see above).
86
+ - **Correctness.** UTF-8-safe edits; atomic compaction with auto-switch-to-child;
87
+ resume keeps the full tool history; cwd-scoped sessions; corrupt-DB recovery
88
+ (incl. `NotADatabaseException`); job-queue compare-and-swap; headless job drain
89
+ so memory works in automation.
90
+ - **Interrupt.** True cancel — stream cancellation with the partial persisted;
91
+ clean one-shot `SIGINT`/`SIGTERM` labels.
92
+ - **Performance.** Bounded huge-output memory; spill/paste eviction; streaming
93
+ grep with consistent ignore rules.
94
+ - **UX.** Config validation; `doctor` checks; resilient timeouts and error
95
+ classification.
96
+
97
+ Every fix was container-verified (non-root QA image, real MiniMax for live
98
+ behavior, true 0 failures); a full pre-release functionality sweep confirmed all
99
+ subsystems release-ready.
100
+
101
+ ## [0.4.1] - 2026-06-13
102
+
103
+ ### Security
104
+
105
+ - **Headless approvals now fail closed (#260).** A one-shot / scripted run
106
+ (`rubino prompt`, `chat -q`, no TTY) no longer auto-runs a tool that would
107
+ otherwise prompt: a write/edit, or a shell command not covered by your
108
+ `permissions` / command allowlist / read-only auto-allow, is **blocked, not
109
+ run**. A `blocked: <tool> needs approval …` line goes to stderr and the run
110
+ exits **2**, so CI/automation fails loudly instead of silently skipping (or
111
+ auto-executing). Full auto-exec now requires an explicit **`--yolo`** —
112
+ honored ONLY as a CLI flag, never grantable by a project-local/persisted
113
+ config — and **`--no-yolo`** forces fail-closed even over a yolo boot default.
114
+
115
+ ### Fixed — installer
116
+
117
+ - **`mise` method (#256)** alongside Homebrew and `rv`, with `global`/`local`
118
+ scope (`RUBINO_INSTALL_SCOPE`); `RUBINO_INSTALL_METHOD` now accepts `mise`.
119
+ - **Activation/PATH is persisted to your shell rc (#268)** (`.zshrc` /
120
+ `.bashrc` / `.profile`) and a **post-install fresh-shell gate** fails loudly
121
+ if `rubino` isn't on PATH in a new shell. `RUBINO_NO_MODIFY_RC=1` opts out.
122
+ - **`mise` installs pin the latest published gem version (#258/#268)** instead
123
+ of drifting to a pre-release / age-gated build.
124
+ - **Method-aware prereq preflight (#272)** (xz/git/toolchain) with real gem
125
+ error surfacing, and a **Debian-12 / glibc-too-old steer from rv → mise
126
+ (#241/#242/#272)** so users don't land on a broken musl Ruby.
127
+
128
+ ### Fixed
129
+
130
+ - **Config corruption + `doctor` crash on a scalar written over a section (#259).**
131
+ - **Streaming persistence (#266):** pre-tool narration is persisted and the
132
+ `tool_calls` audit is populated.
133
+ - **TUI render (#269):** table columns sized to content, nested/markdown fences
134
+ consumed, interrupt "ghost" line cleared.
135
+ - **Memory extraction bounded by a per-session cursor (#249)** — no more
136
+ re-scanning the whole transcript every turn.
137
+ - **Boots under a bare C/POSIX locale (#273)** without
138
+ `Encoding::CompatibilityError`.
139
+ - **Session summary folded into the single system message (#253/#254).**
140
+
141
+ ## [0.4.0] - 2026-06-13
142
+
143
+ ### Added — skills from git (#4)
144
+
145
+ - **`rubino skills install <owner/repo | git-URL>`** — install skills from any
146
+ git repo shipping the `<name>/SKILL.md` layout (`--skill NAME` / `--all` /
147
+ `--list`; `--documents` is shorthand for the four `anthropics/skills`
148
+ document skills). Provenance lands in `~/.rubino/skills/.sources.json`, so
149
+ **`rubino skills update`** re-fetches from the recorded source (up-to-date vs
150
+ updated by commit) and **`rubino skills remove NAME`** only deletes what this
151
+ mechanism installed. `rubino skills list` gains a Source column.
152
+ - The skill registry now also discovers the agent-neutral `.agents/skills/`
153
+ and `~/.agents/skills/` dirs (the `npx skills` / Gemini CLI convention) —
154
+ additive, lowest precedence, trust-gated like `.rubino/skills`.
155
+
156
+ ### Added
157
+
158
+ - **`/agents <id>` watch — live tool-output tail (#5).** The drill-in watch grows an `output:` block showing the tail of the running subagent's current tool output, clearing when the tool finishes.
159
+ - **`soffice` and `qpdf` in the `[Environment]` probe (#4/#6)** so the agent honestly reports whether LibreOffice/qpdf are available for the document skills.
160
+
161
+ ### Fixed
162
+
163
+ - **`read_attachment` extension-spoof gate now covers document MIMEs (#239).** A text file named `report.docx` reads inline as text instead of bouncing off the document converter; a real `.docx` (ZIP magic) still classifies as a document.
164
+ - **No more CLI crash under a C/POSIX locale (#250).** Skill and context files are read as UTF-8 rather than the ambient (US-ASCII) encoding, so `rubino skills list` and prompt assembly no longer raise `invalid byte sequence` on minimal Linux/Docker images.
165
+ - **Installer no longer always exits 1 on a fresh Linux box (#240).** Fixed an unbound `rv_bin` under `set -u` and an invalid `gem environment gembindir` call; `curl … | bash` now installs cleanly and is idempotent.
166
+
167
+ ### Internal
168
+
169
+ - **Test stability (#236):** PTY capture specs read to the child's EOF instead of treating a 0.5s quiet window as end-of-output, removing a rare 2-failure flake under concurrent suite load.
170
+ - **Approval-handoff guard (#10):** the #80 unit guard now genuinely fails on a full revert; the PTY handoff spec is relabeled as a happy-path check.
171
+
172
+ ## [0.3.0] - 2026-06-06
173
+
174
+ Major capability release: the core conversation loop was ported 1:1 from the reference implementation (formalized LLM boundary, retry/backoff/fallback, degenerate-response recovery), background subagents became the default delegation path, the memory subsystem grew a pluggable backend contract with a tiny-Zep SQLite backend that is now the default, CLI gained image/file input and a scroll-native redesign, and a reference-aligned approval model (hardline floor, dangerous-pattern deny, prefix-derived rules) landed. Consolidated from `feature/subagent-view` (#48) plus #49-#58.
4
175
 
5
176
  ### Added — CLI redesign & in-chat surfaces
6
177
 
@@ -107,10 +278,6 @@ The GitHub repository is `github.com/Jhonnyr97/rubino-agent`. Publishing the ren
107
278
 
108
279
  - `docs/api/v1.md` aligned to the real API surface (#165, #166, #167): SSE catalogue documents the non-streaming contract (no `message.delta`/`reasoning.delta`), the approval decision enum lists all seven accepted values with semantics, and `GET /v1/sessions`, `/v1/memory*`, `/v1/tasks*` are documented. A doc-drift spec locks the documented route list to the registered routes.
109
280
 
110
- ## [0.3.0] - 2026-06-06
111
-
112
- Major capability release: the core conversation loop was ported 1:1 from the reference implementation (formalized LLM boundary, retry/backoff/fallback, degenerate-response recovery), background subagents became the default delegation path, the memory subsystem grew a pluggable backend contract with a tiny-Zep SQLite backend that is now the default, CLI gained image/file input and a scroll-native redesign, and a reference-aligned approval model (hardline floor, dangerous-pattern deny, prefix-derived rules) landed. Consolidated from `feature/subagent-view` (#48) plus #49-#58.
113
-
114
281
  ### Breaking / upgrade notes
115
282
 
116
283
  - **Default memory backend is now SQLite (tiny-Zep).** `memory.backend` now defaults to `"sqlite"` (previously `"default"`). The new backend reads/writes the `:memory_facts` table; the old `"default"` backend used the `:memories` table. On upgrade, users who were on the previous `"default"` backend and do **not** pin `memory.backend: "default"` in their config will stop reading their prior memory store — the new backend looks only at `:memory_facts`. Your old data in `:memories` is **not deleted**, just no longer read. **No automatic backfill is shipped.** To keep old recall, pin `memory.backend: "default"` in config. (Acceptable for alpha; documented here.)
data/CONTRIBUTING.md CHANGED
@@ -21,11 +21,20 @@ Run the CLI from the checkout with `bundle exec rubino <command>`.
21
21
  ## Tests
22
22
 
23
23
  ```bash
24
- bundle exec rspec # full suite
24
+ bundle exec rspec # full suite (sequential; generates the coverage report)
25
25
  bundle exec rspec path/to/file_spec.rb
26
26
  bundle exec rake # default task == spec
27
+ bundle exec rake parallel:spec # full suite across all CPU cores (no coverage report)
28
+ bundle exec rake parallel:spec[4] # ...forced to 4 workers
27
29
  ```
28
30
 
31
+ `parallel:spec` shards the suite across one process per core via the
32
+ `parallel_tests` gem; each worker is isolated by `TEST_ENV_NUMBER`
33
+ (per-worker `RUBINO_HOME`, document fixtures, and example-status file).
34
+ SimpleCov is skipped under parallel runs (the workers would race the
35
+ coverage resultset) — use the sequential `bundle exec rspec` when you need
36
+ the coverage report.
37
+
29
38
  The HTTP boundary is locked by an end-to-end contract suite under `spec/rubino/api/contract/`. When the docs and the contract suite disagree, **the contract suite is canonical** — update the docs to match.
30
39
 
31
40
  ## Lint
data/README.md CHANGED
@@ -19,7 +19,15 @@ One line, Linux and macOS (x86_64 / arm64). Installs a compatible Ruby, then the
19
19
  curl -fsSL https://raw.githubusercontent.com/Jhonnyr97/rubino-agent/main/install.sh | bash
20
20
  ```
21
21
 
22
- On **Linux** the installer fetches a precompiled Ruby via [`rv`](https://github.com/spinel-coop/rv). On **macOS**, if [Homebrew](https://brew.sh) is present it asks whether to use Homebrew (`brew install ruby`) or `rv`; without Homebrew it uses `rv` directly. Skip the prompt with `RUBINO_INSTALL_METHOD=brew` or `=rv`.
22
+ The installer supports **three** methods for getting a compatible Ruby + the gem:
23
+
24
+ - **`rv`** ([`rv`](https://github.com/spinel-coop/rv)) — fetches a precompiled Ruby into user space.
25
+ - **Homebrew** (`brew install ruby`) — offered on **macOS** when [Homebrew](https://brew.sh) is present.
26
+ - **`mise`** ([mise](https://mise.jdx.dev)) — a polyglot tool manager; installs `rubino` via its `gem:` backend and pins the latest published gem version.
27
+
28
+ On **macOS** (interactive) you're asked to pick Homebrew / `rv` / `mise`; on **Linux** (interactive) you pick `rv` / `mise` (Homebrew is offered only if `brew` is already on PATH). Skip the prompt with `RUBINO_INSTALL_METHOD=brew`, `=rv`, or `=mise`. For the **mise** method, `RUBINO_INSTALL_SCOPE=global` (default, user-wide `~/.config/mise/config.toml`) or `=local` (this directory only, `./mise.toml`) chooses the scope.
29
+
30
+ On **Debian 12 / old-glibc** systems `rv` would install a musl Ruby this glibc box can't execute; the installer detects that and **steers you from `rv` to `mise`** (precompiled, glibc-correct) so you don't land on a broken `rubino`.
23
31
 
24
32
  > **Review before you pipe.** Piping a script into your shell runs whatever it contains. Read it first:
25
33
  > ```bash
@@ -27,7 +35,7 @@ On **Linux** the installer fetches a precompiled Ruby via [`rv`](https://github.
27
35
  > less install.sh && bash install.sh
28
36
  > ```
29
37
 
30
- The installer is idempotent — safe to re-run and prints the exact `PATH` line for the `rubino` executable plus the next step.
38
+ The installer is idempotent — safe to re-run. It **persists the activation / `PATH` line to your shell rc** (`.zshrc` / `.bashrc` / `.profile`) and then runs a **fresh-shell verification gate** — it opens a clean login shell and fails loudly if `rubino` isn't on `PATH` there, instead of merely printing a hint you might miss. Opt out of any rc modification with `RUBINO_NO_MODIFY_RC=1` (the installer then prints the line for you to add yourself).
31
39
 
32
40
  **Manual install** (if you'd rather not pipe, or already manage Ruby yourself):
33
41
 
@@ -99,7 +107,7 @@ model:
99
107
 
100
108
  agent:
101
109
  max_turns: 90
102
- max_tool_iterations: 8
110
+ max_tool_iterations: 25
103
111
 
104
112
  memory:
105
113
  enabled: true
@@ -142,7 +150,7 @@ Full reference (every key, env vars, precedence): **[docs/configuration.md](docs
142
150
 
143
151
  ## Built-in tools
144
152
 
145
- The agent ships **33 built-in tools**: `read`, `summarize_file`, `write`, `edit`, `multi_edit`, `grep`, `glob`, `git`, `github`, `shell`, `shell_output`, `shell_tail`, `shell_input`, `shell_kill`, `ruby`, `run_tests`, `apply_patch`, `webfetch`, `websearch`, `question`, `todowrite`, `memory`, `session_search`, `attach_file`, `vision`, `skill`, `task`, `task_result`, `task_stop`, `ask_parent`, `steer`, `probe`, `answer_child`. Each is gated by a `tools.<key>` config flag (opt-out) and the approval model. See **[docs/tools.md](docs/tools.md)**.
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)**.
146
154
 
147
155
  ## Skills
148
156
 
@@ -189,7 +197,8 @@ These are designed-in but not fully wired yet — don't depend on them in produc
189
197
 
190
198
  ```bash
191
199
  bundle install
192
- bundle exec rspec # run tests
200
+ bundle exec rspec # run tests (sequential, with coverage)
201
+ bundle exec rake parallel:spec # run tests across all CPU cores
193
202
  bundle exec rubino doctor # verify setup
194
203
  ```
195
204
 
data/Rakefile CHANGED
@@ -6,3 +6,34 @@ require "rspec/core/rake_task"
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  task default: :spec
9
+
10
+ # Parallel test execution across CPU cores via the `parallel_tests` gem.
11
+ #
12
+ # rake parallel:spec # auto: one worker per core
13
+ # rake parallel:spec[4] # force 4 workers
14
+ #
15
+ # Each worker is its own process with a distinct TEST_ENV_NUMBER, so the
16
+ # per-process isolation already baked into spec/spec_helper.rb (RUBINO_HOME,
17
+ # document fixtures, example-status file) keeps workers from colliding.
18
+ # SimpleCov is skipped in parallel (workers would race the resultset); run the
19
+ # plain sequential `rake spec` / `bundle exec rspec` for a coverage report.
20
+ #
21
+ # Balancing: we use parallel_tests' default **filesize** grouping rather than
22
+ # runtime grouping. Runtime grouping (`--group-by runtime`) is strict — it
23
+ # aborts with RuntimeLogTooSmallError whenever the recorded log is missing an
24
+ # entry for any current spec file (i.e. the first run after ANY new spec is
25
+ # added), which makes the entrypoint brittle. The wall-clock floor here is a
26
+ # single ~70s example (agent_e2e error-retry) that cannot be split across
27
+ # workers regardless of grouping, so filesize grouping already lands the
28
+ # longest worker on essentially that floor while staying deterministic and
29
+ # never breaking on a freshly-added spec.
30
+ namespace :parallel do
31
+ desc "Run the RSpec suite in parallel across CPU cores (rake parallel:spec[N])"
32
+ task :spec, [:count] do |_t, args|
33
+ count = args[:count]
34
+ cmd = %w[bundle exec parallel_rspec]
35
+ cmd += ["-n", count.to_s] if count && !count.empty?
36
+ cmd += ["--", "spec"]
37
+ sh(*cmd)
38
+ end
39
+ end
data/docs/agents.md CHANGED
@@ -1,15 +1,22 @@
1
1
  # Agents & Subagents
2
2
 
3
- rubino has two distinct multi-agent surfaces. Only the first one ships today:
3
+ rubino has two distinct multi-agent surfaces, and **both ship today**:
4
4
 
5
5
  1. **Background subagents** (✅ shipping) — the agent delegates bounded sub-tasks
6
6
  to isolated subagent runs via its `task` tool, and you supervise them with
7
- `/agents` and `/reply`. This is the surface you will actually use.
8
- 2. **Primary-agent switching** ( not yet wired) — Tab-cycling between primary
9
- agents and `@mention` routing. The machinery exists (`Agent::Router`,
10
- `Agent::Definition`, `AgentRegistry`) but no call site passes an agent
11
- definition yet, so the default (build) agent handles every turn. See
12
- [the last section](#planned-primary-agent-switching--mentions-not-yet-wired).
7
+ `/agents` and `/reply`.
8
+ 2. **Primary-agent switching** ( shipping) — pick the primary agent that
9
+ handles your turns: `/agent <name>` (or a bare `/<name>` for a primary)
10
+ pins it for the session, **Tab** cycles through the primaries, and a one-shot
11
+ `/<name> <message>` routes a single message to any agent. The selected agent's
12
+ Definition (its system prompt and tool scope) is threaded into the runner each
13
+ turn, so the choice actually changes the model's persona/tools. See
14
+ [Primary-agent switching](#primary-agent-switching) below.
15
+
16
+ > **Channels are cleanly separated:** `@` is the **workspace file** picker
17
+ > (`@path/to/file`), and `/` is the **agent/command** channel. There are no
18
+ > `@mention` agent routes — a filename like `@explore.rb` is always a file, never
19
+ > an agent. Use `/explore`, `/plan`, etc. to reach an agent.
13
20
 
14
21
  ---
15
22
 
@@ -132,14 +139,15 @@ apply (hardline floor still enforced — see [security.md](security.md)).
132
139
  ## Built-in agent definitions
133
140
 
134
141
  These definitions exist in `Agent::AgentRegistry` today. The two *subagents*
135
- are live as `task` targets; the two *primary* agents are only reachable as the
136
- default (`build`) or via the plan **mode** (`/mode plan`), not via agent
137
- switching; the *utility* agents are internal.
142
+ are live as `task` targets; the two *primary* agents are switchable per session
143
+ (`/agent <name>`, a bare `/<name>`, or Tab see
144
+ [Primary-agent switching](#primary-agent-switching)); the *utility* agents are
145
+ internal.
138
146
 
139
147
  | Agent | Type | Access | Description |
140
148
  |-------|------|--------|-------------|
141
- | **build** | primary | Full tools | Default development agent. Handles every turn today. |
142
- | **plan** | primary | Read-only | Analysis/planning definition (the shipping read-only surface is `/mode plan`). |
149
+ | **build** | primary | Full tools | Default development agent (the registry default). |
150
+ | **plan** | primary | Read-only | Analysis/planning agent. Switch to it with `/agent plan`; `/mode plan` is the orthogonal read-only run **mode**. |
143
151
  | **explore** | subagent | Read-only | Fast codebase search and navigation (`task` target). |
144
152
  | **general** | subagent | Full tools | Complex multi-step tasks (`task` target). |
145
153
  | **compaction** | utility | None | Internal: compresses context. Hidden. |
@@ -173,18 +181,29 @@ pattern-based permission overrides (merged over the global rules by
173
181
 
174
182
  ---
175
183
 
176
- ## Planned: primary-agent switching & @mentions (not yet wired)
184
+ ## Primary-agent switching
177
185
 
178
- > **Status:** the machinery exists `Agent::Router` (@mention detection,
179
- > Tab-cycling, default routing) and the `agent_definition:` plumbing through the
180
- > runner but **no call site passes an agent definition**, so Tab and
181
- > `@explore`/`@plan`/`@general` mentions currently do nothing. Use the
182
- > background-subagent surface above for real work.
183
-
184
- The intended design: press **Tab** to cycle through primary agents, or route a
185
- single message with an `@mention`:
186
+ You choose which primary agent handles your turns. The pinned agent is a
187
+ process-level slot (`Rubino::ActiveAgent`, sibling to `Rubino::Modes`): a fresh
188
+ `rubino chat` boots on the registry default (`build`), and an explicit switch
189
+ takes effect for the rest of that process (no premature persistence). Switching
190
+ is entirely on the **slash** channel and **Tab** — there is no `@mention` agent
191
+ routing (`@` is the workspace file picker).
186
192
 
187
193
  ```
188
- you > @explore Where is the database connection configured?
189
- you > @plan How should we restructure the auth module?
194
+ you > /agent plan # pin a primary agent for the session
195
+ you > /plan # bare /<name> same, for a primary agent
196
+ you > <Tab> # cycle through the primary agents, wrapping around
197
+ you > /explore Where is the database connection configured? # one-shot route a single message
190
198
  ```
199
+
200
+ - **`/agent <name>`** (or a bare **`/<name>`** when `<name>` is a primary) pins
201
+ the agent for the session. Only **primary** agents are switchable; subagents
202
+ (`explore`/`general`) are never pinned.
203
+ - **Tab** cycles through the primary agents.
204
+ - **`/<name> <message>`** routes a single message to any agent (primary or
205
+ subagent) without changing the sticky selection.
206
+
207
+ The selected agent's Definition — its system prompt and tool scope — is threaded
208
+ into the runner on every turn, so switching actually changes the model's
209
+ persona and the tools it can call, not just a cosmetic label.
data/docs/architecture.md CHANGED
@@ -148,8 +148,8 @@ User Input
148
148
  ├─→ Commands::Executor (if /command)
149
149
  │ └─→ Render template → feed to agent
150
150
 
151
- ├─→ Agent::Router (if @mention)
152
- │ └─→ Select agent definition
151
+ ├─→ ActiveAgent (if /agent, /<name>, or Tab)
152
+ │ └─→ Select primary agent definition
153
153
 
154
154
  └─→ Interaction::Lifecycle
155
155
 
data/docs/commands.md CHANGED
@@ -13,7 +13,7 @@ Two surfaces: **CLI subcommands** (run from your shell) and **slash commands** (
13
13
  | `rubino memory SUBCOMMAND` | Manage persistent memories (`list` / `show` / `delete` / `backend`) |
14
14
  | `rubino sessions SUBCOMMAND` | Manage chat sessions |
15
15
  | `rubino jobs SUBCOMMAND` | Manage background jobs |
16
- | `rubino skills SUBCOMMAND` | Manage skills (`list` / `show` / `enable` / `disable`) |
16
+ | `rubino skills SUBCOMMAND` | Manage skills (`list` / `show` / `enable` / `disable` / `install` / `update` / `remove`) |
17
17
  | `rubino tools` | List available tools and their enabled/disabled state |
18
18
  | `rubino server` | Start the JSON API + SSE server |
19
19
  | `rubino tls-cert` | Print the agent's self-signed TLS certificate PEM (generating it if absent) |
@@ -60,7 +60,9 @@ is set.
60
60
  | `--new` | | Start a fresh session (bare `chat` resumes the last one by default) |
61
61
  | `--model` | `-m` | Override the model (e.g. `claude-sonnet-4-5`) |
62
62
  | `--provider` | | Override the provider (e.g. `anthropic`, `bedrock`) |
63
- | `--yolo` | | Skip approval prompts (equivalent to `/mode yolo`) |
63
+ | `--yolo` | | Skip approval prompts (equivalent to `/mode yolo`). Honored **only** as a CLI flag — cannot be set from config |
64
+ | `--no-yolo` | | Force fail-closed approvals even over a yolo default (the security half of [#260](#exit-codes-scripting-around-prompt--one-shot)) |
65
+ | `--add-dir` | | Add an extra allowed workspace directory write/edit can reach (repeatable) |
64
66
  | `--max-turns` | | Max tool iterations per turn |
65
67
  | `--ignore-rules` | | Skip `AGENTS.md` and context files |
66
68
 
@@ -93,6 +95,18 @@ Pasting **text** into the chat input goes through the file-backed paste pipeline
93
95
  policy along the way (a write outside the workspace boundary, a denied
94
96
  approval, a hardline-blocked command). A refusal the agent handled and
95
97
  explained is expected behavior, not an error.
98
+ - **Headless approvals fail closed (security).** A one-shot / scripted run has
99
+ no interactive session, so a tool that would otherwise prompt for approval —
100
+ a write/edit, or a shell command **not** covered by your `permissions` /
101
+ command allowlist / read-only auto-allow — is **blocked, not run**. A
102
+ single-line `blocked: <tool> needs approval but no interactive session (use
103
+ --yolo to allow, or allowlist it)` goes to stderr and the run exits
104
+ **non-zero (2)**, so automation/CI fails loudly instead of silently skipping
105
+ (or, worse, auto-executing) the action. Anything you already allowlisted, and
106
+ every read-only command, still runs unprompted. Pass `--yolo` to opt back
107
+ into full auto-execute; `--no-yolo` forces fail-closed even if a yolo default
108
+ was set. `--yolo` is honored **only** as a CLI flag — a project-local config
109
+ can never grant it.
96
110
  - It exits **non-zero** when the run itself fails: no usable credentials, the
97
111
  `--resume`/`--session` target doesn't exist or is ambiguous, or the provider
98
112
  call errors out. The reason is printed to stderr; the answer (when any) stays
@@ -129,10 +143,15 @@ rubino jobs list
129
143
  rubino jobs process # run pending jobs now (manual mode)
130
144
  rubino jobs worker # start a background worker
131
145
 
132
- rubino skills list # list skills with enabled/disabled markers
146
+ rubino skills list # list skills with enabled/disabled markers + provenance
133
147
  rubino skills show NAME # print a skill's SKILL.md body (review before enabling)
134
148
  rubino skills enable NAME # put a skill back in the index (every session)
135
149
  rubino skills disable NAME # drop a skill from the index (every session)
150
+
151
+ rubino skills install owner/repo --skill NAME # install skills from a git repo (#4)
152
+ rubino skills install --documents # anthropics/skills: pdf docx pptx xlsx
153
+ rubino skills update [NAME ...] # re-fetch from the recorded sources
154
+ rubino skills remove NAME # delete an installed skill + provenance
136
155
  ```
137
156
 
138
157
  `config get`/`config show` mask secret-named keys (`api_key`, tokens, …) on display — the file keeps the real value. See [memory.md](memory.md) for the memory backends, [jobs.md](jobs.md) for the queue/cron system and [skills.md](skills.md) for the skill model.
@@ -155,9 +174,11 @@ Type these inside `rubino chat`. Generated from `BuiltIns::DESCRIPTIONS` (drift-
155
174
  | `/compact` | Compact the context now: older turns become a summary |
156
175
  | `/export` | Write the session transcript as markdown (/export [path]) |
157
176
  | `/memory` | Inspect/search/forget what the agent remembers (show ID, backend, --all) |
177
+ | `/agent` | Switch the primary agent (/agent <name>; a bare /<name> or Tab cycles) |
158
178
  | `/agents` | List background subagents; steer/probe a running one, or view output |
159
179
  | `/tasks` | Alias for /agents |
160
180
  | `/reply` | Answer a subagent that is blocked waiting on you (ask_parent) |
181
+ | `/stop` | Stop a running subagent (/stop <id>; alias for /agents <id> --stop) |
161
182
  | `/jobs` | List the background job queue (status counts); /jobs <id> for detail |
162
183
  | `/skills` | List skills; activate one ('none' clears), or enable/disable NAME |
163
184
  | `/mcp` | List MCP servers and their tools; restart or disable one |
@@ -366,6 +387,17 @@ Custom commands live as Markdown templates in `.rubino/commands/` (project) or `
366
387
 
367
388
  `/commands` lists the available custom commands and explains how to author them. See the [README](../README.md) for the template format (`$ARGUMENTS`, YAML frontmatter).
368
389
 
390
+ ### Primary agents: `/agent`, `/<name>`, and Tab
391
+
392
+ Each turn runs under an **agent** — a persona with its own system prompt and tool scope. The built-ins are `build` (full access, the default) and `plan` (read-only analysis); `explore` and `general` are subagents you invoke one-shot. Switching the primary agent changes who answers the *next* turn:
393
+
394
+ - `/agent` lists the switchable primaries (the current one marked `▸`) and the one-shot subagents.
395
+ - `/agent <name>` — or a bare `/<name>` for a primary — **pins** that agent for the rest of the session (sticky). The active agent shows as an `agent <name>` chip in the status bar (omitted when it's the default `build`).
396
+ - **Tab** on an empty prompt cycles the primary agents (the agent counterpart of Shift+Tab's mode cycle), updating the chip live.
397
+ - `/<name> <message>` routes a **single** turn to that agent — any visible agent, primary or subagent (e.g. `/explore where is the parser`) — without disturbing your sticky pick.
398
+
399
+ Distinct from `/agents` (plural), which drills into the background `task` subagents. `@` is the file picker, so a filename like `@explore.rb` is never shadowed by an agent named `explore`; agent switching lives entirely on the slash channel and Tab.
400
+
369
401
  ### Modes
370
402
 
371
403
  `/mode` (or the `--yolo` flag) switches between the modes below. **Shift+Tab** cycles them from the prompt (default → plan → yolo), updates the mode token that LEADS the status bar under the input (dim `default`, yellow `plan`, red `yolo`), and shows a transient `mode <old> → <new>` footer. Entering `yolo` from the cycle takes a second deliberate Shift+Tab to confirm (the toast says so, and warns when running background subagents would lose their approval gates); an explicit `/mode yolo` switches directly.