rubyn-code 0.3.0 → 0.4.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +77 -19
  3. data/db/migrations/013_add_failed_status_to_tasks.rb +51 -0
  4. data/lib/rubyn_code/agent/conversation.rb +32 -3
  5. data/lib/rubyn_code/agent/dynamic_tool_schema.rb +56 -3
  6. data/lib/rubyn_code/agent/llm_caller.rb +9 -1
  7. data/lib/rubyn_code/agent/loop.rb +7 -0
  8. data/lib/rubyn_code/agent/system_prompt_builder.rb +10 -4
  9. data/lib/rubyn_code/agent/tool_processor.rb +21 -1
  10. data/lib/rubyn_code/auth/key_encryption.rb +118 -0
  11. data/lib/rubyn_code/auth/token_store.rb +50 -9
  12. data/lib/rubyn_code/autonomous/daemon.rb +117 -14
  13. data/lib/rubyn_code/autonomous/idle_poller.rb +0 -20
  14. data/lib/rubyn_code/autonomous/task_claimer.rb +17 -11
  15. data/lib/rubyn_code/cli/app.rb +32 -1
  16. data/lib/rubyn_code/cli/commands/doctor.rb +73 -0
  17. data/lib/rubyn_code/cli/commands/mcp.rb +77 -0
  18. data/lib/rubyn_code/cli/commands/model.rb +32 -2
  19. data/lib/rubyn_code/cli/commands/provider.rb +123 -0
  20. data/lib/rubyn_code/cli/commands/skill.rb +52 -3
  21. data/lib/rubyn_code/cli/daemon_runner.rb +36 -0
  22. data/lib/rubyn_code/cli/first_run.rb +159 -0
  23. data/lib/rubyn_code/cli/repl.rb +6 -1
  24. data/lib/rubyn_code/cli/repl_commands.rb +2 -1
  25. data/lib/rubyn_code/cli/repl_lifecycle.rb +1 -0
  26. data/lib/rubyn_code/cli/repl_setup.rb +36 -0
  27. data/lib/rubyn_code/config/defaults.rb +1 -0
  28. data/lib/rubyn_code/config/schema.json +49 -0
  29. data/lib/rubyn_code/config/settings.rb +7 -4
  30. data/lib/rubyn_code/config/validator.rb +63 -0
  31. data/lib/rubyn_code/context/context_budget.rb +16 -1
  32. data/lib/rubyn_code/context/context_collapse.rb +34 -4
  33. data/lib/rubyn_code/context/manager.rb +37 -3
  34. data/lib/rubyn_code/context/manual_compact.rb +1 -1
  35. data/lib/rubyn_code/hooks/registry.rb +4 -0
  36. data/lib/rubyn_code/ide/adapters/tool_output.rb +330 -0
  37. data/lib/rubyn_code/ide/client.rb +110 -0
  38. data/lib/rubyn_code/ide/handlers/accept_edit_handler.rb +35 -0
  39. data/lib/rubyn_code/ide/handlers/approve_tool_use_handler.rb +34 -0
  40. data/lib/rubyn_code/ide/handlers/cancel_handler.rb +41 -0
  41. data/lib/rubyn_code/ide/handlers/config_get_handler.rb +63 -0
  42. data/lib/rubyn_code/ide/handlers/config_set_handler.rb +86 -0
  43. data/lib/rubyn_code/ide/handlers/initialize_handler.rb +79 -0
  44. data/lib/rubyn_code/ide/handlers/models_list_handler.rb +39 -0
  45. data/lib/rubyn_code/ide/handlers/prompt_handler.rb +215 -0
  46. data/lib/rubyn_code/ide/handlers/review_handler.rb +110 -0
  47. data/lib/rubyn_code/ide/handlers/session_fork_handler.rb +49 -0
  48. data/lib/rubyn_code/ide/handlers/session_list_handler.rb +41 -0
  49. data/lib/rubyn_code/ide/handlers/session_reset_handler.rb +31 -0
  50. data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +42 -0
  51. data/lib/rubyn_code/ide/handlers/shutdown_handler.rb +37 -0
  52. data/lib/rubyn_code/ide/handlers.rb +76 -0
  53. data/lib/rubyn_code/ide/protocol.rb +111 -0
  54. data/lib/rubyn_code/ide/server.rb +186 -0
  55. data/lib/rubyn_code/index/codebase_index.rb +67 -1
  56. data/lib/rubyn_code/llm/adapters/anthropic.rb +6 -2
  57. data/lib/rubyn_code/llm/adapters/anthropic_compatible.rb +60 -0
  58. data/lib/rubyn_code/llm/adapters/openai_compatible.rb +6 -2
  59. data/lib/rubyn_code/llm/client.rb +29 -4
  60. data/lib/rubyn_code/mcp/config.rb +2 -1
  61. data/lib/rubyn_code/memory/search.rb +1 -0
  62. data/lib/rubyn_code/self_test.rb +315 -0
  63. data/lib/rubyn_code/skills/catalog.rb +66 -0
  64. data/lib/rubyn_code/skills/loader.rb +43 -0
  65. data/lib/rubyn_code/tasks/models.rb +1 -0
  66. data/lib/rubyn_code/tools/base.rb +13 -0
  67. data/lib/rubyn_code/tools/bash.rb +5 -0
  68. data/lib/rubyn_code/tools/edit_file.rb +62 -5
  69. data/lib/rubyn_code/tools/executor.rb +61 -6
  70. data/lib/rubyn_code/tools/glob.rb +6 -0
  71. data/lib/rubyn_code/tools/grep.rb +6 -0
  72. data/lib/rubyn_code/tools/ide_diagnostics.rb +51 -0
  73. data/lib/rubyn_code/tools/ide_symbols.rb +53 -0
  74. data/lib/rubyn_code/tools/output_compressor.rb +6 -1
  75. data/lib/rubyn_code/tools/read_file.rb +6 -0
  76. data/lib/rubyn_code/tools/registry.rb +11 -0
  77. data/lib/rubyn_code/tools/write_file.rb +17 -0
  78. data/lib/rubyn_code/version.rb +1 -1
  79. data/lib/rubyn_code.rb +22 -0
  80. data/skills/rubyn_self_test.md +13 -1
  81. metadata +31 -1
