rubyn-code 0.8.0 → 0.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7696c779b900fe4dca806aa73d7fcca79309ebe58a242d16e2d5309b43d44ec4
4
- data.tar.gz: 2d8d4c8905255ce11e98569f86f4c2d788c620ea30c513fd6771b3c58bb98073
3
+ metadata.gz: d43be98f4e24cc9bc78bc541ed0554731ed88985adb56ec5700d643dc4fd2ba2
4
+ data.tar.gz: d6adda167ad528fde80900dfb4c5d7114029695c4ae83f814ad2130b2f531781
5
5
  SHA512:
6
- metadata.gz: c5d2b5f15b21d7e02b2f6c2252248eb6e933337b3f4c491135f5453c1ddf4ce47c48605aaeb6e26180a38324c8effc7a149e528c794ca9391ec8c43febbc2bdb
7
- data.tar.gz: 15c9aa06c02dd8c105116b403cba0f36cb5a2b91c8690b66b6320a289011cf6f2986a308fdaacad933fbd221646de84b7f774a47709a7c8409fdc5bddca5b671
6
+ metadata.gz: 3352ad014212276c17d5a96adb3d3dcf43165dffb77b8a95d01ce22bf3fdd57786abca291c4604e937cef26b070b3d43ae791430da30d84baa49a76e753d8f28
7
+ data.tar.gz: 767c3be54cee4d94e157dd7ae73287c5306159586f3057eac199175aa0ac79aba3ef35046cc4eef6936c6f80802277bf578e5588c35b2fd8940d8aa58037c07e
data/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  <a href="https://github.com/MatthewSuttles/rubyn-code/actions/workflows/ci.yml"><img src="https://github.com/MatthewSuttles/rubyn-code/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
16
16
  </p>
17
17
 
18
- Refactor controllers, generate idiomatic RSpec, catch N+1 queries, review code for anti-patterns, and build entire features — all context-aware with your schema, routes, and specs. Powered by Claude Opus 4.8, running on your existing Claude subscription.
18
+ Refactor controllers, generate idiomatic RSpec, catch N+1 queries, review code for anti-patterns, and build entire features — all context-aware with your schema, routes, and specs. Powered by Claude Opus 5, running on your existing Claude subscription.
19
19
 
20
20
  <img width="1230" height="280" alt="image" src="https://github.com/user-attachments/assets/14e07ce8-def0-4a8f-ac89-46661361a4eb" />
21
21
 
