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.
- checksums.yaml +4 -4
- data/README.md +77 -19
- data/db/migrations/013_add_failed_status_to_tasks.rb +51 -0
- data/lib/rubyn_code/agent/conversation.rb +32 -3
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +56 -3
- data/lib/rubyn_code/agent/llm_caller.rb +9 -1
- data/lib/rubyn_code/agent/loop.rb +7 -0
- data/lib/rubyn_code/agent/system_prompt_builder.rb +10 -4
- data/lib/rubyn_code/agent/tool_processor.rb +21 -1
- data/lib/rubyn_code/auth/key_encryption.rb +118 -0
- data/lib/rubyn_code/auth/token_store.rb +50 -9
- data/lib/rubyn_code/autonomous/daemon.rb +117 -14
- data/lib/rubyn_code/autonomous/idle_poller.rb +0 -20
- data/lib/rubyn_code/autonomous/task_claimer.rb +17 -11
- data/lib/rubyn_code/cli/app.rb +32 -1
- data/lib/rubyn_code/cli/commands/doctor.rb +73 -0
- data/lib/rubyn_code/cli/commands/mcp.rb +77 -0
- data/lib/rubyn_code/cli/commands/model.rb +32 -2
- data/lib/rubyn_code/cli/commands/provider.rb +123 -0
- data/lib/rubyn_code/cli/commands/skill.rb +52 -3
- data/lib/rubyn_code/cli/daemon_runner.rb +36 -0
- data/lib/rubyn_code/cli/first_run.rb +159 -0
- data/lib/rubyn_code/cli/repl.rb +6 -1
- data/lib/rubyn_code/cli/repl_commands.rb +2 -1
- data/lib/rubyn_code/cli/repl_lifecycle.rb +1 -0
- data/lib/rubyn_code/cli/repl_setup.rb +36 -0
- data/lib/rubyn_code/config/defaults.rb +1 -0
- data/lib/rubyn_code/config/schema.json +49 -0
- data/lib/rubyn_code/config/settings.rb +7 -4
- data/lib/rubyn_code/config/validator.rb +63 -0
- data/lib/rubyn_code/context/context_budget.rb +16 -1
- data/lib/rubyn_code/context/context_collapse.rb +34 -4
- data/lib/rubyn_code/context/manager.rb +37 -3
- data/lib/rubyn_code/context/manual_compact.rb +1 -1
- data/lib/rubyn_code/hooks/registry.rb +4 -0
- data/lib/rubyn_code/ide/adapters/tool_output.rb +330 -0
- data/lib/rubyn_code/ide/client.rb +110 -0
- data/lib/rubyn_code/ide/handlers/accept_edit_handler.rb +35 -0
- data/lib/rubyn_code/ide/handlers/approve_tool_use_handler.rb +34 -0
- data/lib/rubyn_code/ide/handlers/cancel_handler.rb +41 -0
- data/lib/rubyn_code/ide/handlers/config_get_handler.rb +63 -0
- data/lib/rubyn_code/ide/handlers/config_set_handler.rb +86 -0
- data/lib/rubyn_code/ide/handlers/initialize_handler.rb +79 -0
- data/lib/rubyn_code/ide/handlers/models_list_handler.rb +39 -0
- data/lib/rubyn_code/ide/handlers/prompt_handler.rb +215 -0
- data/lib/rubyn_code/ide/handlers/review_handler.rb +110 -0
- data/lib/rubyn_code/ide/handlers/session_fork_handler.rb +49 -0
- data/lib/rubyn_code/ide/handlers/session_list_handler.rb +41 -0
- data/lib/rubyn_code/ide/handlers/session_reset_handler.rb +31 -0
- data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +42 -0
- data/lib/rubyn_code/ide/handlers/shutdown_handler.rb +37 -0
- data/lib/rubyn_code/ide/handlers.rb +76 -0
- data/lib/rubyn_code/ide/protocol.rb +111 -0
- data/lib/rubyn_code/ide/server.rb +186 -0
- data/lib/rubyn_code/index/codebase_index.rb +67 -1
- data/lib/rubyn_code/llm/adapters/anthropic.rb +6 -2
- data/lib/rubyn_code/llm/adapters/anthropic_compatible.rb +60 -0
- data/lib/rubyn_code/llm/adapters/openai_compatible.rb +6 -2
- data/lib/rubyn_code/llm/client.rb +29 -4
- data/lib/rubyn_code/mcp/config.rb +2 -1
- data/lib/rubyn_code/memory/search.rb +1 -0
- data/lib/rubyn_code/self_test.rb +315 -0
- data/lib/rubyn_code/skills/catalog.rb +66 -0
- data/lib/rubyn_code/skills/loader.rb +43 -0
- data/lib/rubyn_code/tasks/models.rb +1 -0
- data/lib/rubyn_code/tools/base.rb +13 -0
- data/lib/rubyn_code/tools/bash.rb +5 -0
- data/lib/rubyn_code/tools/edit_file.rb +62 -5
- data/lib/rubyn_code/tools/executor.rb +61 -6
- data/lib/rubyn_code/tools/glob.rb +6 -0
- data/lib/rubyn_code/tools/grep.rb +6 -0
- data/lib/rubyn_code/tools/ide_diagnostics.rb +51 -0
- data/lib/rubyn_code/tools/ide_symbols.rb +53 -0
- data/lib/rubyn_code/tools/output_compressor.rb +6 -1
- data/lib/rubyn_code/tools/read_file.rb +6 -0
- data/lib/rubyn_code/tools/registry.rb +11 -0
- data/lib/rubyn_code/tools/write_file.rb +17 -0
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +22 -0
- data/skills/rubyn_self_test.md +13 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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'
|
|
97
|
-
|
|
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)
|
data/lib/rubyn_code/version.rb
CHANGED
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
|
|
data/skills/rubyn_self_test.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|