@@ -22,6 +22,12 @@ module RubynCode
22
22
  RISK_LEVEL = :write
23
23
  REQUIRES_CONFIRMATION = false
24
24
 
25
+ # Take the first line of the tool's output, which is already formatted
26
+ # as "Edited /path.rb (N replacements)".
27
+ def self.summarize(output, _args)
28
+ output.to_s.lines.first.to_s.chomp[0, 200]
29
+ end
30
+
25
31
  def execute(path:, old_text:, new_text:, replace_all: false)
26
32
  resolved = read_file_safely(path)
27
33
  content = File.read(resolved)
@@ -34,12 +40,34 @@ module RubynCode
34
40
  format_diff_result(path, content, old_text, new_text, replace_all)
35
41
  end
36
42
 
43
+ # Compute the proposed file content without writing to disk.
44
+ # Used by IDE mode to preview the edit in a diff view before the user
45
+ # accepts. Raises if old_text is missing or ambiguous, same as execute.
46
+ #
47
+ # @return [Hash] { content: String, type: 'modify' }
48
+ def preview_content(path:, old_text:, new_text:, replace_all: false)
49
+ resolved = read_file_safely(path)
50
+ content = File.read(resolved)
51
+
52
+ validate_occurrences!(path, content, old_text, replace_all)
53
+
54
+ { content: apply_replacement(content, old_text, new_text, replace_all), type: 'modify' }
55
+ end
56
+
37
57
  private
38
58
 
39
59
  def validate_occurrences!(path, content, old_text, replace_all)
40
60
  count = content.scan(old_text).length
41
61
 
42
- raise Error, "old_text not found in #{path}. No changes made." if count.zero?
62
+ # If exact match fails, try with normalized trailing whitespace on
63
+ # each line. Models sometimes strip or add trailing spaces/tabs.
64
+ if count.zero?
65
+ normalized_content = normalize_trailing_ws(content)
66
+ normalized_old = normalize_trailing_ws(old_text)
67
+ count = normalized_content.scan(normalized_old).length
68
+
69
+ raise Error, "old_text not found in #{path}. No changes made." if count.zero?
70
+ end
43
71
 
44
72
  return if replace_all || count == 1
45
73
 
@@ -49,7 +77,28 @@ module RubynCode
49
77
  end
50
78
 
51
79
  def apply_replacement(content, old_text, new_text, replace_all)
52
- replace_all ? content.gsub(old_text, new_text) : content.sub(old_text, new_text)
80
+ # Try exact match first
81
+ if content.include?(old_text)
82
+ return replace_all ? content.gsub(old_text, new_text) : content.sub(old_text, new_text)
83
+ end
84
+
85
+ # Fall back to normalized trailing-whitespace match
86
+ normalized_content = normalize_trailing_ws(content)
87
+ normalized_old = normalize_trailing_ws(old_text)
88
+
89
+ if normalized_content.include?(normalized_old)
90
+ if replace_all
91
+ normalized_content.gsub(normalized_old, new_text)
92
+ else
93
+ normalized_content.sub(normalized_old, new_text)
94
+ end
95
+ else
96
+ content.sub(old_text, new_text)
97
+ end
98
+ end
99
+
100
+ def normalize_trailing_ws(str)
101
+ str.gsub(/[^\S\n]+$/, '')
53
102
  end
