rubyn-code 0.5.1 → 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.
- checksums.yaml +4 -4
- data/README.md +137 -4
- data/db/migrations/014_multi_agent_upgrade.rb +79 -0
- data/lib/rubyn_code/agent/conversation.rb +100 -4
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +1 -1
- data/lib/rubyn_code/agent/llm_caller.rb +7 -3
- data/lib/rubyn_code/agent/loop.rb +106 -12
- data/lib/rubyn_code/agent/response_parser.rb +8 -0
- data/lib/rubyn_code/agent/system_prompt_builder.rb +40 -2
- data/lib/rubyn_code/agent/tool_processor.rb +13 -1
- data/lib/rubyn_code/auth/oauth.rb +1 -1
- data/lib/rubyn_code/auth/token_store.rb +49 -4
- data/lib/rubyn_code/autonomous/daemon.rb +1 -1
- data/lib/rubyn_code/checkpoint/hook.rb +26 -0
- data/lib/rubyn_code/checkpoint/manager.rb +109 -0
- data/lib/rubyn_code/chisel/debt.rb +65 -0
- data/lib/rubyn_code/chisel/inspection.rb +93 -0
- data/lib/rubyn_code/chisel.rb +127 -0
- data/lib/rubyn_code/cli/commands/agents.rb +31 -0
- data/lib/rubyn_code/cli/commands/chisel.rb +52 -0
- data/lib/rubyn_code/cli/commands/chisel_audit.rb +19 -0
- data/lib/rubyn_code/cli/commands/chisel_debt.rb +28 -0
- data/lib/rubyn_code/cli/commands/chisel_gain.rb +30 -0
- data/lib/rubyn_code/cli/commands/chisel_review.rb +19 -0
- data/lib/rubyn_code/cli/commands/command_template.rb +50 -0
- data/lib/rubyn_code/cli/commands/context.rb +30 -1
- data/lib/rubyn_code/cli/commands/custom_command.rb +84 -0
- data/lib/rubyn_code/cli/commands/custom_loader.rb +100 -0
- data/lib/rubyn_code/cli/commands/effort.rb +47 -0
- data/lib/rubyn_code/cli/commands/export.rb +174 -0
- data/lib/rubyn_code/cli/commands/goal.rb +87 -0
- data/lib/rubyn_code/cli/commands/learning.rb +62 -0
- data/lib/rubyn_code/cli/commands/loop.rb +58 -0
- data/lib/rubyn_code/cli/commands/mcp.rb +49 -12
- data/lib/rubyn_code/cli/commands/megaplan.rb +1 -1
- data/lib/rubyn_code/cli/commands/registry.rb +14 -9
- data/lib/rubyn_code/cli/commands/resume.rb +97 -26
- data/lib/rubyn_code/cli/commands/rewind.rb +65 -0
- data/lib/rubyn_code/cli/commands/think.rb +47 -0
- data/lib/rubyn_code/cli/first_run.rb +1 -1
- data/lib/rubyn_code/cli/loop_runner.rb +98 -0
- data/lib/rubyn_code/cli/mention_expander.rb +111 -0
- data/lib/rubyn_code/cli/renderer.rb +3 -2
- data/lib/rubyn_code/cli/repl.rb +68 -15
- data/lib/rubyn_code/cli/repl_commands.rb +76 -2
- data/lib/rubyn_code/cli/repl_setup.rb +17 -7
- data/lib/rubyn_code/cli/stream_formatter.rb +3 -2
- data/lib/rubyn_code/cli/version_check.rb +10 -3
- data/lib/rubyn_code/config/defaults.rb +14 -1
- data/lib/rubyn_code/config/schema.json +9 -0
- data/lib/rubyn_code/config/settings.rb +19 -2
- data/lib/rubyn_code/context/auto_compact.rb +1 -1
- data/lib/rubyn_code/context/manager.rb +29 -12
- data/lib/rubyn_code/context/manual_compact.rb +1 -1
- data/lib/rubyn_code/debug.rb +11 -5
- data/lib/rubyn_code/goal/evaluator.rb +95 -0
- data/lib/rubyn_code/hooks/event_map.rb +56 -0
- data/lib/rubyn_code/hooks/external_dispatcher.rb +199 -0
- data/lib/rubyn_code/hooks/goal_hook.rb +88 -0
- data/lib/rubyn_code/hooks/response.rb +83 -0
- data/lib/rubyn_code/hooks/runner.rb +61 -3
- data/lib/rubyn_code/hooks/settings_json_loader.rb +109 -0
- data/lib/rubyn_code/hooks/subprocess_executor.rb +116 -0
- data/lib/rubyn_code/ide/handlers/plan_interview_answer_handler.rb +13 -13
- data/lib/rubyn_code/ide/handlers/plan_interview_cancel_handler.rb +1 -1
- data/lib/rubyn_code/ide/handlers/plan_interview_start_handler.rb +10 -10
- data/lib/rubyn_code/ide/handlers/plan_propose_handler.rb +1 -1
- data/lib/rubyn_code/ide/handlers/prompt_handler.rb +9 -1
- data/lib/rubyn_code/ide/handlers/recover_ci_handler.rb +27 -16
- data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +1 -1
- data/lib/rubyn_code/index/codebase_index.rb +103 -4
- data/lib/rubyn_code/index/prism_extractor.rb +82 -0
- data/lib/rubyn_code/learning/injector.rb +1 -2
- data/lib/rubyn_code/learning/porter.rb +129 -0
- data/lib/rubyn_code/llm/adapters/anthropic.rb +168 -29
- data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +13 -0
- data/lib/rubyn_code/llm/adapters/base.rb +2 -1
- data/lib/rubyn_code/llm/adapters/openai.rb +2 -2
- data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +21 -0
- data/lib/rubyn_code/llm/adapters/prompt_caching.rb +5 -1
- data/lib/rubyn_code/llm/client.rb +16 -3
- data/lib/rubyn_code/llm/image_reader.rb +60 -0
- data/lib/rubyn_code/llm/message_builder.rb +21 -1
- data/lib/rubyn_code/llm/model_router.rb +4 -4
- data/lib/rubyn_code/mcp/client.rb +59 -0
- data/lib/rubyn_code/mcp/discovery.rb +93 -0
- data/lib/rubyn_code/mcp/server_extras_bridge.rb +110 -0
- data/lib/rubyn_code/mcp/sse_transport.rb +2 -1
- data/lib/rubyn_code/mcp/tool_bridge.rb +16 -14
- data/lib/rubyn_code/megaplan/ci_recovery.rb +3 -3
- data/lib/rubyn_code/megaplan/interview_session.rb +8 -3
- data/lib/rubyn_code/megaplan/plan_proposer.rb +3 -3
- data/lib/rubyn_code/memory/search.rb +9 -5
- data/lib/rubyn_code/memory/session_persistence.rb +159 -21
- data/lib/rubyn_code/observability/cost_calculator.rb +8 -3
- data/lib/rubyn_code/output/diff_renderer.rb +62 -7
- data/lib/rubyn_code/protocols/RUBYN.md +0 -3
- data/lib/rubyn_code/skills/auto_suggest.rb +70 -2
- data/lib/rubyn_code/skills/registry_client.rb +4 -3
- data/lib/rubyn_code/sub_agents/agent_type.rb +17 -0
- data/lib/rubyn_code/sub_agents/catalog.rb +124 -0
- data/lib/rubyn_code/tasks/models.rb +0 -16
- data/lib/rubyn_code/teams/agent_registry.rb +120 -0
- data/lib/rubyn_code/teams/mailbox.rb +99 -10
- data/lib/rubyn_code/teams/manager.rb +83 -5
- data/lib/rubyn_code/teams/teammate.rb +3 -14
- data/lib/rubyn_code/tools/RUBYN.md +2 -2
- data/lib/rubyn_code/tools/ask_user.rb +15 -1
- data/lib/rubyn_code/tools/bash.rb +3 -3
- data/lib/rubyn_code/tools/code_graph.rb +134 -0
- data/lib/rubyn_code/tools/executor.rb +9 -4
- data/lib/rubyn_code/tools/spawn_agent.rb +47 -62
- data/lib/rubyn_code/tools/spawn_teammate.rb +7 -2
- data/lib/rubyn_code/tools/todo_store.rb +55 -0
- data/lib/rubyn_code/tools/todo_write.rb +88 -0
- data/lib/rubyn_code/tools/web_fetch.rb +1 -1
- data/lib/rubyn_code/tools/web_search.rb +4 -1
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +57 -9
- data/skills/rubyn_self_test.md +462 -14
- data/skills/self_test/chisel_smoke.rb +84 -0
- data/skills/self_test/fixtures/chisel_sample.rb +64 -0
- metadata +45 -6
- data/lib/rubyn_code/context/context_budget.rb +0 -183
- data/lib/rubyn_code/context/schema_filter.rb +0 -64
- data/lib/rubyn_code/learning/shortcut.rb +0 -95
- data/lib/rubyn_code/llm/streaming.rb +0 -10
- data/lib/rubyn_code/protocols/plan_approval.rb +0 -72
|
@@ -60,6 +60,63 @@ module RubynCode
|
|
|
60
60
|
})
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
# Returns the resources advertised by the server (empty unless the
|
|
64
|
+
# server declared the `resources` capability). Each entry is a Hash with
|
|
65
|
+
# "uri", "name", and optionally "description"/"mimeType".
|
|
66
|
+
#
|
|
67
|
+
# @return [Array<Hash>]
|
|
68
|
+
def resources
|
|
69
|
+
return [] unless supports_resources?
|
|
70
|
+
|
|
71
|
+
@resources ||= @transport.send_request('resources/list')&.fetch('resources', []) || []
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
RubynCode::Debug.warn("MCP '#{@name}' resources/list failed: #{e.message}")
|
|
74
|
+
[]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Reads a single resource by URI.
|
|
78
|
+
#
|
|
79
|
+
# @param uri [String]
|
|
80
|
+
# @return [Hash] result with a "contents" array
|
|
81
|
+
def read_resource(uri)
|
|
82
|
+
ensure_connected!
|
|
83
|
+
@transport.send_request('resources/read', { uri: uri })
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns the prompts advertised by the server (empty unless the server
|
|
87
|
+
# declared the `prompts` capability). Each entry has "name" and
|
|
88
|
+
# optionally "description"/"arguments".
|
|
89
|
+
#
|
|
90
|
+
# @return [Array<Hash>]
|
|
91
|
+
def prompts
|
|
92
|
+
return [] unless supports_prompts?
|
|
93
|
+
|
|
94
|
+
@prompts ||= @transport.send_request('prompts/list')&.fetch('prompts', []) || []
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
RubynCode::Debug.warn("MCP '#{@name}' prompts/list failed: #{e.message}")
|
|
97
|
+
[]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Fetches a prompt, expanding its template with the given arguments.
|
|
101
|
+
#
|
|
102
|
+
# @param name [String]
|
|
103
|
+
# @param arguments [Hash]
|
|
104
|
+
# @return [Hash] result with "messages" (and optionally "description")
|
|
105
|
+
def get_prompt(name, arguments = {})
|
|
106
|
+
ensure_connected!
|
|
107
|
+
@transport.send_request('prompts/get', { name: name, arguments: arguments })
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Boolean] whether the server advertised the resources capability
|
|
111
|
+
def supports_resources?
|
|
112
|
+
@server_capabilities.is_a?(Hash) && @server_capabilities.key?('resources')
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @return [Boolean] whether the server advertised the prompts capability
|
|
116
|
+
def supports_prompts?
|
|
117
|
+
@server_capabilities.is_a?(Hash) && @server_capabilities.key?('prompts')
|
|
118
|
+
end
|
|
119
|
+
|
|
63
120
|
# Gracefully disconnects from the MCP server.
|
|
64
121
|
#
|
|
65
122
|
# @return [void]
|
|
@@ -67,6 +124,8 @@ module RubynCode
|
|
|
67
124
|
@transport.stop!
|
|
68
125
|
@initialized = false
|
|
69
126
|
@tools = nil
|
|
127
|
+
@resources = nil
|
|
128
|
+
@prompts = nil
|
|
70
129
|
end
|
|
71
130
|
|
|
72
131
|
# Returns whether the client is connected and the transport is alive.
|
|
@@ -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
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module MCP
|
|
5
|
+
# Bridges an MCP server's *resources* and *prompts* into native RubynCode
|
|
6
|
+
# tools — the non-"tools" half of the MCP surface. Split out of ToolBridge
|
|
7
|
+
# so tool bridging and resource/prompt bridging each stay one focused job.
|
|
8
|
+
#
|
|
9
|
+
# Each capability registers a single tool whose description lists what's
|
|
10
|
+
# available (resource URIs / prompt names) and reads or fetches any of them.
|
|
11
|
+
module ServerExtrasBridge
|
|
12
|
+
class << self
|
|
13
|
+
# If the server exposes resources, register one tool that lists the
|
|
14
|
+
# available URIs (in its description) and reads any of them.
|
|
15
|
+
#
|
|
16
|
+
# @return [Array<Class>] the [read_resource] tool class, or [] if none
|
|
17
|
+
def bridge_resources(mcp_client)
|
|
18
|
+
resources = safe_list(mcp_client, :resources)
|
|
19
|
+
return [] if resources.empty?
|
|
20
|
+
|
|
21
|
+
klass = create_resource_tool(mcp_client, resources)
|
|
22
|
+
Tools::Registry.register(klass)
|
|
23
|
+
[klass]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# If the server exposes prompts, register one tool that lists the
|
|
27
|
+
# available prompt names and fetches any of them (with arguments).
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<Class>] the [get_prompt] tool class, or [] if none
|
|
30
|
+
def bridge_prompts(mcp_client)
|
|
31
|
+
prompts = safe_list(mcp_client, :prompts)
|
|
32
|
+
return [] if prompts.empty?
|
|
33
|
+
|
|
34
|
+
klass = create_prompt_tool(mcp_client, prompts)
|
|
35
|
+
Tools::Registry.register(klass)
|
|
36
|
+
[klass]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def safe_list(mcp_client, method)
|
|
42
|
+
Array(mcp_client.public_send(method))
|
|
43
|
+
rescue StandardError
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def create_resource_tool(mcp_client, resources)
|
|
48
|
+
listing = resources.map { |r| "- #{r['uri']}#{r['name'] && " (#{r['name']})"}" }.join("\n")
|
|
49
|
+
description = "Read a resource from MCP server '#{mcp_client.name}'. Available resources:\n#{listing}"
|
|
50
|
+
params = { uri: { type: :string, description: 'The resource URI to read', required: true } }
|
|
51
|
+
|
|
52
|
+
tool_name = "mcp_#{ToolBridge.sanitize_name(mcp_client.name)}_read_resource"
|
|
53
|
+
build_dynamic_tool(tool_name, description, params) do |args|
|
|
54
|
+
format_resource_contents(mcp_client.read_resource(args[:uri] || args['uri']))
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def create_prompt_tool(mcp_client, prompts)
|
|
59
|
+
listing = prompts.map { |p| "- #{p['name']}#{p['description'] && ": #{p['description']}"}" }.join("\n")
|
|
60
|
+
description = "Fetch a prompt from MCP server '#{mcp_client.name}'. Available prompts:\n#{listing}"
|
|
61
|
+
params = {
|
|
62
|
+
name: { type: :string, description: 'The prompt name', required: true },
|
|
63
|
+
arguments: { type: :object, description: 'Prompt arguments (optional)', required: false }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
tool_name = "mcp_#{ToolBridge.sanitize_name(mcp_client.name)}_get_prompt"
|
|
67
|
+
build_dynamic_tool(tool_name, description, params) do |args|
|
|
68
|
+
result = mcp_client.get_prompt(args[:name] || args['name'], args[:arguments] || args['arguments'] || {})
|
|
69
|
+
format_prompt_messages(result)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Build a Tools::Base subclass whose #execute delegates to the block.
|
|
74
|
+
def build_dynamic_tool(tool_name, description, parameters, &handler)
|
|
75
|
+
Class.new(Tools::Base) do
|
|
76
|
+
const_set(:TOOL_NAME, tool_name)
|
|
77
|
+
const_set(:DESCRIPTION, description)
|
|
78
|
+
const_set(:PARAMETERS, parameters)
|
|
79
|
+
const_set(:RISK_LEVEL, :external)
|
|
80
|
+
const_set(:REQUIRES_CONFIRMATION, true)
|
|
81
|
+
|
|
82
|
+
define_method(:execute) { |**params| handler.call(params) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def format_resource_contents(result)
|
|
87
|
+
contents = result.is_a?(Hash) ? Array(result['contents']) : []
|
|
88
|
+
return result.to_s if contents.empty?
|
|
89
|
+
|
|
90
|
+
contents.map { |c| c['text'] || "[binary resource: #{c['uri']} #{c['mimeType']}]" }.join("\n")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def format_prompt_messages(result)
|
|
94
|
+
messages = result.is_a?(Hash) ? Array(result['messages']) : []
|
|
95
|
+
return result.to_s if messages.empty?
|
|
96
|
+
|
|
97
|
+
messages.map { |m| "#{m['role']}: #{prompt_message_text(m['content'])}" }.join("\n\n")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def prompt_message_text(content)
|
|
101
|
+
case content
|
|
102
|
+
when Hash then content['text'] || content.to_s
|
|
103
|
+
when Array then content.map { |b| b.is_a?(Hash) ? b['text'] : b }.compact.join("\n")
|
|
104
|
+
else content.to_s
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'faraday'
|
|
4
3
|
require 'json'
|
|
5
4
|
require 'uri'
|
|
6
5
|
|
|
@@ -111,6 +110,7 @@ module RubynCode
|
|
|
111
110
|
end
|
|
112
111
|
|
|
113
112
|
def connection
|
|
113
|
+
require 'faraday'
|
|
114
114
|
@connection ||= Faraday.new(url: base_url) do |f|
|
|
115
115
|
f.options.timeout = @timeout
|
|
116
116
|
f.options.open_timeout = @timeout
|
|
@@ -165,6 +165,7 @@ module RubynCode
|
|
|
165
165
|
end
|
|
166
166
|
|
|
167
167
|
def build_sse_connection
|
|
168
|
+
require 'faraday'
|
|
168
169
|
Faraday.new(url: base_url) do |f|
|
|
169
170
|
f.options.timeout = nil
|
|
170
171
|
f.options.open_timeout = @timeout
|
|
@@ -10,6 +10,9 @@ module RubynCode
|
|
|
10
10
|
# - Has RISK_LEVEL = :external
|
|
11
11
|
# - Delegates #execute to the MCP client's #call_tool
|
|
12
12
|
# - Registers itself with Tools::Registry
|
|
13
|
+
#
|
|
14
|
+
# A server's resources and prompts are bridged separately by
|
|
15
|
+
# {ServerExtrasBridge}, which #bridge folds into the returned classes.
|
|
13
16
|
module ToolBridge
|
|
14
17
|
JSON_TYPE_MAP = {
|
|
15
18
|
'string' => :string, 'integer' => :integer, 'number' => :number,
|
|
@@ -18,17 +21,24 @@ module RubynCode
|
|
|
18
21
|
|
|
19
22
|
class << self
|
|
20
23
|
# Discovers tools from an MCP client and creates corresponding
|
|
21
|
-
# RubynCode tool classes.
|
|
24
|
+
# RubynCode tool classes (plus any resource/prompt bridge tools).
|
|
22
25
|
#
|
|
23
26
|
# @param mcp_client [MCP::Client] a connected MCP client
|
|
24
27
|
# @return [Array<Class>] the dynamically created tool classes
|
|
25
28
|
def bridge(mcp_client)
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
classes = Array(mcp_client.tools).map { |tool_def| build_tool_class(mcp_client, tool_def) }
|
|
30
|
+
classes.concat(ServerExtrasBridge.bridge_resources(mcp_client))
|
|
31
|
+
classes.concat(ServerExtrasBridge.bridge_prompts(mcp_client))
|
|
32
|
+
classes
|
|
33
|
+
end
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
# Sanitizes a tool name into a Ruby-friendly identifier. Public so
|
|
36
|
+
# {ServerExtrasBridge} names its resource/prompt tools the same way.
|
|
37
|
+
#
|
|
38
|
+
# @param name [String] the original tool name
|
|
39
|
+
# @return [String] sanitized name
|
|
40
|
+
def sanitize_name(name)
|
|
41
|
+
name.to_s.gsub(/[^a-zA-Z0-9_]/, '_').gsub(/_+/, '_').downcase
|
|
32
42
|
end
|
|
33
43
|
|
|
34
44
|
private
|
|
@@ -119,14 +129,6 @@ module RubynCode
|
|
|
119
129
|
def map_json_type(json_type)
|
|
120
130
|
JSON_TYPE_MAP.fetch(json_type, :string)
|
|
121
131
|
end
|
|
122
|
-
|
|
123
|
-
# Sanitizes a tool name for use as a Ruby-friendly identifier.
|
|
124
|
-
#
|
|
125
|
-
# @param name [String] the original tool name
|
|
126
|
-
# @return [String] sanitized name
|
|
127
|
-
def sanitize_name(name)
|
|
128
|
-
name.to_s.gsub(/[^a-zA-Z0-9_]/, '_').gsub(/_+/, '_').downcase
|
|
129
|
-
end
|
|
130
132
|
end
|
|
131
133
|
end
|
|
132
134
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubynCode
|
|
4
4
|
module Megaplan
|
|
@@ -17,7 +17,7 @@ module RubynCode
|
|
|
17
17
|
class CiRecovery
|
|
18
18
|
class RecoveryError < RubynCode::Error; end
|
|
19
19
|
|
|
20
|
-
SYSTEM_PROMPT = <<~PROMPT
|
|
20
|
+
SYSTEM_PROMPT = <<~PROMPT
|
|
21
21
|
You are Rubyn doing CI auto-recovery on a megaplan PR.
|
|
22
22
|
|
|
23
23
|
Read the failing job log, identify the root cause, push a fix
|
|
@@ -93,7 +93,7 @@ module RubynCode
|
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
def default_agent_invoker(
|
|
96
|
+
def default_agent_invoker(_prompt, _context)
|
|
97
97
|
# Stub for now — real wiring happens in RecoverCiHandler which has
|
|
98
98
|
# an Agent::Loop on hand. The handler injects its own invoker via
|
|
99
99
|
# the constructor.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'securerandom'
|
|
@@ -65,7 +65,7 @@ module RubynCode
|
|
|
65
65
|
# The skill teaches *what* a megaplan is and *how* to interview; this
|
|
66
66
|
# contract teaches the LLM the wire format the gem expects on every
|
|
67
67
|
# turn AND that its tool palette is read-only.
|
|
68
|
-
JSON_OUTPUT_CONTRACT = <<~CONTRACT
|
|
68
|
+
JSON_OUTPUT_CONTRACT = <<~CONTRACT
|
|
69
69
|
# Output contract (overrides any other formatting instinct)
|
|
70
70
|
|
|
71
71
|
You are an interviewer, not a coding agent. You have a READ-ONLY
|
|
@@ -192,13 +192,18 @@ module RubynCode
|
|
|
192
192
|
result = if INTERVIEW_TOOLS.include?(call.name.to_s)
|
|
193
193
|
@executor.execute(call.name, stringify_keys(call.input))
|
|
194
194
|
else
|
|
195
|
-
|
|
195
|
+
unavailable_tool_message(call.name)
|
|
196
196
|
end
|
|
197
197
|
{ type: 'tool_result', tool_use_id: call.id, content: result.to_s }
|
|
198
198
|
end
|
|
199
199
|
{ role: 'user', content: content }
|
|
200
200
|
end
|
|
201
201
|
|
|
202
|
+
def unavailable_tool_message(name)
|
|
203
|
+
"Tool '#{name}' is not available in interview mode " \
|
|
204
|
+
"(read-only palette: #{INTERVIEW_TOOLS.join(', ')})."
|
|
205
|
+
end
|
|
206
|
+
|
|
202
207
|
def stringify_keys(input)
|
|
203
208
|
return input unless input.is_a?(Hash)
|
|
204
209
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'securerandom'
|
|
@@ -18,7 +18,7 @@ module RubynCode
|
|
|
18
18
|
class InvalidProposalError < RubynCode::Error; end
|
|
19
19
|
|
|
20
20
|
MAX_PHASES = 12
|
|
21
|
-
DEFAULT_SYSTEM_PROMPT = <<~PROMPT
|
|
21
|
+
DEFAULT_SYSTEM_PROMPT = <<~PROMPT
|
|
22
22
|
You are a senior Ruby/Rails architect breaking a feature request into a megaplan.
|
|
23
23
|
|
|
24
24
|
A megaplan is a multi-phase development plan where each phase is a
|
|
@@ -127,7 +127,7 @@ module RubynCode
|
|
|
127
127
|
slug = slugify(feature) if slug.empty?
|
|
128
128
|
phases = payload['phases'].each_with_index.map do |phase, idx|
|
|
129
129
|
{
|
|
130
|
-
'number' => phase['number'] || idx + 1,
|
|
130
|
+
'number' => phase['number'] || (idx + 1),
|
|
131
131
|
'slug' => (phase['slug'].to_s.strip.empty? ? slugify(phase['name']) : phase['slug']),
|
|
132
132
|
'name' => phase['name'],
|
|
133
133
|
'summary' => phase['summary'],
|
|
@@ -6,9 +6,10 @@ require_relative 'models'
|
|
|
6
6
|
module RubynCode
|
|
7
7
|
module Memory
|
|
8
8
|
# Searches memories using SQLite FTS5 full-text search and standard
|
|
9
|
-
# queries.
|
|
10
|
-
#
|
|
11
|
-
# frequently-accessed memories against decay
|
|
9
|
+
# queries. Search methods automatically increment access_count and
|
|
10
|
+
# update last_accessed_at on returned records, reinforcing
|
|
11
|
+
# frequently-accessed memories against decay; #recent accepts
|
|
12
|
+
# touch: false to opt out for passive reads.
|
|
12
13
|
class Search
|
|
13
14
|
# @param db [DB::Connection] database connection
|
|
14
15
|
# @param project_path [String] scoping path for searches
|
|
@@ -60,8 +61,11 @@ module RubynCode
|
|
|
60
61
|
# Returns the most recently created memories.
|
|
61
62
|
#
|
|
62
63
|
# @param limit [Integer] maximum results (default 10)
|
|
64
|
+
# @param touch [Boolean] whether to record an access on returned
|
|
65
|
+
# records; pass false for passive reads (e.g. prompt assembly)
|
|
66
|
+
# that shouldn't reinforce memories or issue a write
|
|
63
67
|
# @return [Array<MemoryRecord>]
|
|
64
|
-
def recent(limit: 10)
|
|
68
|
+
def recent(limit: 10, touch: true)
|
|
65
69
|
rows = @db.query(<<~SQL, [@project_path, limit]).to_a
|
|
66
70
|
SELECT id, project_path, tier, category, content,
|
|
67
71
|
relevance_score, access_count, last_accessed_at,
|
|
@@ -73,7 +77,7 @@ module RubynCode
|
|
|
73
77
|
SQL
|
|
74
78
|
|
|
75
79
|
records = rows.map { |row| build_record(row) }
|
|
76
|
-
touch_accessed(records)
|
|
80
|
+
touch_accessed(records) if touch
|
|
77
81
|
records
|
|
78
82
|
end
|
|
79
83
|
|