@@ -31,7 +31,7 @@ Refactor controllers, generate idiomatic RSpec, catch N+1 queries, review code f
31
31
  - [Quick Start](#quick-start)
32
32
  - [What Can Rubyn Do?](#what-can-rubyn-do)
33
33
  - [VS Code Extension](#vs-code-extension)
34
- - [29 Built-in Tools](#29-built-in-tools)
34
+ - [32 Built-in Tools](#32-built-in-tools)
35
35
  - [MCP — External Tool Servers](#mcp--external-tool-servers)
36
36
  - [Codebase Indexing](#codebase-indexing)
37
37
  - [112 Best Practice Skills](#112-best-practice-skills)
@@ -74,8 +74,8 @@ Refactor controllers, generate idiomatic RSpec, catch N+1 queries, review code f
74
74
  Phase 4 ships nine parity features with Claude Code:
75
75
 
76
76
  - **Extended thinking** — `/think <budget>` toggles per-session reasoning; the Anthropic adapter emits `thinking: {type: 'adaptive'}` on Claude 4.6+ models (`{type: 'enabled', budget_tokens}` on older ones).
77
- - **Reasoning effort** — `/effort <low|medium|high|xhigh|max>` sets the request-level reasoning depth; the Anthropic adapter emits `output_config: {effort}` on the wire. GA on Claude 4.6+ (Opus 4.6/4.7/4.8, Sonnet 4.6/5, Fable 5); `xhigh` needs Opus 4.7+ / Sonnet 5 / Fable 5. Model default is `high`.
78
- - **Task budgets (beta)** — the agent loop's remaining token budget is sent as `output_config: {task_budget: {type: 'tokens', total}}` on supported models (Fable 5, Sonnet 5, Opus 4.7/4.8), advisory pacing only — `max_tokens` stays the enforced per-response cap.
77
+ - **Reasoning effort** — `/effort <low|medium|high|xhigh|max>` sets the request-level reasoning depth; the Anthropic adapter emits `output_config: {effort}` on the wire. GA on Claude 4.6+ (Opus 4.6/4.7/4.8/5, Sonnet 4.6/5, Fable 5); `xhigh` needs Opus 4.7+ / Sonnet 5 / Fable 5. Model default is `high`.
78
+ - **Task budgets (beta)** — the agent loop's remaining token budget is sent as `output_config: {task_budget: {type: 'tokens', total}}` on supported models (Fable 5, Sonnet 5, Opus 5, Opus 4.7/4.8), advisory pacing only — `max_tokens` stays the enforced per-response cap.
79
79
  - **Image / vision input** — `@chart.png` (and `.jpg` / `.jpeg` / `.gif` / `.webp`) becomes a real image content block attached to the user turn; Anthropic and OpenAI each emit their native shape.
80
80
  - **TodoWrite live checklist** — `TodoWrite` tool; the checklist refreshes above the spinner on every tool result so you see in-turn progress at a glance.
81
81
  - **Custom-command frontmatter** — `argument-hint`, `allowed-tools`, and `model:` keys in `~/.rubyn-code/commands/*.md`; the loop honors the per-prompt tool restriction and model override.
@@ -215,7 +215,7 @@ Rubyn Code includes a VS Code extension that provides a full IDE experience with
215
215
  - Full session management — resume, list, fork, and reset conversations
216
216
  - Structured code review feedback with severity ratings
217
217
  - IDE config get/set for persistent settings
218
- - All 29 tools available, including MCP tools
218
+ - All 32 tools available, including MCP tools
219
219
 
220
220
  **Permission modes:**
221
221
 
@@ -226,19 +226,19 @@ Rubyn Code includes a VS Code extension that provides a full IDE experience with
226
226
 
227
227
  The extension communicates over 19 RPC methods: `initialize`, `prompt`, `cancel`, `review`, `approveToolUse`, `acceptEdit`, `session/*`, `config/*`, `models/list`, `plan/propose`, `plan/interview/*` (chat-resident [megaplan](#megaplan--phased-planning)), `recover_ci`, and `shutdown`.
228
228
 
229
- ## 29 Built-in Tools
229
+ ## 32 Built-in Tools
230
230
 
231
231
  | Category | Tools |
232
232
  |----------|-------|
233
233
  | **File I/O** | `read_file`, `write_file`, `edit_file` |
234
- | **Search** | `glob`, `grep` |
234
+ | **Search** | `glob`, `grep`, `code_graph` |
235
235
  | **Execution** | `bash` (sandboxed, dangerous commands blocked) |
236
236
  | **Web** | `web_search`, `web_fetch` |
237
237
  | **Git** | `git_status`, `git_diff`, `git_log`, `git_commit` |
238
238
  | **Rails** | `rails_generate`, `db_migrate`, `run_specs`, `bundle_install`, `bundle_add` |
239
239
  | **Review** | `review_pr` (diff-based best practice code review) |
240
- | **Agents** | `spawn_agent`, `spawn_teammate`, `background_run` |
241
- | **Context** | `compact`, `load_skill`, `task` |
240
+ | **Agents** | `spawn_agent`, `spawn_teammate`, `background_run`, `phone_a_friend` |
241
+ | **Context** | `compact`, `load_skill`, `task`, `todo_write` |
242
242
  | **Memory** | `memory_search`, `memory_write` |
243
243
  | **Teams** | `send_message`, `read_inbox` |
244
244
  | **Interactive** | `ask_user` (ask clarifying questions mid-task) |
@@ -518,6 +518,18 @@ rubyn > Send alice a message to write specs for the User model
518
518
 
519
519
  Teammates run in background threads with their own agent loop and mailbox.
520
520
 
521
+ ### Phone a Friend (second opinion)
522
+
523
+ When the agent is stuck or weighing two approaches, the `phone_a_friend` tool asks a *different* model for a one-shot second opinion. The friend is picked for perspective diversity: the top-tier model of another configured provider whose API key is present (a genuinely different model family), falling back to the active provider's top tier. The friend gets no tools and no conversation history — only the question and whatever context the agent chooses to send — and its answer comes back as plain text labeled with the provider and model that gave it.
524
+
525
+ ```
526
+ rubyn > I keep going back and forth on STI vs polymorphic here...
527
+
528
+ [tool] phone_a_friend
529
+ ## Second Opinion — openai/gpt-5.4
530
+ Commit to the polymorphic association. The key reason: ...
531
+ ```
532
+
521
533
  ## GOLEM — Autonomous Daemon
522
534
 
523
535
  GOLEM is an always-on autonomous agent that claims tasks from a queue and works through them independently. It runs a full agent loop per task with access to all tools, MCP servers, and memory.
@@ -691,7 +703,7 @@ Rubyn snapshots a checkpoint at the start of every turn — capturing the conver
691
703
  | 2 | Token file | `~/.rubyn-code/tokens.yml` |
692
704
  | 3 | Environment | `export ANTHROPIC_API_KEY=sk-ant-...` |
693
705
 
694
- Works with Claude Pro, Max, Team, and Enterprise. Default model: **Claude Opus 4.6**.
706
+ Works with Claude Pro, Max, Team, and Enterprise. Default model: **Claude Opus 5**.
695
707
 
696
708
  ### OpenAI
697
709
 
@@ -709,7 +721,7 @@ Add a provider and its API key in one command:
709
721
  /provider add groq https://api.groq.com/openai/v1 --key gsk-xxx --models llama-3.3-70b
710
722
 
711
723
  # For Anthropic-format proxies (e.g., Bedrock, custom gateways)
712
- /provider add my-proxy https://proxy.example.com/v1 --format anthropic --key sk-xxx --models claude-sonnet-4-6
724
+ /provider add my-proxy https://proxy.example.com/v1 --format anthropic --key sk-xxx --models claude-sonnet-5
713
725
 
714
726
  # Update a key later
715
727
  /provider set-key groq gsk-new-key
@@ -740,7 +752,7 @@ providers:
740
752
  base_url: https://proxy.example.com/v1
741
753
  env_key: PROXY_API_KEY
742
754
  models:
743
- top: claude-sonnet-4-6
755
+ top: claude-sonnet-5
744
756
  ```
745
757
 
746
758
  Then switch with `/model groq:llama-3.3-70b`.
@@ -767,7 +779,7 @@ Local providers (Ollama, LM Studio) running on `localhost`/`127.0.0.1` don't req
767
779
  │ Layer 5: Skills (112 best practice docs, on-demand loading) │
768
780
  │ Layer 4: Context Management (3-layer compression pipeline) │
769
781
  │ Layer 3: Permissions (tiered access + deny lists + hooks) │
770
- │ Layer 2: Tool System (29 tools, dispatch map registry) │
782
+ │ Layer 2: Tool System (32 tools, dispatch map registry) │
771
783
  │ Layer 1: THE AGENT LOOP (while tool_use → execute → repeat) │
772
784
  └──────────────────────────────────────────────────────────────┘
773
785
  ```
@@ -780,7 +792,7 @@ These must match a provider defined in the `providers` section (or a built-in li
780
792
  ```yaml
781
793
  # ~/.rubyn-code/config.yml (global)
782
794
  provider: anthropic # default provider on startup
783
- model: claude-opus-4-6 # default model on startup
795
+ model: claude-opus-5 # default model on startup
784
796
  permission_mode: allow_read
785
797
  session_budget: 5.00
786
798
  daily_budget: 10.00
@@ -798,15 +810,15 @@ Rubyn can automatically route tasks to different AI models based on complexity.
798
810
  ```yaml
799
811
  # ~/.rubyn-code/config.yml
800
812
  provider: anthropic
801
- model: claude-opus-4-6
813
+ model: claude-opus-5
802
814
 
803
815
  providers:
804
816
  anthropic:
805
817
  env_key: ANTHROPIC_API_KEY
806
818
  models:
807
819
  cheap: claude-haiku-4-5 # file search, git ops, formatting
808
- mid: claude-sonnet-4-6 # code gen, specs, refactors, reviews
809
- top: claude-opus-4-6 # architecture, security, complex work
820
+ mid: claude-sonnet-5 # code gen, specs, refactors, reviews
821
+ top: claude-opus-5 # architecture, security, complex work
810
822
 
811
823
  openai:
812
824
  env_key: OPENAI_API_KEY
@@ -838,8 +850,8 @@ providers:
838
850
  | Tier | Task types | Default model |
839
851
  |------|-----------|---------------|
840
852
  | **cheap** | File search, git ops, formatting, summaries | `claude-haiku-4-5` |
841
- | **mid** | Code generation, specs, refactors, code review, bug fixes | `claude-sonnet-4-6` |
842
- | **top** | Architecture, security review, complex refactors, planning | `claude-opus-4-6` |
853
+ | **mid** | Code generation, specs, refactors, code review, bug fixes | `claude-sonnet-5` |
854
+ | **top** | Architecture, security review, complex refactors, planning | `claude-opus-5` |
843
855
 
844
856
  You can also set custom pricing per model so `/cost` reports accurate spending for third-party providers.
845
857
 
@@ -1,6 +1,6 @@
1
1
  # Layer 2: Tools
2
2
 
3
- 33 built-in tools that Claude can invoke. The extensibility surface of the system.
3
+ 34 built-in tools that Claude can invoke. The extensibility surface of the system.
4
4
 
5
5
  ## Core Classes
6
6
 
@@ -26,7 +26,7 @@
26
26
  | Git | `git_commit`, `git_diff`, `git_log`, `git_status` |
27
27
  | Web | `web_search`, `web_fetch` |
28
28
  | Memory | `memory_search`, `memory_write` |
29
- | Agents | `spawn_agent`, `spawn_teammate`, `send_message`, `read_inbox` |
29
+ | Agents | `spawn_agent`, `spawn_teammate`, `send_message`, `read_inbox`, `phone_a_friend` |
30
30
  | Meta | `compact`, `load_skill`, `task`, `review_pr` |
31
31
 
32
32
  ## Adding a Tool
@@ -85,6 +85,8 @@ module RubynCode
85
85
  when 'spawn_agent', 'spawn_teammate'
86
86
  inject_agent_deps(tool)
87
87
  tool.db = @db if tool_name == 'spawn_teammate' && tool.respond_to?(:db=)
88
+ when 'phone_a_friend'
89
+ tool.llm_client = @llm_client if tool.respond_to?(:llm_client=)
88
90
  when 'background_run'
89
91
  tool.background_worker = @background_worker if tool.respond_to?(:background_worker=)
90
92
  when 'ask_user'
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'registry'
5
+
6
+ module RubynCode
7
+ module Tools
8
+ # Ask a different model for a second opinion. The "friend" is chosen to
9
+ # maximize perspective diversity: prefer the top-tier model of another
10
+ # configured provider (a genuinely different model family), and fall back
11
+ # to the active provider's top-tier model when no other provider has an
12
+ # API key available. One-shot, no tools, returns plain text.
13
+ class PhoneAFriend < Base
14
+ TOOL_NAME = 'phone_a_friend'
15
+ DESCRIPTION = 'Ask a different model for a second opinion when you are stuck, ' \
16
+ 'weighing two approaches, or want your reasoning sanity-checked. ' \
17
+ 'Sends one question (plus optional context) to another model — ' \
18
+ 'a different provider when one is configured, otherwise the ' \
19
+ 'top-tier model of the current provider — and returns its answer. ' \
20
+ 'The friend has no tools and sees nothing except what you send.'
21
+ PARAMETERS = {
22
+ question: {
23
+ type: :string,
24
+ required: true,
25
+ description: 'The question to ask. Be specific about what kind of answer you ' \
26
+ 'need (a decision, a review of reasoning, an alternative approach).'
27
+ },
28
+ context: {
29
+ type: :string,
30
+ required: false,
31
+ description: 'Relevant code, error output, or background. The friend sees ' \
32
+ 'only this — include everything needed to answer well.'
33
+ }
34
+ }.freeze
35
+ RISK_LEVEL = :external
36
+
37
+ SYSTEM_PROMPT = <<~PROMPT
38
+ You are giving a second opinion to another AI coding agent that is working
39
+ inside a project and has hit a question it wants an outside perspective on.
40
+ You cannot see the project — only what the agent sent you. Answer directly
41
+ and concretely: commit to a recommendation, explain the key reason, and
42
+ flag anything important the agent may have missed. If the provided context
43
+ is insufficient to answer well, say exactly what is missing.
44
+ PROMPT
45
+
46
+ # Injected by the Executor (active client) and overridable in tests.
47
+ attr_writer :llm_client, :client_factory
48
+
49
+ def execute(question:, context: nil)
50
+ return 'phone_a_friend: no LLM client available.' unless @llm_client
51
+
52
+ provider, model = pick_friend
53
+ response = call_friend(provider, model, build_message(question, context))
54
+ answer = extract_text(response)
55
+ return "phone_a_friend: #{provider}/#{model} returned no text." if answer.empty?
56
+
57
+ "## Second Opinion — #{provider}/#{model}\n\n#{answer}"
58
+ rescue StandardError => e
59
+ friend = provider ? "#{provider}/#{model}" : 'friend'
60
+ "phone_a_friend: call to #{friend} failed: #{e.message}"
61
+ end
62
+
63
+ def self.summarize(_output, _args)
64
+ 'asked another model for a second opinion'
65
+ end
66
+
67
+ private
68
+
69
+ # Returns [provider, model]. Prefers another provider whose API key is
70
+ # present; otherwise escalates to the active provider's top tier.
71
+ def pick_friend
72
+ active = @llm_client.provider_name
73
+ other = (known_providers - [active]).find { |p| key_present?(p) }
74
+ provider = other || active
75
+ [provider, top_model_for(provider)]
76
+ end
77
+
78
+ def call_friend(provider, model, message)
79
+ client = provider == @llm_client.provider_name ? @llm_client : friend_client(provider, model)
80
+ client.chat(
81
+ messages: [{ role: 'user', content: message }],
82
+ tools: nil,
83
+ system: SYSTEM_PROMPT,
84
+ model: model
85
+ )
86
+ end
87
+
88
+ def friend_client(provider, model)
89
+ factory = @client_factory || ->(prov, mod) { LLM::Client.new(provider: prov, model: mod) }
90
+ factory.call(provider, model)
91
+ end
92
+
93
+ def build_message(question, context)
94
+ return question if context.to_s.empty?
95
+
96
+ "#{question}\n\n## Context\n\n#{context}"
97
+ end
98
+
99
+ def known_providers
100
+ configured = settings.to_h['providers']
101
+ names = configured.is_a?(Hash) ? configured.keys : []
102
+ (names + Config::Settings::DEFAULT_PROVIDER_MODELS.keys).uniq
103
+ end
104
+
105
+ def key_present?(provider)
106
+ env_key = provider_setting(provider, 'env_key')
107
+ env_key ? !ENV[env_key].to_s.empty? : false
108
+ end
109
+
110
+ def top_model_for(provider)
111
+ config_models = provider_setting(provider, 'models')
112
+ model = config_models['top'] || config_models.values.last if config_models.is_a?(Hash)
113
+ model || @llm_client.model
114
+ end
115
+
116
+ # Reads a provider key from config.yml, falling back to the built-in
117
+ # provider defaults for anthropic/openai.
118
+ def provider_setting(provider, key)
119
+ value = settings.provider_config(provider)&.fetch(key, nil)
120
+ value || Config::Settings::DEFAULT_PROVIDER_MODELS.dig(provider, key)
121
+ end
122
+
123
+ def settings
124
+ @settings ||= Config::Settings.new
125
+ end
126
+
127
+ def extract_text(response)
128
+ content = response.respond_to?(:content) ? Array(response.content) : []
129
+ content.select { |b| b.respond_to?(:type) && b.type == 'text' }.map(&:text).join("\n")
130
+ end
131
+ end
132
+
133
+ Registry.register(PhoneAFriend)
134
+ end
135
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubynCode
4
- VERSION = '0.8.0'
4
+ VERSION = '0.9.0'
5
5
  end
data/lib/rubyn_code.rb CHANGED
@@ -107,6 +107,7 @@ module RubynCode
107
107
  autoload :ReadInbox, 'rubyn_code/tools/read_inbox'
108
108
  autoload :ReviewPr, 'rubyn_code/tools/review_pr'
109
109
  autoload :SpawnAgent, 'rubyn_code/tools/spawn_agent'
110
+ autoload :PhoneAFriend, 'rubyn_code/tools/phone_a_friend'
110
111
  autoload :BackgroundRun, 'rubyn_code/tools/background_run'
111
112
  autoload :WebSearch, 'rubyn_code/tools/web_search'
112
113
  autoload :WebFetch, 'rubyn_code/tools/web_fetch'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyn-code
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fadedmaturity
@@ -448,6 +448,7 @@ files:
448
448
  - lib/rubyn_code/tools/memory_search.rb
449
449
  - lib/rubyn_code/tools/memory_write.rb
450
450
  - lib/rubyn_code/tools/output_compressor.rb
451
+ - lib/rubyn_code/tools/phone_a_friend.rb
451
452
  - lib/rubyn_code/tools/rails_generate.rb
452
453
  - lib/rubyn_code/tools/read_file.rb
453
454
  - lib/rubyn_code/tools/read_inbox.rb