54
103
 
55
104
  CONTEXT_LINES = 3 # rubocop:disable Lint/UselessConstantScoping
@@ -76,7 +125,7 @@ module RubynCode
76
125
  end
77
126
 
78
127
  def context_before(content, text)
79
- idx = content.index(text)
128
+ idx = find_index(content, text)
80
129
  return [] unless idx
81
130
 
82
131
  before = content[0...idx].lines.last(CONTEXT_LINES)
@@ -84,7 +133,7 @@ module RubynCode
84
133
  end
85
134
 
86
135
  def context_after(content, text)
87
- idx = content.index(text)
136
+ idx = find_index(content, text)
88
137
  return [] unless idx
89
138
 
90
139
  after_start = idx + text.length
@@ -93,11 +142,19 @@ module RubynCode
93
142
  end
94
143
 
95
144
  def find_line_number(content, text)
96
- idx = content.index(text)
145
+ idx = find_index(content, text)
97
146
  return nil unless idx
98
147
 
99
148
  content[0...idx].count("\n") + 1
100
149
  end
150
+
151
+ # Find the index of text in content, falling back to normalized match.
152
+ def find_index(content, text)
153
+ idx = content.index(text)
154
+ return idx if idx
155
+
156
+ normalize_trailing_ws(content).index(normalize_trailing_ws(text))
157
+ end
101
158
  end
102
159
 
103
160
  Registry.register(EditFile)
@@ -4,10 +4,12 @@ module RubynCode
4
4
  module Tools
5
5
  class Executor
6
6
  attr_reader :project_root, :output_compressor, :file_cache
7
- attr_accessor :llm_client, :background_worker, :on_agent_status, :db, :ask_user_callback
7
+ attr_accessor :llm_client, :background_worker, :on_agent_status, :db, :ask_user_callback,
8
+ :codebase_index, :ide_client
8
9
 
9
- def initialize(project_root:)
10
+ def initialize(project_root:, ide_client: nil)
10
11
  @project_root = File.expand_path(project_root)
12
+ @ide_client = ide_client
11
13
  @injections = {}
12
14
  @output_compressor = OutputCompressor.new
13
15
  @file_cache = FileCache.new
@@ -23,6 +25,7 @@ module RubynCode
23
25
  filtered = filter_params(tool, params)
24
26
  raw = tool.truncate(tool.execute(**filtered).to_s)
25
27
  update_file_cache(tool_name, filtered, raw)
28
+ maybe_update_codebase_index(tool_name, filtered)
26
29
  @output_compressor.compress(tool_name, raw)
27
30
  rescue ToolNotFoundError => e
28
31
  error_result("Tool error: #{e.message}")
@@ -40,11 +43,24 @@ module RubynCode
40
43
  Registry.tool_definitions
41
44
  end
42
45
 
46
+ # Patterns that indicate a bash command writes to a file.
47
+ BASH_WRITE_PATTERNS = [
48
+ /(?:>>?)\s*(\S+)/, # > file or >> file
49
+ /\btee\s+(?:-a\s+)?(\S+)/, # tee file or tee -a file
50
+ /\bsed\s+-i\S*\s+.*\s(\S+)$/, # sed -i 's/...' file
51
+ /\bsed\s+-i\S*\s+.*\s(\S+)\s/ # sed -i 's/...' file (mid-command)
52
+ ].freeze
53
+
43
54
  private
44
55
 
45
56
  def build_tool(tool_name)
46
57
  tool_class = Registry.get(tool_name)
47
- tool = tool_class.new(project_root: project_root)
58
+ # IDE-aware tools accept an ide_client parameter.
59
+ if @ide_client && tool_class.method(:new).parameters.any? { |_, name| name == :ide_client }
60
+ tool = tool_class.new(project_root: project_root, ide_client: @ide_client)
61
+ else
62
+ tool = tool_class.new(project_root: project_root)
63
+ end
48
64
  inject_dependencies(tool, tool_name)
49
65
  tool
50
66
  end
@@ -88,13 +104,17 @@ module RubynCode
88
104
  end
89
105
 
90
106
  # Cache read_file results; invalidate on write_file/edit_file.
107
+ # Also detects bash commands that write to files (redirect, sed -i, tee).
91
108
  def update_file_cache(tool_name, params, _raw)
92
109
  path = resolve_cache_path(params)
93
- return unless path
94
110
 
95
111
  case tool_name
