rubyn-code 0.7.0 → 0.8.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -1
  3. data/lib/rubyn_code/agent/conversation.rb +11 -1
  4. data/lib/rubyn_code/agent/dynamic_tool_schema.rb +1 -1
  5. data/lib/rubyn_code/agent/llm_caller.rb +5 -1
  6. data/lib/rubyn_code/agent/loop.rb +57 -3
  7. data/lib/rubyn_code/agent/response_parser.rb +8 -0
  8. data/lib/rubyn_code/agent/system_prompt_builder.rb +3 -0
  9. data/lib/rubyn_code/agent/tool_processor.rb +10 -0
  10. data/lib/rubyn_code/autonomous/daemon.rb +1 -1
  11. data/lib/rubyn_code/cli/commands/context.rb +27 -0
  12. data/lib/rubyn_code/cli/commands/custom_command.rb +44 -2
  13. data/lib/rubyn_code/cli/commands/custom_loader.rb +36 -5
  14. data/lib/rubyn_code/cli/commands/effort.rb +47 -0
  15. data/lib/rubyn_code/cli/commands/export.rb +174 -0
  16. data/lib/rubyn_code/cli/commands/mcp.rb +32 -8
  17. data/lib/rubyn_code/cli/commands/resume.rb +97 -26
  18. data/lib/rubyn_code/cli/commands/think.rb +47 -0
  19. data/lib/rubyn_code/cli/first_run.rb +1 -1
  20. data/lib/rubyn_code/cli/mention_expander.rb +19 -0
  21. data/lib/rubyn_code/cli/repl.rb +31 -1
  22. data/lib/rubyn_code/cli/repl_commands.rb +1 -1
  23. data/lib/rubyn_code/cli/repl_setup.rb +8 -6
  24. data/lib/rubyn_code/config/defaults.rb +2 -1
  25. data/lib/rubyn_code/config/schema.json +5 -0
  26. data/lib/rubyn_code/config/settings.rb +4 -2
  27. data/lib/rubyn_code/context/auto_compact.rb +1 -1
  28. data/lib/rubyn_code/context/manual_compact.rb +1 -1
  29. data/lib/rubyn_code/index/codebase_index.rb +64 -3
  30. data/lib/rubyn_code/index/prism_extractor.rb +82 -0
  31. data/lib/rubyn_code/learning/injector.rb +1 -2
  32. data/lib/rubyn_code/llm/adapters/anthropic.rb +107 -17
  33. data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +13 -0
  34. data/lib/rubyn_code/llm/adapters/base.rb +2 -1
  35. data/lib/rubyn_code/llm/adapters/openai.rb +1 -1
  36. data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +21 -0
  37. data/lib/rubyn_code/llm/client.rb +16 -3
  38. data/lib/rubyn_code/llm/image_reader.rb +60 -0
  39. data/lib/rubyn_code/llm/message_builder.rb +21 -1
  40. data/lib/rubyn_code/llm/model_router.rb +4 -4
  41. data/lib/rubyn_code/mcp/discovery.rb +93 -0
  42. data/lib/rubyn_code/memory/session_persistence.rb +1 -1
  43. data/lib/rubyn_code/observability/cost_calculator.rb +6 -3
  44. data/lib/rubyn_code/protocols/RUBYN.md +0 -3
  45. data/lib/rubyn_code/tasks/models.rb +0 -16
  46. data/lib/rubyn_code/teams/teammate.rb +0 -15
  47. data/lib/rubyn_code/tools/RUBYN.md +2 -2
  48. data/lib/rubyn_code/tools/bash.rb +3 -3
  49. data/lib/rubyn_code/tools/code_graph.rb +134 -0
  50. data/lib/rubyn_code/tools/executor.rb +4 -1
  51. data/lib/rubyn_code/tools/todo_store.rb +55 -0
  52. data/lib/rubyn_code/tools/todo_write.rb +88 -0
  53. data/lib/rubyn_code/version.rb +1 -1
  54. data/lib/rubyn_code.rb +13 -8
  55. data/skills/rubyn_self_test.md +140 -0
  56. metadata +10 -7
  57. data/lib/rubyn_code/context/context_budget.rb +0 -183
  58. data/lib/rubyn_code/context/schema_filter.rb +0 -64
  59. data/lib/rubyn_code/learning/shortcut.rb +0 -95
  60. data/lib/rubyn_code/llm/adapters/token_caching.rb +0 -54
  61. data/lib/rubyn_code/llm/streaming.rb +0 -10
  62. data/lib/rubyn_code/protocols/plan_approval.rb +0 -72
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'rubyn_code/mcp/config'
5
+
6
+ module RubynCode
7
+ module MCP
8
+ # Discovers MCP server definitions from a project's `.mcp.json` file
9
+ # (the Claude Code–style layout at the project root) and merges them
10
+ # with the user-level `.rubyn-code/mcp.json` definitions parsed by
11
+ # `MCP::Config`.
12
+ #
13
+ # Project-level entries are tagged `source: :project` so /mcp can
14
+ # distinguish them from user-level entries.
15
+ module Discovery
16
+ PROJECT_CONFIG_FILENAME = '.mcp.json'
17
+
18
+ Entry = Data.define(:name, :command, :args, :env, :url, :source)
19
+
20
+ module_function
21
+
22
+ # @param project_root [String, nil] project root (nil => nothing discovered)
23
+ # @return [Array<Entry>]
24
+ def discover(project_root)
25
+ user = MCP::Config.load(project_root.to_s).map { |cfg| to_entry(cfg, :user) }
26
+ proj = load_project(project_root)
27
+ user + proj
28
+ end
29
+
30
+ # @param project_root [String, nil]
31
+ # @return [Array<Entry>]
32
+ def load_project(project_root)
33
+ return [] if project_root.to_s.empty?
34
+
35
+ path = File.join(project_root, PROJECT_CONFIG_FILENAME)
36
+ return [] unless File.exist?(path)
37
+
38
+ data = JSON.parse(File.read(path))
39
+ Array(data['mcpServers']).filter_map do |name, server_def|
40
+ build_entry(name, server_def, :project)
41
+ end
42
+ rescue JSON::ParserError => e
43
+ RubynCode::Debug.warn("[MCP::Discovery] Failed to parse #{path}: #{e.message}")
44
+ []
45
+ rescue SystemCallError => e
46
+ RubynCode::Debug.warn("[MCP::Discovery] Could not read #{path}: #{e.message}")
47
+ []
48
+ end
49
+
50
+ # @return [Array<Entry>] entries that the local runner can start right now
51
+ def stdio_servers(entries)
52
+ entries.reject { |e| e.command.to_s.empty? }
53
+ end
54
+
55
+ # @return [Array<Entry>] entries that need a network protocol (deferred)
56
+ def remote_servers(entries)
57
+ entries.select { |e| e.command.to_s.empty? && !e.url.to_s.empty? }
58
+ end
59
+
60
+ def to_entry(cfg, source)
61
+ Entry.new(
62
+ name: cfg[:name],
63
+ command: cfg[:command],
64
+ args: cfg[:args],
65
+ env: cfg[:env],
66
+ url: cfg[:url],
67
+ source: source
68
+ )
69
+ end
70
+
71
+ def build_entry(name, server_def, source)
72
+ return nil unless server_def.is_a?(Hash)
73
+
74
+ command = server_def['command'].to_s
75
+ url = server_def['url'].to_s
76
+
77
+ if command.empty? && url.empty?
78
+ RubynCode::Debug.warn("[MCP::Discovery] Skipping #{name}: no command or url")
79
+ return nil
80
+ end
81
+
82
+ Entry.new(
83
+ name: name,
84
+ command: command,
85
+ args: Array(server_def['args']),
86
+ env: server_def['env'].is_a?(Hash) ? server_def['env'].transform_keys(&:to_s) : {},
87
+ url: url,
88
+ source: source
89
+ )
90
+ end
91
+ end
92
+ end
93
+ end
@@ -303,7 +303,7 @@ module RubynCode
303
303
  CREATE TABLE IF NOT EXISTS messages (
304
304
  id INTEGER PRIMARY KEY AUTOINCREMENT,
305
305
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
306
- role TEXT NOT NULL CHECK(role IN ('system','user','assistant')),
306
+ role TEXT NOT NULL CHECK(role IN ('system','user','assistant','tool','function')),
307
307
  content TEXT,
308
308
  tool_calls TEXT,
309
309
  tool_use_id TEXT,
@@ -9,12 +9,15 @@ module RubynCode
9
9
  module CostCalculator
10
10
  # Per-million-token rates: { model_prefix => [input_rate, output_rate] }
11
11
  PRICING = {
12
- # Anthropic — Claude 5 / 4.8 / 4.6
12
+ # Anthropic — Claude 5 / 4.8 / 4.7 / 4.6
13
13
  'claude-fable-5' => [10.00, 50.00],
14
+ 'claude-opus-5' => [5.00, 25.00],
14
15
  'claude-opus-4-8' => [5.00, 25.00],
15
- 'claude-haiku-4-5' => [1.00, 5.00],
16
+ 'claude-opus-4-7' => [5.00, 25.00],
17
+ 'claude-opus-4-6' => [5.00, 25.00],
18
+ 'claude-sonnet-5' => [3.00, 15.00],
16
19
  'claude-sonnet-4-6' => [3.00, 15.00],
17
- 'claude-opus-4-6' => [15.00, 75.00],
20
+ 'claude-haiku-4-5' => [1.00, 5.00],
18
21
  # OpenAI — GPT-5.4
19
22
  'gpt-5.4' => [2.50, 10.00],
20
23
  'gpt-5.4-mini' => [0.15, 0.60],
@@ -7,8 +7,5 @@ Safety and coordination protocols for agent lifecycle.
7
7
  - **`ShutdownHandshake`** — Graceful shutdown. Waits for the current tool call to complete,
8
8
  saves conversation state, and cleans up resources.
9
9
 
10
- - **`PlanApproval`** — When the agent proposes a multi-step plan, this prompts the user
11
- for approval before execution. Shows the plan, waits for yes/no/edit.
12
-
13
10
  - **`InterruptHandler`** — Traps SIGINT (Ctrl+C). First interrupt cancels the current
14
11
  operation. Second interrupt within 2 seconds triggers shutdown.
@@ -11,22 +11,6 @@ module RubynCode
11
11
  def completed? = status == 'completed'
12
12
  def blocked? = status == 'blocked'
13
13
  def failed? = status == 'failed'
14
-
15
- def to_h
16
- {
17
- id: id,
18
- session_id: session_id,
19
- title: title,
20
- description: description,
21
- status: status,
22
- priority: priority,
23
- owner: owner,
24
- result: result,
25
- metadata: metadata,
26
- created_at: created_at,
27
- updated_at: updated_at
28
- }
29
- end
30
14
  end
31
15
  end
32
16
  end
@@ -21,21 +21,6 @@ module RubynCode
21
21
 
22
22
  # @return [Boolean] true if this teammate was not spawned by another agent
23
23
  def root? = parent_agent_id.nil?
24
-
25
- # @return [Hash]
26
- def to_h
27
- {
28
- id: id,
29
- name: name,
30
- role: role,
31
- persona: persona,
32
- model: model,
33
- status: status,
34
- parent_agent_id: parent_agent_id,
35
- metadata: metadata,
36
- created_at: created_at
37
- }
38
- end
39
24
  end
40
25
  end
41
26
  end
@@ -1,6 +1,6 @@
1
1
  # Layer 2: Tools
2
2
 
3
- 32 built-in tools that Claude can invoke. The extensibility surface of the system.
3
+ 33 built-in tools that Claude can invoke. The extensibility surface of the system.
4
4
 
5
5
  ## Core Classes
6
6
 
@@ -20,7 +20,7 @@
20
20
 
21
21
  | Category | Tools |
22
22
  |----------|-------|
23
- | File I/O | `read_file`, `write_file`, `edit_file`, `glob`, `grep` |
23
+ | File I/O | `read_file`, `write_file`, `edit_file`, `glob`, `grep`, `code_graph` |
24
24
  | Shell | `bash`, `background_run` |
25
25
  | Rails | `rails_generate`, `db_migrate`, `run_specs`, `bundle_install`, `bundle_add` |
26
26
  | Git | `git_commit`, `git_diff`, `git_log`, `git_status` |
@@ -43,9 +43,9 @@ module RubynCode
43
43
  env = ENV.to_h.dup
44
44
 
45
45
  env.each_key do |key|
46
- if Config::Defaults::SCRUB_ENV_VARS.any? { |sensitive| key.upcase.include?(sensitive) }
47
- env[key] = '[SCRUBBED]'
48
- end
46
+ # String#[] instead of #include?: Style/ArrayIntersect misfires on
47
+ # the any?+include? shape even though key is a String.
48
+ env[key] = '[SCRUBBED]' if Config::Defaults::SCRUB_ENV_VARS.any? { |sensitive| key.upcase[sensitive] }
49
49
  end
50
50
 
51
51
  env
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'registry'
5
+
6
+ module RubynCode
7
+ module Tools
8
+ # Codegraph-style exploration over the persistent codebase index: one
9
+ # call answers "where is X and how is it wired?" with the verbatim
10
+ # line-numbered source of matching symbols plus their callers, callees,
11
+ # and affected files — replacing a grep + read_file loop.
12
+ class CodeGraph < Base
13
+ TOOL_NAME = 'code_graph'
14
+ DESCRIPTION = 'Explores the codebase knowledge graph. Given symbol names or keywords, returns the ' \
15
+ 'matching definitions with verbatim line-numbered source, the methods that call them, ' \
16
+ 'the methods they call, and the affected files (including specs). Prefer this over ' \
17
+ 'grep/read_file when locating or understanding code — one call replaces a search loop.'
18
+ PARAMETERS = {
19
+ query: { type: :string, required: true,
20
+ description: 'Symbol names or keywords, e.g. "build_request_body" or "task budget"' },
21
+ max_symbols: { type: :integer, required: false, default: 5,
22
+ description: 'Maximum matching symbols to expand (default 5)' }
23
+ }.freeze
24
+ RISK_LEVEL = :read
25
+ REQUIRES_CONFIRMATION = false
26
+
27
+ MAX_SOURCE_LINES = 60
28
+ MAX_RELATED = 15
29
+ MATCHABLE_TYPES = %w[class module model controller service concern method].freeze
30
+
31
+ def self.summarize(output, args)
32
+ query = args['query'] || args[:query] || ''
33
+ count = output.to_s.scan(/^## /).size
34
+ "code_graph #{query} (#{count} symbols)"
35
+ end
36
+
37
+ def execute(query:, max_symbols: 5)
38
+ index = Index::CodebaseIndex.new(project_root: project_root)
39
+ index.load_or_build!
40
+
41
+ matches = ranked_matches(index, query, max_symbols)
42
+ return "No symbols matching '#{query}' in the codebase index." if matches.empty?
43
+
44
+ sections = matches.map { |node| render_symbol(index, node) }
45
+ truncate(sections.join("\n\n"))
46
+ end
47
+
48
+ private
49
+
50
+ def ranked_matches(index, query, limit)
51
+ terms = query.to_s.downcase.split(/\W+/).reject(&:empty?)
52
+ return [] if terms.empty?
53
+
54
+ scored = index.nodes.filter_map do |node|
55
+ next unless MATCHABLE_TYPES.include?(node['type'])
56
+
57
+ score = score_node(node, terms)
58
+ [score, node] if score.positive?
59
+ end
60
+
61
+ scored.sort_by { |score, node| [-score, node['file'].to_s] }
62
+ .map(&:last)
63
+ .uniq { |n| [n['name'], n['file'], n['line']] }
64
+ .first(limit)
65
+ end
66
+
67
+ def score_node(node, terms)
68
+ name = node['name'].to_s.downcase
69
+ file = node['file'].to_s.downcase
70
+ score = terms.sum do |term|
71
+ if name == term then 100
72
+ elsif name.include?(term) then 40
73
+ elsif file.include?(term) then 10
74
+ else 0
75
+ end
76
+ end
77
+ # A name hitting every query term beats a single exact-term match.
78
+ score += 50 if terms.size > 1 && terms.all? { |t| name.include?(t) }
79
+ score
80
+ end
81
+
82
+ def render_symbol(index, node)
83
+ header = [node['owner'], node['name']].reject { |p| p.nil? || p.empty? }.join('#')
84
+ lines = ["## #{header} (#{node['file']}:#{node['line']})"]
85
+ lines << source_slice(node)
86
+ append_call_info(lines, index, node) if node['type'] == 'method'
87
+ append_affected_files(lines, index, node)
88
+ lines.join("\n")
89
+ end
90
+
91
+ def source_slice(node)
92
+ path = File.join(project_root, node['file'].to_s)
93
+ return '(source unavailable)' unless File.file?(path)
94
+
95
+ start = [node['line'].to_i, 1].max
96
+ finish = (node['end_line'] || (start + MAX_SOURCE_LINES)).to_i
97
+ truncated = finish > start + MAX_SOURCE_LINES
98
+ finish = start + MAX_SOURCE_LINES if truncated
99
+
100
+ slice = File.readlines(path)[(start - 1)..(finish - 1)] || []
101
+ rendered = slice.each_with_index.map { |line, i| "#{(start + i).to_s.rjust(5)}| #{line.rstrip}" }
102
+ rendered << " ...| (truncated at #{MAX_SOURCE_LINES} lines)" if truncated
103
+ rendered.join("\n")
104
+ end
105
+
106
+ def append_call_info(lines, index, node)
107
+ callers = call_edges(index).select { |e| e['to'] == node['name'] }
108
+ callees = call_edges(index).select { |e| e['from'] == node['file'] && e['from_method'] == node['name'] }
109
+
110
+ unless callers.empty?
111
+ described = callers.first(MAX_RELATED).map { |e| "#{e['from_method']} (#{e['from']}:#{e['line']})" }
112
+ lines << "Called by: #{described.uniq.join(', ')}"
113
+ end
114
+ return if callees.empty?
115
+
116
+ lines << "Calls: #{callees.map { |e| e['to'] }.uniq.first(MAX_RELATED).join(', ')}"
117
+ end
118
+
119
+ def append_affected_files(lines, index, node)
120
+ caller_files = call_edges(index).select { |e| e['to'] == node['name'] }.map { |e| e['from'] }
121
+ spec_edges = index.edges.select { |e| e['relationship'] == 'tests' && e['to'] == node['file'] }
122
+ spec_files = spec_edges.map { |e| e['from'] }
123
+ affected = (caller_files + spec_files).uniq - [node['file']]
124
+ lines << "Affected files: #{affected.first(MAX_RELATED).join(', ')}" unless affected.empty?
125
+ end
126
+
127
+ def call_edges(index)
128
+ @call_edges ||= index.edges.select { |e| e['relationship'] == 'calls' }
129
+ end
130
+ end
131
+
132
+ Registry.register(CodeGraph)
133
+ end
134
+ end
@@ -5,7 +5,7 @@ module RubynCode
5
5
  class Executor
6
6
  attr_reader :project_root, :output_compressor, :file_cache
7
7
  attr_accessor :llm_client, :background_worker, :on_agent_status, :db, :ask_user_callback,
8
- :codebase_index, :ide_client
8
+ :codebase_index, :ide_client, :todo_store
9
9
 
10
10
  def initialize(project_root:, ide_client: nil)
11
11
  @project_root = File.expand_path(project_root)
@@ -64,6 +64,9 @@ module RubynCode
64
64
  else
65
65
  tool = tool_class.new(project_root: project_root)
66
66
  end
67
+ if @todo_store && tool_class.instance_method(:initialize).parameters.any? { |_, name| name == :store }
68
+ tool.instance_variable_set(:@store, @todo_store)
69
+ end
67
70
  inject_dependencies(tool, tool_name)
68
71
  tool
69
72
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubynCode
4
+ module Tools
5
+ # Shared, thread-safe checklist store. The Agent::Loop owns one instance and
6
+ # exposes it to (a) the renderer so the user sees what's in flight, and
7
+ # (b) the TodoWrite tool so the model can mutate it.
8
+ class TodoStore
9
+ include MonitorMixin
10
+
11
+ Item = Data.define(:content, :status, :active_form)
12
+
13
+ def initialize
14
+ super
15
+ @items = []
16
+ end
17
+
18
+ def replace(items)
19
+ synchronize do
20
+ @items = items.map do |item|
21
+ Item.new(
22
+ content: item['content'] || item[:content],
23
+ status: item['status'] || item[:status],
24
+ active_form: (item['active_form'] || item[:active_form]).to_s
25
+ )
26
+ end
27
+ end
28
+ end
29
+
30
+ def current
31
+ synchronize { @items.map(&:to_h) }
32
+ end
33
+
34
+ def clear
35
+ synchronize { @items = [] }
36
+ end
37
+
38
+ def empty?
39
+ current.empty?
40
+ end
41
+
42
+ def render
43
+ current.map do |item|
44
+ mark =
45
+ case item[:status]
46
+ when 'completed' then '☑'
47
+ when 'in_progress' then '[~]'
48
+ else '[ ]'
49
+ end
50
+ "#{mark} #{item[:content]}"
51
+ end.join("\n")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'registry'
5
+
6
+ module RubynCode
7
+ module Tools
8
+ # Update the in-turn task checklist. The model uses this to keep the user
9
+ # informed of progress while it works. The store is shared between the
10
+ # Agent::Loop (which exposes it for the renderer) and this tool.
11
+ class TodoWrite < Base
12
+ TOOL_NAME = 'TodoWrite'
13
+ DESCRIPTION = 'Update the in-turn task checklist. ' \
14
+ 'Use this to keep the user informed of what you plan to do, ' \
15
+ 'what you are currently working on, and what you have finished.'
16
+ PARAMETERS = {
17
+ todos: { type: :array, required: true,
18
+ description: 'The full set of tasks currently on the checklist. ' \
19
+ 'Replace the existing list with this. Each task is ' \
20
+ '{ "content": "...", "status": "pending|in_progress|completed", ' \
21
+ '"active_form": "..." }.' }
22
+ }.freeze
23
+ RISK_LEVEL = :read
24
+ REQUIRES_CONFIRMATION = false
25
+
26
+ VALID_STATUS = %w[pending in_progress completed].freeze
27
+
28
+ def initialize(project_root:, store: nil)
29
+ super(project_root: project_root)
30
+ @store = store
31
+ end
32
+
33
+ def execute(todos:)
34
+ items = Array(todos)
35
+ validated = []
36
+
37
+ items.each_with_index do |item, i|
38
+ return "TodoWrite: item #{i} is not a hash — got #{item.class}" unless item.is_a?(Hash)
39
+
40
+ content = item[:content] || item['content']
41
+ status = (item[:status] || item['status']).to_s
42
+ active_form = item[:active_form] || item['active_form']
43
+
44
+ return "TodoWrite: item #{i} missing 'content'" if content.to_s.empty?
45
+ unless VALID_STATUS.include?(status)
46
+ return "TodoWrite: item #{i} status must be one of #{VALID_STATUS.join('/')} (got #{status.inspect})"
47
+ end
48
+
49
+ validated << {
50
+ 'content' => content.to_s,
51
+ 'status' => status,
52
+ 'active_form' => active_form.to_s.empty? ? content.to_s : active_form.to_s
53
+ }
54
+ end
55
+
56
+ @store&.replace(validated)
57
+ format(validated)
58
+ end
59
+
60
+ def self.summarize(_output, args)
61
+ count = Array(args[:todos] || args['todos']).size
62
+ if count.zero?
63
+ 'cleared checklist'
64
+ else
65
+ "checklist: #{count} item#{'s' unless count == 1}"
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def format(items)
72
+ return 'Checklist cleared.' if items.empty?
73
+
74
+ items.map do |item|
75
+ mark =
76
+ case item['status']
77
+ when 'completed' then '[x]'
78
+ when 'in_progress' then '[~]'
79
+ else '[ ]'
80
+ end
81
+ "#{mark} #{item['content']}"
82
+ end.join("\n")
83
+ end
84
+ end
85
+
86
+ Registry.register(TodoWrite)
87
+ end
88
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubynCode
4
- VERSION = '0.7.0'
4
+ VERSION = '0.8.0'
5
5
  end
data/lib/rubyn_code.rb CHANGED
@@ -47,6 +47,11 @@ module RubynCode
47
47
  module LLM
48
48
  autoload :Client, 'rubyn_code/llm/client'
49
49
  autoload :MessageBuilder, 'rubyn_code/llm/message_builder'
50
+ autoload :TextBlock, 'rubyn_code/llm/message_builder'
51
+ autoload :ThinkingBlock, 'rubyn_code/llm/message_builder'
52
+ autoload :ImageBlock, 'rubyn_code/llm/message_builder'
53
+ autoload :ToolUseBlock, 'rubyn_code/llm/message_builder'
54
+ autoload :ImageReader, 'rubyn_code/llm/image_reader'
50
55
  autoload :ModelRouter, 'rubyn_code/llm/model_router'
51
56
 
52
57
  # Adapters (provider-specific implementations)
@@ -54,7 +59,6 @@ module RubynCode
54
59
  autoload :Base, 'rubyn_code/llm/adapters/base'
55
60
  autoload :JsonParsing, 'rubyn_code/llm/adapters/json_parsing'
56
61
  autoload :PromptCaching, 'rubyn_code/llm/adapters/prompt_caching'
57
- autoload :TokenCaching, 'rubyn_code/llm/adapters/token_caching'
58
62
  autoload :Anthropic, 'rubyn_code/llm/adapters/anthropic'
59
63
  autoload :AnthropicCompatible, 'rubyn_code/llm/adapters/anthropic_compatible'
60
64
  autoload :AnthropicStreaming, 'rubyn_code/llm/adapters/anthropic_streaming'
@@ -63,9 +67,6 @@ module RubynCode
63
67
  autoload :OpenAICompatible, 'rubyn_code/llm/adapters/openai_compatible'
64
68
  autoload :OpenAIMessageTranslator, 'rubyn_code/llm/adapters/openai_message_translator'
65
69
  end
66
-
67
- # Backward-compat: LLM::Streaming → Adapters::AnthropicStreaming
68
- autoload :Streaming, 'rubyn_code/llm/streaming'
69
70
  end
70
71
 
71
72
  # Layer 1: Agent Loop
@@ -81,6 +82,7 @@ module RubynCode
81
82
  module Tools
82
83
  autoload :Base, 'rubyn_code/tools/base'
83
84
  autoload :Registry, 'rubyn_code/tools/registry'
85
+ autoload :TodoStore, 'rubyn_code/tools/todo_store'
84
86
  autoload :Schema, 'rubyn_code/tools/schema'
85
87
  autoload :Executor, 'rubyn_code/tools/executor'
86
88
  autoload :ReadFile, 'rubyn_code/tools/read_file'
@@ -88,6 +90,7 @@ module RubynCode
88
90
  autoload :EditFile, 'rubyn_code/tools/edit_file'
89
91
  autoload :Glob, 'rubyn_code/tools/glob'
90
92
  autoload :Grep, 'rubyn_code/tools/grep'
93
+ autoload :CodeGraph, 'rubyn_code/tools/code_graph'
91
94
  autoload :Bash, 'rubyn_code/tools/bash'
92
95
  autoload :RailsGenerate, 'rubyn_code/tools/rails_generate'
93
96
  autoload :DbMigrate, 'rubyn_code/tools/db_migrate'
@@ -95,6 +98,7 @@ module RubynCode
95
98
  autoload :BundleInstall, 'rubyn_code/tools/bundle_install'
96
99
  autoload :BundleAdd, 'rubyn_code/tools/bundle_add'
97
100
  autoload :Compact, 'rubyn_code/tools/compact'
101
+ autoload :TodoWrite, 'rubyn_code/tools/todo_write'
98
102
  autoload :LoadSkill, 'rubyn_code/tools/load_skill'
99
103
  autoload :Task, 'rubyn_code/tools/task'
100
104
  autoload :MemorySearch, 'rubyn_code/tools/memory_search'
@@ -133,8 +137,6 @@ module RubynCode
133
137
  autoload :AutoCompact, 'rubyn_code/context/auto_compact'
134
138
  autoload :ManualCompact, 'rubyn_code/context/manual_compact'
135
139
  autoload :ContextCollapse, 'rubyn_code/context/context_collapse'
136
- autoload :ContextBudget, 'rubyn_code/context/context_budget'
137
- autoload :SchemaFilter, 'rubyn_code/context/schema_filter'
138
140
  autoload :DecisionCompactor, 'rubyn_code/context/decision_compactor'
139
141
  end
140
142
 
@@ -201,7 +203,6 @@ module RubynCode
201
203
  # Layer 10: Protocols
202
204
  module Protocols
203
205
  autoload :ShutdownHandshake, 'rubyn_code/protocols/shutdown_handshake'
204
- autoload :PlanApproval, 'rubyn_code/protocols/plan_approval'
205
206
  autoload :InterruptHandler, 'rubyn_code/protocols/interrupt_handler'
206
207
  end
207
208
 
@@ -258,6 +259,7 @@ module RubynCode
258
259
  autoload :ToolBridge, 'rubyn_code/mcp/tool_bridge'
259
260
  autoload :ServerExtrasBridge, 'rubyn_code/mcp/server_extras_bridge'
260
261
  autoload :Config, 'rubyn_code/mcp/config'
262
+ autoload :Discovery, 'rubyn_code/mcp/discovery'
261
263
  end
262
264
 
263
265
  # Layer 16: Learning
@@ -266,7 +268,6 @@ module RubynCode
266
268
  autoload :Instinct, 'rubyn_code/learning/instinct'
267
269
  autoload :InstinctMethods, 'rubyn_code/learning/instinct'
268
270
  autoload :Injector, 'rubyn_code/learning/injector'
269
- autoload :Shortcut, 'rubyn_code/learning/shortcut'
270
271
  autoload :Porter, 'rubyn_code/learning/porter'
271
272
  end
272
273
 
@@ -286,6 +287,7 @@ module RubynCode
286
287
  # Codebase Index
287
288
  module Index
288
289
  autoload :CodebaseIndex, 'rubyn_code/index/codebase_index'
290
+ autoload :PrismExtractor, 'rubyn_code/index/prism_extractor'
289
291
  end
290
292
 
291
293
  # CLI
@@ -347,6 +349,9 @@ module RubynCode
347
349
  autoload :ChiselAudit, 'rubyn_code/cli/commands/chisel_audit'
348
350
  autoload :ChiselDebt, 'rubyn_code/cli/commands/chisel_debt'
349
351
  autoload :ChiselGain, 'rubyn_code/cli/commands/chisel_gain'
352
+ autoload :Think, 'rubyn_code/cli/commands/think'
353
+ autoload :Effort, 'rubyn_code/cli/commands/effort'
354
+ autoload :Export, 'rubyn_code/cli/commands/export'
350
355
  end
351
356
  end
352
357