rubyn-code 0.1.0 → 0.2.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 +269 -467
- data/db/migrations/009_create_teams.sql +6 -6
- data/db/migrations/011_fix_mailbox_messages_columns.rb +35 -0
- data/db/migrations/012_expand_mailbox_message_types.rb +37 -0
- data/exe/rubyn-code +1 -1
- data/lib/rubyn_code/agent/RUBYN.md +17 -0
- data/lib/rubyn_code/agent/conversation.rb +68 -19
- data/lib/rubyn_code/agent/loop.rb +312 -54
- data/lib/rubyn_code/agent/loop_detector.rb +6 -6
- data/lib/rubyn_code/auth/RUBYN.md +19 -0
- data/lib/rubyn_code/auth/oauth.rb +40 -35
- data/lib/rubyn_code/auth/server.rb +16 -12
- data/lib/rubyn_code/auth/token_store.rb +22 -22
- data/lib/rubyn_code/autonomous/RUBYN.md +14 -0
- data/lib/rubyn_code/autonomous/daemon.rb +115 -79
- data/lib/rubyn_code/autonomous/idle_poller.rb +4 -8
- data/lib/rubyn_code/autonomous/task_claimer.rb +11 -11
- data/lib/rubyn_code/background/RUBYN.md +13 -0
- data/lib/rubyn_code/background/notifier.rb +0 -2
- data/lib/rubyn_code/background/worker.rb +60 -15
- data/lib/rubyn_code/cli/RUBYN.md +30 -0
- data/lib/rubyn_code/cli/app.rb +85 -9
- data/lib/rubyn_code/cli/commands/RUBYN.md +133 -0
- data/lib/rubyn_code/cli/commands/base.rb +53 -0
- data/lib/rubyn_code/cli/commands/budget.rb +24 -0
- data/lib/rubyn_code/cli/commands/clear.rb +16 -0
- data/lib/rubyn_code/cli/commands/compact.rb +21 -0
- data/lib/rubyn_code/cli/commands/context.rb +44 -0
- data/lib/rubyn_code/cli/commands/context_info.rb +56 -0
- data/lib/rubyn_code/cli/commands/cost.rb +23 -0
- data/lib/rubyn_code/cli/commands/diff.rb +30 -0
- data/lib/rubyn_code/cli/commands/doctor.rb +112 -0
- data/lib/rubyn_code/cli/commands/help.rb +41 -0
- data/lib/rubyn_code/cli/commands/model.rb +37 -0
- data/lib/rubyn_code/cli/commands/plan.rb +22 -0
- data/lib/rubyn_code/cli/commands/quit.rb +17 -0
- data/lib/rubyn_code/cli/commands/registry.rb +64 -0
- data/lib/rubyn_code/cli/commands/resume.rb +51 -0
- data/lib/rubyn_code/cli/commands/review.rb +26 -0
- data/lib/rubyn_code/cli/commands/skill.rb +32 -0
- data/lib/rubyn_code/cli/commands/spawn.rb +24 -0
- data/lib/rubyn_code/cli/commands/tasks.rb +32 -0
- data/lib/rubyn_code/cli/commands/tokens.rb +76 -0
- data/lib/rubyn_code/cli/commands/undo.rb +17 -0
- data/lib/rubyn_code/cli/commands/version.rb +16 -0
- data/lib/rubyn_code/cli/daemon_runner.rb +129 -0
- data/lib/rubyn_code/cli/input_handler.rb +20 -23
- data/lib/rubyn_code/cli/renderer.rb +25 -27
- data/lib/rubyn_code/cli/repl.rb +161 -194
- data/lib/rubyn_code/cli/setup.rb +117 -0
- data/lib/rubyn_code/cli/spinner.rb +40 -40
- data/lib/rubyn_code/cli/stream_formatter.rb +29 -28
- data/lib/rubyn_code/cli/version_check.rb +94 -0
- data/lib/rubyn_code/config/RUBYN.md +14 -0
- data/lib/rubyn_code/config/defaults.rb +28 -19
- data/lib/rubyn_code/config/project_config.rb +7 -9
- data/lib/rubyn_code/config/settings.rb +3 -3
- data/lib/rubyn_code/context/RUBYN.md +20 -0
- data/lib/rubyn_code/context/auto_compact.rb +7 -7
- data/lib/rubyn_code/context/compactor.rb +2 -2
- data/lib/rubyn_code/context/context_collapse.rb +45 -0
- data/lib/rubyn_code/context/manager.rb +20 -3
- data/lib/rubyn_code/context/manual_compact.rb +7 -7
- data/lib/rubyn_code/context/micro_compact.rb +12 -12
- data/lib/rubyn_code/db/RUBYN.md +40 -0
- data/lib/rubyn_code/db/connection.rb +13 -13
- data/lib/rubyn_code/db/migrator.rb +67 -27
- data/lib/rubyn_code/db/schema.rb +6 -6
- data/lib/rubyn_code/debug.rb +74 -0
- data/lib/rubyn_code/hooks/RUBYN.md +17 -0
- data/lib/rubyn_code/hooks/built_in.rb +9 -9
- data/lib/rubyn_code/hooks/registry.rb +5 -5
- data/lib/rubyn_code/hooks/runner.rb +1 -1
- data/lib/rubyn_code/hooks/user_hooks.rb +16 -16
- data/lib/rubyn_code/learning/RUBYN.md +16 -0
- data/lib/rubyn_code/learning/extractor.rb +22 -22
- data/lib/rubyn_code/learning/injector.rb +17 -18
- data/lib/rubyn_code/learning/instinct.rb +18 -14
- data/lib/rubyn_code/llm/RUBYN.md +15 -0
- data/lib/rubyn_code/llm/client.rb +121 -55
- data/lib/rubyn_code/llm/message_builder.rb +19 -15
- data/lib/rubyn_code/llm/streaming.rb +80 -50
- data/lib/rubyn_code/mcp/RUBYN.md +21 -0
- data/lib/rubyn_code/mcp/client.rb +25 -24
- data/lib/rubyn_code/mcp/config.rb +7 -7
- data/lib/rubyn_code/mcp/sse_transport.rb +27 -26
- data/lib/rubyn_code/mcp/stdio_transport.rb +22 -19
- data/lib/rubyn_code/mcp/tool_bridge.rb +32 -32
- data/lib/rubyn_code/memory/RUBYN.md +17 -0
- data/lib/rubyn_code/memory/models.rb +3 -3
- data/lib/rubyn_code/memory/search.rb +17 -17
- data/lib/rubyn_code/memory/session_persistence.rb +49 -34
- data/lib/rubyn_code/memory/store.rb +17 -17
- data/lib/rubyn_code/observability/RUBYN.md +19 -0
- data/lib/rubyn_code/observability/budget_enforcer.rb +16 -15
- data/lib/rubyn_code/observability/cost_calculator.rb +3 -3
- data/lib/rubyn_code/observability/token_counter.rb +1 -1
- data/lib/rubyn_code/observability/usage_reporter.rb +35 -35
- data/lib/rubyn_code/output/RUBYN.md +11 -0
- data/lib/rubyn_code/output/diff_renderer.rb +6 -6
- data/lib/rubyn_code/output/formatter.rb +4 -4
- data/lib/rubyn_code/permissions/RUBYN.md +17 -0
- data/lib/rubyn_code/permissions/prompter.rb +8 -8
- data/lib/rubyn_code/protocols/RUBYN.md +14 -0
- data/lib/rubyn_code/protocols/interrupt_handler.rb +1 -1
- data/lib/rubyn_code/protocols/plan_approval.rb +9 -9
- data/lib/rubyn_code/protocols/shutdown_handshake.rb +9 -11
- data/lib/rubyn_code/skills/RUBYN.md +19 -0
- data/lib/rubyn_code/skills/catalog.rb +7 -7
- data/lib/rubyn_code/skills/document.rb +15 -15
- data/lib/rubyn_code/skills/loader.rb +6 -8
- data/lib/rubyn_code/sub_agents/RUBYN.md +12 -0
- data/lib/rubyn_code/sub_agents/runner.rb +15 -15
- data/lib/rubyn_code/sub_agents/summarizer.rb +1 -1
- data/lib/rubyn_code/tasks/RUBYN.md +13 -0
- data/lib/rubyn_code/tasks/dag.rb +12 -16
- data/lib/rubyn_code/tasks/manager.rb +24 -24
- data/lib/rubyn_code/tasks/models.rb +4 -4
- data/lib/rubyn_code/teams/RUBYN.md +14 -0
- data/lib/rubyn_code/teams/mailbox.rb +38 -18
- data/lib/rubyn_code/teams/manager.rb +19 -19
- data/lib/rubyn_code/teams/teammate.rb +3 -4
- data/lib/rubyn_code/tools/RUBYN.md +38 -0
- data/lib/rubyn_code/tools/background_run.rb +9 -11
- data/lib/rubyn_code/tools/base.rb +54 -3
- data/lib/rubyn_code/tools/bash.rb +16 -34
- data/lib/rubyn_code/tools/bundle_add.rb +10 -12
- data/lib/rubyn_code/tools/bundle_install.rb +9 -11
- data/lib/rubyn_code/tools/compact.rb +10 -9
- data/lib/rubyn_code/tools/db_migrate.rb +17 -15
- data/lib/rubyn_code/tools/edit_file.rb +12 -12
- data/lib/rubyn_code/tools/executor.rb +9 -4
- data/lib/rubyn_code/tools/git_commit.rb +29 -34
- data/lib/rubyn_code/tools/git_diff.rb +17 -18
- data/lib/rubyn_code/tools/git_log.rb +17 -19
- data/lib/rubyn_code/tools/git_status.rb +18 -20
- data/lib/rubyn_code/tools/glob.rb +7 -9
- data/lib/rubyn_code/tools/grep.rb +11 -9
- data/lib/rubyn_code/tools/load_skill.rb +7 -7
- data/lib/rubyn_code/tools/memory_search.rb +13 -12
- data/lib/rubyn_code/tools/memory_write.rb +14 -12
- data/lib/rubyn_code/tools/rails_generate.rb +16 -16
- data/lib/rubyn_code/tools/read_file.rb +8 -7
- data/lib/rubyn_code/tools/read_inbox.rb +5 -5
- data/lib/rubyn_code/tools/registry.rb +2 -2
- data/lib/rubyn_code/tools/review_pr.rb +55 -55
- data/lib/rubyn_code/tools/run_specs.rb +20 -19
- data/lib/rubyn_code/tools/schema.rb +9 -11
- data/lib/rubyn_code/tools/send_message.rb +10 -10
- data/lib/rubyn_code/tools/spawn_agent.rb +51 -23
- data/lib/rubyn_code/tools/spawn_teammate.rb +21 -21
- data/lib/rubyn_code/tools/task.rb +28 -28
- data/lib/rubyn_code/tools/web_fetch.rb +46 -31
- data/lib/rubyn_code/tools/web_search.rb +64 -66
- data/lib/rubyn_code/tools/write_file.rb +7 -6
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +136 -105
- metadata +94 -21
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'registry'
|
|
5
5
|
|
|
6
6
|
module RubynCode
|
|
7
7
|
module Tools
|
|
8
8
|
class Glob < Base
|
|
9
|
-
TOOL_NAME =
|
|
10
|
-
DESCRIPTION =
|
|
9
|
+
TOOL_NAME = 'glob'
|
|
10
|
+
DESCRIPTION = 'File pattern matching. Returns sorted list of file paths matching the glob pattern.'
|
|
11
11
|
PARAMETERS = {
|
|
12
12
|
pattern: { type: :string, required: true, description: "Glob pattern (e.g. '**/*.rb', 'app/**/*.erb')" },
|
|
13
|
-
path: { type: :string, required: false, description:
|
|
13
|
+
path: { type: :string, required: false, description: 'Directory to search in (defaults to project root)' }
|
|
14
14
|
}.freeze
|
|
15
15
|
RISK_LEVEL = :read
|
|
16
16
|
REQUIRES_CONFIRMATION = false
|
|
@@ -18,16 +18,14 @@ module RubynCode
|
|
|
18
18
|
def execute(pattern:, path: nil)
|
|
19
19
|
search_dir = path ? safe_path(path) : project_root
|
|
20
20
|
|
|
21
|
-
unless File.directory?(search_dir)
|
|
22
|
-
raise Error, "Directory not found: #{path || project_root}"
|
|
23
|
-
end
|
|
21
|
+
raise Error, "Directory not found: #{path || project_root}" unless File.directory?(search_dir)
|
|
24
22
|
|
|
25
23
|
full_pattern = File.join(search_dir, pattern)
|
|
26
24
|
matches = Dir.glob(full_pattern, File::FNM_DOTMATCH).sort
|
|
27
25
|
|
|
28
26
|
matches
|
|
29
27
|
.select { |f| File.file?(f) }
|
|
30
|
-
.reject { |f| File.basename(f).start_with?(
|
|
28
|
+
.reject { |f| (File.basename(f).start_with?('.') && File.basename(f) == '.') || File.basename(f) == '..' }
|
|
31
29
|
.map { |f| relative_to_root(f) }
|
|
32
30
|
.join("\n")
|
|
33
31
|
end
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'registry'
|
|
5
5
|
|
|
6
6
|
module RubynCode
|
|
7
7
|
module Tools
|
|
8
8
|
class Grep < Base
|
|
9
|
-
TOOL_NAME =
|
|
10
|
-
DESCRIPTION =
|
|
9
|
+
TOOL_NAME = 'grep'
|
|
10
|
+
DESCRIPTION = 'Searches file contents using regular expressions. Returns matching lines with file paths and line numbers.'
|
|
11
11
|
PARAMETERS = {
|
|
12
|
-
pattern: { type: :string, required: true, description:
|
|
13
|
-
path: { type: :string, required: false,
|
|
12
|
+
pattern: { type: :string, required: true, description: 'Regular expression pattern to search for' },
|
|
13
|
+
path: { type: :string, required: false,
|
|
14
|
+
description: 'File or directory to search in (defaults to project root)' },
|
|
14
15
|
glob_filter: { type: :string, required: false, description: "Glob pattern to filter files (e.g. '*.rb')" },
|
|
15
|
-
max_results: { type: :integer, required: false, default: 50,
|
|
16
|
+
max_results: { type: :integer, required: false, default: 50,
|
|
17
|
+
description: 'Maximum number of matching lines to return' }
|
|
16
18
|
}.freeze
|
|
17
19
|
RISK_LEVEL = :read
|
|
18
20
|
REQUIRES_CONFIRMATION = false
|
|
@@ -41,7 +43,7 @@ module RubynCode
|
|
|
41
43
|
if File.file?(search_path)
|
|
42
44
|
[search_path]
|
|
43
45
|
elsif File.directory?(search_path)
|
|
44
|
-
glob_pattern = glob_filter ||
|
|
46
|
+
glob_pattern = glob_filter || '**/*'
|
|
45
47
|
Dir.glob(File.join(search_path, glob_pattern))
|
|
46
48
|
.select { |f| File.file?(f) }
|
|
47
49
|
.reject { |f| binary_file?(f) }
|
|
@@ -70,7 +72,7 @@ module RubynCode
|
|
|
70
72
|
sample = File.read(path, 512)
|
|
71
73
|
return false if sample.nil?
|
|
72
74
|
|
|
73
|
-
sample.bytes.any?
|
|
75
|
+
sample.bytes.any?(&:zero?)
|
|
74
76
|
rescue StandardError
|
|
75
77
|
true
|
|
76
78
|
end
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'registry'
|
|
5
5
|
|
|
6
6
|
module RubynCode
|
|
7
7
|
module Tools
|
|
8
8
|
class LoadSkill < Base
|
|
9
|
-
TOOL_NAME =
|
|
10
|
-
DESCRIPTION =
|
|
9
|
+
TOOL_NAME = 'load_skill'
|
|
10
|
+
DESCRIPTION = 'Loads a skill document into the conversation context. Use /skill-name or provide the skill name.'
|
|
11
11
|
PARAMETERS = {
|
|
12
|
-
name: { type: :string, required: true, description:
|
|
12
|
+
name: { type: :string, required: true, description: 'Name of the skill to load' }
|
|
13
13
|
}.freeze
|
|
14
14
|
RISK_LEVEL = :read
|
|
15
15
|
REQUIRES_CONFIRMATION = false
|
|
@@ -28,8 +28,8 @@ module RubynCode
|
|
|
28
28
|
|
|
29
29
|
def default_loader
|
|
30
30
|
skills_dirs = [
|
|
31
|
-
File.join(project_root,
|
|
32
|
-
File.join(Dir.home,
|
|
31
|
+
File.join(project_root, '.rubyn', 'skills'),
|
|
32
|
+
File.join(Dir.home, '.rubyn', 'skills')
|
|
33
33
|
]
|
|
34
34
|
catalog = Skills::Catalog.new(skills_dirs)
|
|
35
35
|
Skills::Loader.new(catalog)
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'registry'
|
|
5
5
|
|
|
6
6
|
module RubynCode
|
|
7
7
|
module Tools
|
|
8
8
|
class MemorySearch < Base
|
|
9
|
-
TOOL_NAME =
|
|
10
|
-
DESCRIPTION =
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
TOOL_NAME = 'memory_search'
|
|
10
|
+
DESCRIPTION = 'Searches project memories using full-text search. ' \
|
|
11
|
+
'Returns relevant memories including code patterns, user preferences, ' \
|
|
12
|
+
'project conventions, error resolutions, and past decisions.'
|
|
13
13
|
PARAMETERS = {
|
|
14
|
-
query: { type: :string, required: true, description:
|
|
15
|
-
tier: { type: :string, required: false, description:
|
|
16
|
-
category: { type: :string, required: false,
|
|
17
|
-
|
|
14
|
+
query: { type: :string, required: true, description: 'Search query for finding relevant memories' },
|
|
15
|
+
tier: { type: :string, required: false, description: 'Filter by memory tier: short, medium, or long' },
|
|
16
|
+
category: { type: :string, required: false,
|
|
17
|
+
description: 'Filter by category: code_pattern, user_preference, project_convention, error_resolution, or decision' },
|
|
18
|
+
limit: { type: :integer, required: false, description: 'Maximum number of results to return (default 10)' }
|
|
18
19
|
}.freeze
|
|
19
20
|
RISK_LEVEL = :read
|
|
20
21
|
REQUIRES_CONFIRMATION = false
|
|
@@ -55,9 +56,9 @@ module RubynCode
|
|
|
55
56
|
lines << "Tier: #{record.tier} | Category: #{record.category || 'none'}"
|
|
56
57
|
lines << "Relevance: #{format('%.2f', record.relevance_score)} | Accessed: #{record.access_count} times"
|
|
57
58
|
lines << "Created: #{record.created_at}"
|
|
58
|
-
lines <<
|
|
59
|
+
lines << ''
|
|
59
60
|
lines << record.content
|
|
60
|
-
lines <<
|
|
61
|
+
lines << ''
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
lines.join("\n")
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'registry'
|
|
5
5
|
|
|
6
6
|
module RubynCode
|
|
7
7
|
module Tools
|
|
8
8
|
class MemoryWrite < Base
|
|
9
|
-
TOOL_NAME =
|
|
10
|
-
DESCRIPTION =
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
TOOL_NAME = 'memory_write'
|
|
10
|
+
DESCRIPTION = 'Writes a new memory to the project memory store. ' \
|
|
11
|
+
'Use this to persist code patterns, user preferences, project conventions, ' \
|
|
12
|
+
'error resolutions, or architectural decisions for future reference.'
|
|
13
13
|
PARAMETERS = {
|
|
14
|
-
content: { type: :string, required: true, description:
|
|
15
|
-
tier: { type: :string, required: false,
|
|
16
|
-
|
|
14
|
+
content: { type: :string, required: true, description: 'The memory content to store' },
|
|
15
|
+
tier: { type: :string, required: false,
|
|
16
|
+
description: 'Memory retention tier: short, medium (default), or long' },
|
|
17
|
+
category: { type: :string, required: false,
|
|
18
|
+
description: 'Category: code_pattern, user_preference, project_convention, error_resolution, or decision' }
|
|
17
19
|
}.freeze
|
|
18
|
-
RISK_LEVEL = :read
|
|
20
|
+
RISK_LEVEL = :read # Memory is internal — no user approval needed
|
|
19
21
|
|
|
20
22
|
# @param project_root [String]
|
|
21
23
|
# @param memory_store [Memory::Store] injected store instance
|
|
@@ -28,12 +30,12 @@ module RubynCode
|
|
|
28
30
|
# @param tier [String] defaults to "medium"
|
|
29
31
|
# @param category [String, nil]
|
|
30
32
|
# @return [String] confirmation message
|
|
31
|
-
def execute(content:, tier:
|
|
33
|
+
def execute(content:, tier: 'medium', category: nil)
|
|
32
34
|
store = @memory_store || resolve_memory_store
|
|
33
35
|
record = store.write(content: content, tier: tier, category: category)
|
|
34
36
|
|
|
35
37
|
"Memory saved (ID: #{record.id}, tier: #{record.tier}" \
|
|
36
|
-
"#{
|
|
38
|
+
"#{", category: #{record.category}" if record.category})."
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
private
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
3
|
+
require 'open3'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative 'registry'
|
|
6
6
|
|
|
7
7
|
module RubynCode
|
|
8
8
|
module Tools
|
|
9
9
|
class RailsGenerate < Base
|
|
10
|
-
TOOL_NAME =
|
|
11
|
-
DESCRIPTION =
|
|
10
|
+
TOOL_NAME = 'rails_generate'
|
|
11
|
+
DESCRIPTION = 'Runs a Rails generator command. Validates that the project is a Rails application.'
|
|
12
12
|
PARAMETERS = {
|
|
13
|
-
generator: { type: :string, required: true,
|
|
14
|
-
|
|
13
|
+
generator: { type: :string, required: true,
|
|
14
|
+
description: "Generator name (e.g. 'model', 'controller', 'migration')" },
|
|
15
|
+
args: { type: :string, required: true,
|
|
16
|
+
description: "Arguments for the generator (e.g. 'User name:string email:string')" }
|
|
15
17
|
}.freeze
|
|
16
18
|
RISK_LEVEL = :execute
|
|
17
19
|
REQUIRES_CONFIRMATION = false
|
|
@@ -20,7 +22,7 @@ module RubynCode
|
|
|
20
22
|
validate_rails_project!
|
|
21
23
|
|
|
22
24
|
command = "bundle exec rails generate #{generator} #{args}"
|
|
23
|
-
stdout, stderr, status =
|
|
25
|
+
stdout, stderr, status = safe_capture3(command, chdir: project_root)
|
|
24
26
|
|
|
25
27
|
build_output(stdout, stderr, status)
|
|
26
28
|
end
|
|
@@ -28,16 +30,14 @@ module RubynCode
|
|
|
28
30
|
private
|
|
29
31
|
|
|
30
32
|
def validate_rails_project!
|
|
31
|
-
gemfile_path = File.join(project_root,
|
|
33
|
+
gemfile_path = File.join(project_root, 'Gemfile')
|
|
32
34
|
|
|
33
|
-
unless File.exist?(gemfile_path)
|
|
34
|
-
raise Error, "No Gemfile found. This does not appear to be a Ruby project."
|
|
35
|
-
end
|
|
35
|
+
raise Error, 'No Gemfile found. This does not appear to be a Ruby project.' unless File.exist?(gemfile_path)
|
|
36
36
|
|
|
37
37
|
gemfile_content = File.read(gemfile_path)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
return if gemfile_content.match?(/['"]rails['"]/)
|
|
39
|
+
|
|
40
|
+
raise Error, 'Gemfile does not include Rails. This does not appear to be a Rails project.'
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def build_output(stdout, stderr, status)
|
|
@@ -45,7 +45,7 @@ module RubynCode
|
|
|
45
45
|
parts << stdout unless stdout.empty?
|
|
46
46
|
parts << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
47
47
|
parts << "Exit code: #{status.exitstatus}" unless status.success?
|
|
48
|
-
parts.empty? ?
|
|
48
|
+
parts.empty? ? '(no output)' : parts.join("\n")
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'registry'
|
|
5
5
|
|
|
6
6
|
module RubynCode
|
|
7
7
|
module Tools
|
|
8
8
|
class ReadFile < Base
|
|
9
|
-
TOOL_NAME =
|
|
10
|
-
DESCRIPTION =
|
|
9
|
+
TOOL_NAME = 'read_file'
|
|
10
|
+
DESCRIPTION = 'Reads a file from the filesystem. Returns file content with line numbers prepended.'
|
|
11
11
|
PARAMETERS = {
|
|
12
|
-
path: { type: :string, required: true,
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
path: { type: :string, required: true,
|
|
13
|
+
description: 'Path to the file to read (relative to project root or absolute)' },
|
|
14
|
+
offset: { type: :integer, required: false, description: 'Line number to start reading from (1-based)' },
|
|
15
|
+
limit: { type: :integer, required: false, description: 'Number of lines to read' }
|
|
15
16
|
}.freeze
|
|
16
17
|
RISK_LEVEL = :read
|
|
17
18
|
REQUIRES_CONFIRMATION = false
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'registry'
|
|
5
5
|
|
|
6
6
|
module RubynCode
|
|
7
7
|
module Tools
|
|
8
8
|
# Tool for reading unread messages from a teammate's inbox.
|
|
9
9
|
class ReadInbox < Base
|
|
10
|
-
TOOL_NAME =
|
|
10
|
+
TOOL_NAME = 'read_inbox'
|
|
11
11
|
DESCRIPTION = "Reads all unread messages from the agent's inbox and marks them as read."
|
|
12
12
|
PARAMETERS = {
|
|
13
|
-
name: { type: :string, required: true, description:
|
|
13
|
+
name: { type: :string, required: true, description: 'The agent name whose inbox to read' }
|
|
14
14
|
}.freeze
|
|
15
15
|
RISK_LEVEL = :read
|
|
16
16
|
REQUIRES_CONFIRMATION = false
|
|
@@ -27,7 +27,7 @@ module RubynCode
|
|
|
27
27
|
# @param name [String] the reader's agent name
|
|
28
28
|
# @return [String] formatted messages or a notice if the inbox is empty
|
|
29
29
|
def execute(name:)
|
|
30
|
-
raise Error,
|
|
30
|
+
raise Error, 'Agent name is required' if name.nil? || name.strip.empty?
|
|
31
31
|
|
|
32
32
|
messages = @mailbox.read_inbox(name)
|
|
33
33
|
|
|
@@ -34,9 +34,9 @@ module RubynCode
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def load_all!
|
|
37
|
-
tool_files = Dir[File.join(__dir__,
|
|
37
|
+
tool_files = Dir[File.join(__dir__, '*.rb')]
|
|
38
38
|
tool_files.each do |file|
|
|
39
|
-
basename = File.basename(file,
|
|
39
|
+
basename = File.basename(file, '.rb')
|
|
40
40
|
next if %w[base registry schema executor].include?(basename)
|
|
41
41
|
|
|
42
42
|
require_relative basename
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'registry'
|
|
5
5
|
|
|
6
6
|
module RubynCode
|
|
7
7
|
module Tools
|
|
8
8
|
class ReviewPr < Base
|
|
9
|
-
TOOL_NAME =
|
|
10
|
-
DESCRIPTION =
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
TOOL_NAME = 'review_pr'
|
|
10
|
+
DESCRIPTION = 'Review current branch changes against Ruby/Rails best practices. ' \
|
|
11
|
+
'Gets the diff of the current branch vs the base branch, analyzes each changed file, ' \
|
|
12
|
+
'and provides actionable suggestions with explanations.'
|
|
13
13
|
PARAMETERS = {
|
|
14
14
|
base_branch: {
|
|
15
15
|
type: :string,
|
|
16
|
-
description:
|
|
16
|
+
description: 'Base branch to diff against (default: main)',
|
|
17
17
|
required: false
|
|
18
18
|
},
|
|
19
19
|
focus: {
|
|
@@ -24,25 +24,25 @@ module RubynCode
|
|
|
24
24
|
}.freeze
|
|
25
25
|
RISK_LEVEL = :read
|
|
26
26
|
|
|
27
|
-
def execute(base_branch:
|
|
27
|
+
def execute(base_branch: 'main', focus: 'all')
|
|
28
28
|
# Check git is available
|
|
29
|
-
unless system(
|
|
30
|
-
return
|
|
29
|
+
unless system('git rev-parse --is-inside-work-tree > /dev/null 2>&1', chdir: project_root)
|
|
30
|
+
return 'Error: Not a git repository or git is not installed.'
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Get current branch
|
|
34
|
-
current = run_git(
|
|
35
|
-
return
|
|
34
|
+
current = run_git('rev-parse --abbrev-ref HEAD').strip
|
|
35
|
+
return 'Error: Could not determine current branch.' if current.empty?
|
|
36
36
|
|
|
37
37
|
if current == base_branch
|
|
38
38
|
return "You're on #{base_branch}. Switch to a feature branch first, or specify a different base: review_pr(base_branch: 'develop')"
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# Check base branch exists
|
|
42
|
-
unless run_git("rev-parse --verify #{base_branch} 2>/dev/null").strip.length
|
|
42
|
+
unless run_git("rev-parse --verify #{base_branch} 2>/dev/null").strip.length.positive?
|
|
43
43
|
# Try origin/main
|
|
44
44
|
base_branch = "origin/#{base_branch}"
|
|
45
|
-
unless run_git("rev-parse --verify #{base_branch} 2>/dev/null").strip.length
|
|
45
|
+
unless run_git("rev-parse --verify #{base_branch} 2>/dev/null").strip.length.positive?
|
|
46
46
|
return "Error: Base branch '#{base_branch}' not found."
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -57,33 +57,33 @@ module RubynCode
|
|
|
57
57
|
commit_log = run_git("log #{base_branch}..HEAD --oneline")
|
|
58
58
|
|
|
59
59
|
# Build the review context
|
|
60
|
-
ruby_files = files_changed.
|
|
61
|
-
erb_files = files_changed.
|
|
62
|
-
spec_files = files_changed.
|
|
63
|
-
migration_files = files_changed.select { |f| f.include?(
|
|
64
|
-
config_files = files_changed.
|
|
60
|
+
ruby_files = files_changed.grep(/\.(rb|rake|gemspec|ru)$/)
|
|
61
|
+
erb_files = files_changed.grep(/\.(erb|haml|slim)$/)
|
|
62
|
+
spec_files = files_changed.grep(/_spec\.rb$|_test\.rb$/)
|
|
63
|
+
migration_files = files_changed.select { |f| f.include?('db/migrate') }
|
|
64
|
+
config_files = files_changed.grep(%r{config/|\.yml$|\.yaml$})
|
|
65
65
|
|
|
66
66
|
review = []
|
|
67
67
|
review << "# PR Review: #{current} → #{base_branch}"
|
|
68
|
-
review <<
|
|
69
|
-
review <<
|
|
68
|
+
review << ''
|
|
69
|
+
review << '## Summary'
|
|
70
70
|
review << stat
|
|
71
|
-
review <<
|
|
72
|
-
review <<
|
|
71
|
+
review << ''
|
|
72
|
+
review << '## Commits'
|
|
73
73
|
review << commit_log
|
|
74
|
-
review <<
|
|
75
|
-
review <<
|
|
74
|
+
review << ''
|
|
75
|
+
review << '## Files by Category'
|
|
76
76
|
review << "- Ruby: #{ruby_files.length} files" unless ruby_files.empty?
|
|
77
77
|
review << "- Templates: #{erb_files.length} files" unless erb_files.empty?
|
|
78
78
|
review << "- Specs: #{spec_files.length} files" unless spec_files.empty?
|
|
79
79
|
review << "- Migrations: #{migration_files.length} files" unless migration_files.empty?
|
|
80
80
|
review << "- Config: #{config_files.length} files" unless config_files.empty?
|
|
81
|
-
review <<
|
|
81
|
+
review << ''
|
|
82
82
|
|
|
83
83
|
# Add focus-specific review instructions
|
|
84
84
|
review << "## Review Focus: #{focus.upcase}"
|
|
85
85
|
review << review_instructions(focus)
|
|
86
|
-
review <<
|
|
86
|
+
review << ''
|
|
87
87
|
|
|
88
88
|
# Add the diff (truncated if too large)
|
|
89
89
|
if diff.length > 40_000
|
|
@@ -91,24 +91,24 @@ module RubynCode
|
|
|
91
91
|
review << diff[0...40_000]
|
|
92
92
|
review << "\n... [truncated #{diff.length - 40_000} chars]"
|
|
93
93
|
else
|
|
94
|
-
review <<
|
|
94
|
+
review << '## Full Diff'
|
|
95
95
|
review << diff
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
review <<
|
|
99
|
-
review <<
|
|
100
|
-
review <<
|
|
101
|
-
review <<
|
|
98
|
+
review << ''
|
|
99
|
+
review << '---'
|
|
100
|
+
review << 'Review this diff against Ruby/Rails best practices. For each issue found:'
|
|
101
|
+
review << '1. Quote the specific code'
|
|
102
102
|
review << "2. Explain what's wrong and WHY it matters"
|
|
103
|
-
review <<
|
|
104
|
-
review <<
|
|
105
|
-
review <<
|
|
106
|
-
review <<
|
|
107
|
-
review <<
|
|
108
|
-
review <<
|
|
109
|
-
review <<
|
|
110
|
-
review <<
|
|
111
|
-
review <<
|
|
103
|
+
review << '3. Show the suggested fix'
|
|
104
|
+
review << '4. Rate severity: [critical] [warning] [suggestion] [nitpick]'
|
|
105
|
+
review << ''
|
|
106
|
+
review << 'Also check for:'
|
|
107
|
+
review << '- Missing tests for new code'
|
|
108
|
+
review << '- N+1 queries in ActiveRecord changes'
|
|
109
|
+
review << '- Security issues (SQL injection, XSS, mass assignment)'
|
|
110
|
+
review << '- Missing database indexes for new associations'
|
|
111
|
+
review << '- Proper error handling'
|
|
112
112
|
|
|
113
113
|
truncate(review.join("\n"))
|
|
114
114
|
end
|
|
@@ -121,21 +121,21 @@ module RubynCode
|
|
|
121
121
|
|
|
122
122
|
def review_instructions(focus)
|
|
123
123
|
case focus.to_s.downcase
|
|
124
|
-
when
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
when
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
when
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
when
|
|
134
|
-
|
|
135
|
-
|
|
124
|
+
when 'security'
|
|
125
|
+
'Focus on: SQL injection, XSS, CSRF, mass assignment, authentication/authorization gaps, ' \
|
|
126
|
+
'sensitive data exposure, insecure dependencies, command injection, path traversal.'
|
|
127
|
+
when 'performance'
|
|
128
|
+
'Focus on: N+1 queries, missing indexes, eager loading, caching opportunities, ' \
|
|
129
|
+
'unnecessary database calls, memory bloat, slow iterations, missing pagination.'
|
|
130
|
+
when 'style'
|
|
131
|
+
'Focus on: Ruby idioms, naming conventions, method length, class organization, ' \
|
|
132
|
+
'frozen string literals, guard clauses, DRY violations, dead code.'
|
|
133
|
+
when 'testing'
|
|
134
|
+
'Focus on: Missing test coverage, test quality, factory usage, assertion quality, ' \
|
|
135
|
+
'test isolation, flaky test risks, edge cases, integration vs unit test balance.'
|
|
136
136
|
else
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
'Review all aspects: code quality, security, performance, testing, Rails conventions, ' \
|
|
138
|
+
'Ruby idioms, and architectural patterns.'
|
|
139
139
|
end
|
|
140
140
|
end
|
|
141
141
|
end
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
3
|
+
require 'open3'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative 'registry'
|
|
6
6
|
|
|
7
7
|
module RubynCode
|
|
8
8
|
module Tools
|
|
9
9
|
class RunSpecs < Base
|
|
10
|
-
TOOL_NAME =
|
|
11
|
-
DESCRIPTION =
|
|
10
|
+
TOOL_NAME = 'run_specs'
|
|
11
|
+
DESCRIPTION = 'Runs RSpec or Minitest specs. Auto-detects which test framework is in use.'
|
|
12
12
|
PARAMETERS = {
|
|
13
|
-
path: { type: :string, required: false, description:
|
|
14
|
-
format: { type: :string, required: false, default:
|
|
15
|
-
|
|
13
|
+
path: { type: :string, required: false, description: 'Specific test file or directory to run' },
|
|
14
|
+
format: { type: :string, required: false, default: 'documentation',
|
|
15
|
+
description: "Output format (default: 'documentation')" },
|
|
16
|
+
fail_fast: { type: :boolean, required: false, description: 'Stop on first failure' }
|
|
16
17
|
}.freeze
|
|
17
18
|
RISK_LEVEL = :execute
|
|
18
19
|
REQUIRES_CONFIRMATION = false
|
|
19
20
|
|
|
20
|
-
def execute(path: nil, format:
|
|
21
|
+
def execute(path: nil, format: 'documentation', fail_fast: false)
|
|
21
22
|
framework = detect_framework
|
|
22
23
|
|
|
23
24
|
command = build_command(framework, path, format, fail_fast)
|
|
24
|
-
stdout, stderr, status =
|
|
25
|
+
stdout, stderr, status = safe_capture3(command, chdir: project_root)
|
|
25
26
|
|
|
26
27
|
build_output(stdout, stderr, status)
|
|
27
28
|
end
|
|
@@ -29,7 +30,7 @@ module RubynCode
|
|
|
29
30
|
private
|
|
30
31
|
|
|
31
32
|
def detect_framework
|
|
32
|
-
gemfile_path = File.join(project_root,
|
|
33
|
+
gemfile_path = File.join(project_root, 'Gemfile')
|
|
33
34
|
|
|
34
35
|
if File.exist?(gemfile_path)
|
|
35
36
|
content = File.read(gemfile_path)
|
|
@@ -37,26 +38,26 @@ module RubynCode
|
|
|
37
38
|
return :minitest if content.match?(/['"]minitest['"]/)
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
return :rspec if File.exist?(File.join(project_root,
|
|
41
|
-
return :rspec if File.directory?(File.join(project_root,
|
|
42
|
-
return :minitest if File.directory?(File.join(project_root,
|
|
41
|
+
return :rspec if File.exist?(File.join(project_root, '.rspec'))
|
|
42
|
+
return :rspec if File.directory?(File.join(project_root, 'spec'))
|
|
43
|
+
return :minitest if File.directory?(File.join(project_root, 'test'))
|
|
43
44
|
|
|
44
|
-
raise Error,
|
|
45
|
+
raise Error, 'Could not detect test framework. Ensure RSpec or Minitest is configured.'
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def build_command(framework, path, format, fail_fast)
|
|
48
49
|
case framework
|
|
49
50
|
when :rspec
|
|
50
|
-
cmd =
|
|
51
|
+
cmd = 'bundle exec rspec'
|
|
51
52
|
cmd += " --format #{format}" if format
|
|
52
|
-
cmd +=
|
|
53
|
+
cmd += ' --fail-fast' if fail_fast
|
|
53
54
|
cmd += " #{path}" if path
|
|
54
55
|
cmd
|
|
55
56
|
when :minitest
|
|
56
57
|
if path
|
|
57
58
|
"bundle exec ruby -Itest #{path}"
|
|
58
59
|
else
|
|
59
|
-
|
|
60
|
+
'bundle exec rails test'
|
|
60
61
|
end
|
|
61
62
|
end
|
|
62
63
|
end
|
|
@@ -66,7 +67,7 @@ module RubynCode
|
|
|
66
67
|
parts << stdout unless stdout.empty?
|
|
67
68
|
parts << "STDERR:\n#{stderr}" unless stderr.empty?
|
|
68
69
|
parts << "Exit code: #{status.exitstatus}" unless status.success?
|
|
69
|
-
parts.empty? ?
|
|
70
|
+
parts.empty? ? '(no output)' : parts.join("\n")
|
|
70
71
|
end
|
|
71
72
|
end
|
|
72
73
|
|