96
- when 'read_file' then @file_cache.read(path) # populates cache
97
- when 'write_file', 'edit_file' then @file_cache.on_write(path)
112
+ when 'read_file'
113
+ @file_cache.read(path) if path # populates cache
114
+ when 'write_file', 'edit_file'
115
+ @file_cache.on_write(path) if path
116
+ when 'bash'
117
+ invalidate_bash_write_targets(params)
98
118
  end
99
119
  rescue StandardError
100
120
  nil
@@ -109,6 +129,41 @@ module RubynCode
109
129
  nil
110
130
  end
111
131
 
132
+ # Trigger an incremental codebase index update after writing a Ruby file.
133
+ # Non-blocking: if the update fails, log and continue.
134
+ def maybe_update_codebase_index(tool_name, params)
135
+ return unless %w[write_file edit_file].include?(tool_name)
136
+ return unless @codebase_index
137
+
138
+ path = resolve_cache_path(params)
139
+ return unless path&.end_with?('.rb')
140
+
141
+ @codebase_index.update!
142
+ rescue StandardError => e
143
+ RubynCode::Debug.warn("CodebaseIndex incremental update failed: #{e.message}")
144
+ end
145
+
146
+ # Detect file paths that a bash command may have written to and
147
+ # invalidate them from the file cache.
148
+ def invalidate_bash_write_targets(params)
149
+ command = params[:command] || params['command']
150
+ return unless command.is_a?(String)
151
+
152
+ paths = extract_bash_write_paths(command)
153
+ paths.each do |p|
154
+ resolved = File.expand_path(p, @project_root)
155
+ @file_cache.on_write(resolved)
156
+ end
157
+ end
158
+
159
+ def extract_bash_write_paths(command)
160
+ paths = []
161
+ BASH_WRITE_PATTERNS.each do |pattern|
162
+ command.scan(pattern) { |match| paths << match[0] if match[0] }
163
+ end
164
+ paths.uniq
165
+ end
166
+
112
167
  def error_result(message)
113
168
  message
114
169
  end
@@ -21,6 +21,12 @@ module RubynCode
21
21
  RISK_LEVEL = :read
22
22
  REQUIRES_CONFIRMATION = false
23
23
 
24
+ def self.summarize(output, args)
25
+ pattern = args['pattern'] || args[:pattern] || ''
26
+ count = output.to_s.strip.empty? ? 0 : output.to_s.lines.count
27
+ "glob #{pattern} (#{count} files)"
28
+ end
29
+
24
30
  def execute(pattern:, path: nil)
25
31
  search_dir = resolve_search_dir(path)
26
32
  full_pattern = File.join(search_dir, pattern)
@@ -20,6 +20,12 @@ module RubynCode
20
20
  RISK_LEVEL = :read
21
21
  REQUIRES_CONFIRMATION = false
22
22
 
23
+ def self.summarize(output, args)
24
+ pattern = args['pattern'] || args[:pattern] || ''
25
+ count = output.to_s.lines.count
26
+ count.zero? || output.to_s.start_with?('No matches') ? "grep #{pattern} (0 matches)" : "grep #{pattern} (#{count} lines)"
27
+ end
28
+
23
29
  def execute(pattern:, path: nil, glob_filter: nil, max_results: 50)
24
30
  search_path = path ? safe_path(path) : project_root
25
31
  regex = Regexp.new(pattern)
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubynCode
4
+ module Tools
5
+ # Retrieves VS Code diagnostics (errors/warnings from the Problems panel)
6
+ # via the IDE RPC bridge. Only available when running in IDE mode.
7
+ class IdeDiagnostics < Base
8
+ TOOL_NAME = 'ide_diagnostics'
9
+ DESCRIPTION = 'Get VS Code diagnostics (errors, warnings) for a file or the whole workspace. Only available in IDE mode.'
10
+ PARAMETERS = {
11
+ file: {
12
+ type: 'string',
13
+ description: 'File path to get diagnostics for. Omit to get all workspace diagnostics.'
14
+ }
15
+ }.freeze
16
+ RISK_LEVEL = :read
17
+
18
+ def initialize(project_root:, ide_client: nil)
19
+ super(project_root: project_root)
20
+ @ide_client = ide_client
21
+ end
22
+
23
+ def execute(**params)
24
+ unless @ide_client
25
+ return 'IDE diagnostics are only available when running inside VS Code.'
26
+ end
27
+
28
+ rpc_params = {}
29
+ rpc_params[:file] = params[:file] if params[:file]
30
+
31
+ result = @ide_client.request('ide/getDiagnostics', rpc_params, timeout: 10)
32
+ diagnostics = result['diagnostics'] || []
33
+
34
+ return 'No diagnostics found.' if diagnostics.empty?
35
+
36
+ lines = diagnostics.map do |d|
37
+ severity = d['severity']&.upcase || 'INFO'
38
+ source = d['source'] ? " (#{d['source']})" : ''
39
+ "#{severity}: #{d['file']}:#{d['line']} — #{d['message']}#{source}"
40
+ end
41
+
42
+ lines.join("\n")
43
+ end
44
+
45
+ def self.summarize(output, _args)
46
+ count = output.lines.count
47
+ "#{count} diagnostic(s)"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubynCode
4
+ module Tools
5
+ # Searches VS Code workspace symbols via the language server.
6
+ # Only available when running in IDE mode.
7
+ class IdeSymbols < Base
8
+ TOOL_NAME = 'ide_symbols'
9
+ DESCRIPTION = 'Search workspace symbols (classes, methods, modules) via VS Code language server. Only available in IDE mode.'
10
+ PARAMETERS = {
11
+ query: {
12
+ type: 'string',
13
+ description: 'Symbol search query (e.g. "User", "authenticate")',
14
+ required: true
15
+ }
16
+ }.freeze
17
+ RISK_LEVEL = :read
18
+
19
+ def initialize(project_root:, ide_client: nil)
20
+ super(project_root: project_root)
21
+ @ide_client = ide_client
22
+ end
23
+
24
+ def execute(**params)
25
+ unless @ide_client
26
+ return 'IDE symbols are only available when running inside VS Code.'
27
+ end
28
+
29
+ query = params[:query] || ''
30
+ return 'Query is required.' if query.empty?
31
+
32
+ result = @ide_client.request('ide/getWorkspaceSymbols', { query: query }, timeout: 10)
33
+ symbols = result['symbols'] || []
34
+
35
+ return "No symbols found matching '#{query}'." if symbols.empty?
36
+
37
+ lines = symbols.first(50).map do |s|
38
+ container = s['containerName'] ? " (in #{s['containerName']})" : ''
39
+ line_info = s['line'] ? ":#{s['line']}" : ''
40
+ "#{s['kind']} #{s['name']}#{container} — #{s['file']}#{line_info}"
41
+ end
42
+
43
+ header = "Found #{symbols.size} symbol(s) matching '#{query}':"
44
+ ([header] + lines).join("\n")
45
+ end
46
+
47
+ def self.summarize(output, _args)
48
+ first_line = output.lines.first&.strip || ''
49
+ first_line.start_with?('Found') ? first_line : ''
50
+ end
51
+ end
52
+ end
53
+ end
@@ -63,7 +63,12 @@ module RubynCode
63
63
  failures = extract_spec_failures(lines)
64
64
  return summary_line.strip if failures.empty? && summary_line
65
65
 
66
- assemble_failure_report(failures, summary_line)
66
+ result = assemble_failure_report(failures, summary_line)
67
+
68
+ # Guard: if compression produced an empty string (no summary line
69
+ # and no extractable failures), return the original output so the
70
+ # agent still sees spec results.
71
+ result.nil? || result.strip.empty? ? output : result
67
72
  end
68
73
 
69
74
  def find_summary_line(lines)
@@ -17,6 +17,12 @@ module RubynCode
17
17
  RISK_LEVEL = :read
18
18
  REQUIRES_CONFIRMATION = false
19
19
 
20
+ def self.summarize(output, args)
21
+ path = args['path'] || args[:path] || ''
22
+ line_count = output.to_s.lines.count
23
+ "Read #{path} (#{line_count} lines)"
24
+ end
25
+
20
26
  def execute(path:, offset: nil, limit: nil)
21
27
  resolved = read_file_safely(path)
22
28
  lines = File.readlines(resolved)
@@ -33,15 +33,26 @@ module RubynCode
33
33
  @tools = {}
34
34
  end
35
35
 
36
+ # IDE-only tools that require an ide_client to function.
37
+ IDE_ONLY_TOOLS = %w[ide_diagnostics ide_symbols].freeze
38
+
36
39
  def load_all!
37
40
  tool_files = Dir[File.join(__dir__, '*.rb')]
38
41
  tool_files.each do |file|
39
42
  basename = File.basename(file, '.rb')
40
43
  next if %w[base registry schema executor].include?(basename)
44
+ next if IDE_ONLY_TOOLS.include?(basename)
41
45
 
42
46
  require_relative basename
43
47
  end
44
48
  end
49
+
50
+ # Register IDE-only tools when an ide_client is available.
51
+ def load_ide_tools!
52
+ IDE_ONLY_TOOLS.each do |name|
53
+ require_relative name
54
+ end
55
+ end
45
56
  end
46
57
  end
47
58
  end
@@ -18,6 +18,12 @@ module RubynCode
18
18
 
19
19
  PREVIEW_LINES = 15
20
20
 
21
+ # Take the first line of the tool's output, which is already formatted
22
+ # as "Updated /path.rb (N bytes)" or "Created /path.rb (N bytes)".
23
+ def self.summarize(output, _args)
24
+ output.to_s.lines.first.to_s.chomp[0, 200]
25
+ end
26
+
21
27
  def execute(path:, content:)
22
28
  resolved = safe_path(path)
23
29
  existed = File.exist?(resolved)
@@ -29,6 +35,17 @@ module RubynCode
29
35
  format_result(path, bytes, existed, old_content, content)
30
36
  end
31
37
 
38
+ # Compute the proposed file content without writing to disk.
39
+ # Used by IDE mode to preview the write in a diff view (modify) or
40
+ # preview tab (create) before the user accepts.
41
+ #
42
+ # @return [Hash] { content: String, type: 'modify' | 'create' }
43
+ def preview_content(path:, content:)
44
+ resolved = safe_path(path)
45
+ type = File.exist?(resolved) ? 'modify' : 'create'
46
+ { content: content, type: type }
47
+ end
48
+
32
49
  private
33
50
 
34
51
  def format_result(path, bytes, existed, old_content, new_content)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubynCode
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/rubyn_code.rb CHANGED
@@ -11,6 +11,10 @@ module RubynCode
11
11
  class StallDetectedError < Error; end
12
12
  class ToolNotFoundError < Error; end
13
13
  class ConfigError < Error; end
14
+ # Raised when the user refuses a tool invocation in IDE mode. Signals the
15
+ # agent loop to surface this as is_error: true so the model sees a refusal
16
+ # rather than a successful tool call returning a string like "denied".
17
+ class UserDeniedError < Error; end
14
18
 
15
19
  # Infrastructure
16
20
  autoload :Config, 'rubyn_code/config/settings'
@@ -29,6 +33,7 @@ module RubynCode
29
33
  # Auth
30
34
  module Auth
31
35
  autoload :OAuth, 'rubyn_code/auth/oauth'
36
+ autoload :KeyEncryption, 'rubyn_code/auth/key_encryption'
32
37
  autoload :TokenStore, 'rubyn_code/auth/token_store'
33
38
  autoload :Server, 'rubyn_code/auth/server'
34
39
  end
@@ -45,6 +50,7 @@ module RubynCode
45
50
  autoload :JsonParsing, 'rubyn_code/llm/adapters/json_parsing'
46
51
  autoload :PromptCaching, 'rubyn_code/llm/adapters/prompt_caching'
47
52
  autoload :Anthropic, 'rubyn_code/llm/adapters/anthropic'
53
+ autoload :AnthropicCompatible, 'rubyn_code/llm/adapters/anthropic_compatible'
48
54
  autoload :AnthropicStreaming, 'rubyn_code/llm/adapters/anthropic_streaming'
49
55
  autoload :OpenAI, 'rubyn_code/llm/adapters/openai'
50
56
  autoload :OpenAIStreaming, 'rubyn_code/llm/adapters/openai_streaming'
@@ -220,6 +226,19 @@ module RubynCode
220
226
  autoload :Shortcut, 'rubyn_code/learning/shortcut'
221
227
  end
222
228
 
229
+ # IDE (VS Code extension server)
230
+ module IDE
231
+ autoload :Protocol, 'rubyn_code/ide/protocol'
232
+ autoload :Server, 'rubyn_code/ide/server'
233
+
234
+ module Adapters
235
+ autoload :ToolOutput, 'rubyn_code/ide/adapters/tool_output'
236
+ end
237
+ end
238
+
239
+ # Self-Test
240
+ autoload :SelfTest, 'rubyn_code/self_test'
241
+
223
242
  # Codebase Index
224
243
  module Index
225
244
  autoload :CodebaseIndex, 'rubyn_code/index/codebase_index'
@@ -234,6 +253,7 @@ module RubynCode
234
253
  autoload :Spinner, 'rubyn_code/cli/spinner'
235
254
  autoload :StreamFormatter, 'rubyn_code/cli/stream_formatter'
236
255
  autoload :Setup, 'rubyn_code/cli/setup'
256
+ autoload :FirstRun, 'rubyn_code/cli/first_run'
237
257
  autoload :DaemonRunner, 'rubyn_code/cli/daemon_runner'
238
258
  autoload :VersionCheck, 'rubyn_code/cli/version_check'
239
259
 
@@ -260,8 +280,10 @@ module RubynCode
260
280
  autoload :Plan, 'rubyn_code/cli/commands/plan'
261
281
  autoload :ContextInfo, 'rubyn_code/cli/commands/context_info'
262
282
  autoload :Diff, 'rubyn_code/cli/commands/diff'
283
+ autoload :Mcp, 'rubyn_code/cli/commands/mcp'
263
284
  autoload :Model, 'rubyn_code/cli/commands/model'
264
285
  autoload :NewSession, 'rubyn_code/cli/commands/new_session'
286
+ autoload :Provider, 'rubyn_code/cli/commands/provider'
265
287
  end
266
288
  end
267
289
 
@@ -98,7 +98,19 @@ Score: 18/22 (82%) — 4 failures
98
98
  ### 11. Slash Commands (report only — don't execute)
99
99
  - Report which slash commands are registered by reading `lib/rubyn_code/cli/commands/registry.rb` or the help output. PASS if at least 15 commands found.
100
100
 
101
- ### 12. Architecture Integrity
101
+ ### 12. MCP Integration
102
+ - **grep**: Search for `url:.*server_def` in `lib/rubyn_code/mcp/config.rb`. PASS if at least 1 match found (confirms SSE url is extracted — a critical bug was shipped without this).
103
+ - **grep**: Search for `autoload.*Mcp` in `lib/rubyn_code.rb`. PASS if found (confirms `/mcp` command is wired up).
104
+ - **run_specs**: Run `bundle exec rspec spec/rubyn_code/mcp/config_spec.rb --format progress`. PASS if output contains `0 failures`.
105
+ - **bash**: Check if `.rubyn-code/mcp.json` exists in the project root. PASS if exists, SKIP if not (MCP is optional per-project).
106
+
107
+ ### 13. GOLEM Autonomous Mode
108
+ - **grep**: Search for `class Daemon` in `lib/rubyn_code/autonomous/daemon.rb`. PASS if found (confirms daemon framework exists).
109
+ - **grep**: Search for `failed\?` in `lib/rubyn_code/tasks/models.rb`. PASS if found (confirms failed task status is available).
110
+ - **grep**: Search for `total_cost` in `lib/rubyn_code/autonomous/daemon.rb`. PASS if at least 2 matches (confirms cost tracking is implemented).
111
+ - **run_specs**: Run `bundle exec rspec spec/rubyn_code/autonomous/daemon_spec.rb --format progress`. PASS if output contains `0 failures` (verifies lifecycle, retries, cost limits, audit trails, concurrent claiming).
112
+
113
+ ### 14. Architecture Integrity
102
114
  - **grep**: Search for `autoload` in `lib/rubyn_code.rb`. PASS if at least 40 autoload entries found.
103
115
  - **glob**: Check that all 16 layer directories exist under `lib/rubyn_code/`. PASS if at least 14 found.
104
116
  - **read_file**: Read `lib/rubyn_code.rb` and verify it has modules for Agent, Tools, Context, Skills, Memory, Observability, Learning. PASS if all 7 found.
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fadedmaturity
@@ -183,6 +183,7 @@ files:
183
183
  - db/migrations/010_create_instincts.sql
184
184
  - db/migrations/011_fix_mailbox_messages_columns.rb
185
185
  - db/migrations/012_expand_mailbox_message_types.rb
186
+ - db/migrations/013_add_failed_status_to_tasks.rb
186
187
  - exe/rubyn-code
187
188
  - lib/rubyn_code.rb
188
189
  - lib/rubyn_code/agent/RUBYN.md
@@ -200,6 +201,7 @@ files:
200
201
  - lib/rubyn_code/agent/tool_processor.rb
201
202
  - lib/rubyn_code/agent/usage_tracker.rb
202
203
  - lib/rubyn_code/auth/RUBYN.md
204
+ - lib/rubyn_code/auth/key_encryption.rb
203
205
  - lib/rubyn_code/auth/oauth.rb
204
206
  - lib/rubyn_code/auth/server.rb
205
207
  - lib/rubyn_code/auth/token_store.rb
@@ -224,9 +226,11 @@ files:
224
226
  - lib/rubyn_code/cli/commands/diff.rb
225
227
  - lib/rubyn_code/cli/commands/doctor.rb
226
228
  - lib/rubyn_code/cli/commands/help.rb
229
+ - lib/rubyn_code/cli/commands/mcp.rb
227
230
  - lib/rubyn_code/cli/commands/model.rb
228
231
  - lib/rubyn_code/cli/commands/new_session.rb
229
232
  - lib/rubyn_code/cli/commands/plan.rb
233
+ - lib/rubyn_code/cli/commands/provider.rb
230
234
  - lib/rubyn_code/cli/commands/quit.rb
231
235
  - lib/rubyn_code/cli/commands/registry.rb
232
236
  - lib/rubyn_code/cli/commands/resume.rb
@@ -238,6 +242,7 @@ files:
238
242
  - lib/rubyn_code/cli/commands/undo.rb
239
243
  - lib/rubyn_code/cli/commands/version.rb
240
244
  - lib/rubyn_code/cli/daemon_runner.rb
245
+ - lib/rubyn_code/cli/first_run.rb
241
246
  - lib/rubyn_code/cli/input_handler.rb
242
247
  - lib/rubyn_code/cli/renderer.rb
243
248
  - lib/rubyn_code/cli/repl.rb
@@ -252,7 +257,9 @@ files:
252
257
  - lib/rubyn_code/config/defaults.rb
253
258
  - lib/rubyn_code/config/project_config.rb
254
259
  - lib/rubyn_code/config/project_profile.rb
260
+ - lib/rubyn_code/config/schema.json
255
261
  - lib/rubyn_code/config/settings.rb
262
+ - lib/rubyn_code/config/validator.rb
256
263
  - lib/rubyn_code/context/RUBYN.md
257
264
  - lib/rubyn_code/context/auto_compact.rb
258
265
  - lib/rubyn_code/context/compactor.rb
@@ -273,6 +280,25 @@ files:
273
280
  - lib/rubyn_code/hooks/registry.rb
274
281
  - lib/rubyn_code/hooks/runner.rb
275
282
  - lib/rubyn_code/hooks/user_hooks.rb
283
+ - lib/rubyn_code/ide/adapters/tool_output.rb
284
+ - lib/rubyn_code/ide/client.rb
285
+ - lib/rubyn_code/ide/handlers.rb
286
+ - lib/rubyn_code/ide/handlers/accept_edit_handler.rb
287
+ - lib/rubyn_code/ide/handlers/approve_tool_use_handler.rb
288
+ - lib/rubyn_code/ide/handlers/cancel_handler.rb
289
+ - lib/rubyn_code/ide/handlers/config_get_handler.rb
290
+ - lib/rubyn_code/ide/handlers/config_set_handler.rb
291
+ - lib/rubyn_code/ide/handlers/initialize_handler.rb
292
+ - lib/rubyn_code/ide/handlers/models_list_handler.rb
293
+ - lib/rubyn_code/ide/handlers/prompt_handler.rb
294
+ - lib/rubyn_code/ide/handlers/review_handler.rb
295
+ - lib/rubyn_code/ide/handlers/session_fork_handler.rb
296
+ - lib/rubyn_code/ide/handlers/session_list_handler.rb
297
+ - lib/rubyn_code/ide/handlers/session_reset_handler.rb
298
+ - lib/rubyn_code/ide/handlers/session_resume_handler.rb
299
+ - lib/rubyn_code/ide/handlers/shutdown_handler.rb
300
+ - lib/rubyn_code/ide/protocol.rb
301
+ - lib/rubyn_code/ide/server.rb
276
302
  - lib/rubyn_code/index/codebase_index.rb
277
303
  - lib/rubyn_code/learning/RUBYN.md
278
304
  - lib/rubyn_code/learning/extractor.rb
@@ -281,6 +307,7 @@ files:
281
307
  - lib/rubyn_code/learning/shortcut.rb
282
308
  - lib/rubyn_code/llm/RUBYN.md
283
309
  - lib/rubyn_code/llm/adapters/anthropic.rb
310
+ - lib/rubyn_code/llm/adapters/anthropic_compatible.rb
284
311
  - lib/rubyn_code/llm/adapters/anthropic_streaming.rb
285
312
  - lib/rubyn_code/llm/adapters/base.rb
286
313
  - lib/rubyn_code/llm/adapters/json_parsing.rb
@@ -324,6 +351,7 @@ files:
324
351
  - lib/rubyn_code/protocols/interrupt_handler.rb
325
352
  - lib/rubyn_code/protocols/plan_approval.rb
326
353
  - lib/rubyn_code/protocols/shutdown_handshake.rb
354
+ - lib/rubyn_code/self_test.rb
327
355
  - lib/rubyn_code/skills/RUBYN.md
328
356
  - lib/rubyn_code/skills/catalog.rb
329
357
  - lib/rubyn_code/skills/document.rb
@@ -358,6 +386,8 @@ files:
358
386
  - lib/rubyn_code/tools/git_status.rb
359
387
  - lib/rubyn_code/tools/glob.rb
360
388
  - lib/rubyn_code/tools/grep.rb
389
+ - lib/rubyn_code/tools/ide_diagnostics.rb
390
+ - lib/rubyn_code/tools/ide_symbols.rb
361
391
  - lib/rubyn_code/tools/load_skill.rb
362
392
  - lib/rubyn_code/tools/memory_search.rb
363
393
  - lib/rubyn_code/tools/memory_write.rb