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.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +269 -467
  3. data/db/migrations/009_create_teams.sql +6 -6
  4. data/db/migrations/011_fix_mailbox_messages_columns.rb +35 -0
  5. data/db/migrations/012_expand_mailbox_message_types.rb +37 -0
  6. data/exe/rubyn-code +1 -1
  7. data/lib/rubyn_code/agent/RUBYN.md +17 -0
  8. data/lib/rubyn_code/agent/conversation.rb +68 -19
  9. data/lib/rubyn_code/agent/loop.rb +312 -54
  10. data/lib/rubyn_code/agent/loop_detector.rb +6 -6
  11. data/lib/rubyn_code/auth/RUBYN.md +19 -0
  12. data/lib/rubyn_code/auth/oauth.rb +40 -35
  13. data/lib/rubyn_code/auth/server.rb +16 -12
  14. data/lib/rubyn_code/auth/token_store.rb +22 -22
  15. data/lib/rubyn_code/autonomous/RUBYN.md +14 -0
  16. data/lib/rubyn_code/autonomous/daemon.rb +115 -79
  17. data/lib/rubyn_code/autonomous/idle_poller.rb +4 -8
  18. data/lib/rubyn_code/autonomous/task_claimer.rb +11 -11
  19. data/lib/rubyn_code/background/RUBYN.md +13 -0
  20. data/lib/rubyn_code/background/notifier.rb +0 -2
  21. data/lib/rubyn_code/background/worker.rb +60 -15
  22. data/lib/rubyn_code/cli/RUBYN.md +30 -0
  23. data/lib/rubyn_code/cli/app.rb +85 -9
  24. data/lib/rubyn_code/cli/commands/RUBYN.md +133 -0
  25. data/lib/rubyn_code/cli/commands/base.rb +53 -0
  26. data/lib/rubyn_code/cli/commands/budget.rb +24 -0
  27. data/lib/rubyn_code/cli/commands/clear.rb +16 -0
  28. data/lib/rubyn_code/cli/commands/compact.rb +21 -0
  29. data/lib/rubyn_code/cli/commands/context.rb +44 -0
  30. data/lib/rubyn_code/cli/commands/context_info.rb +56 -0
  31. data/lib/rubyn_code/cli/commands/cost.rb +23 -0
  32. data/lib/rubyn_code/cli/commands/diff.rb +30 -0
  33. data/lib/rubyn_code/cli/commands/doctor.rb +112 -0
  34. data/lib/rubyn_code/cli/commands/help.rb +41 -0
  35. data/lib/rubyn_code/cli/commands/model.rb +37 -0
  36. data/lib/rubyn_code/cli/commands/plan.rb +22 -0
  37. data/lib/rubyn_code/cli/commands/quit.rb +17 -0
  38. data/lib/rubyn_code/cli/commands/registry.rb +64 -0
  39. data/lib/rubyn_code/cli/commands/resume.rb +51 -0
  40. data/lib/rubyn_code/cli/commands/review.rb +26 -0
  41. data/lib/rubyn_code/cli/commands/skill.rb +32 -0
  42. data/lib/rubyn_code/cli/commands/spawn.rb +24 -0
  43. data/lib/rubyn_code/cli/commands/tasks.rb +32 -0
  44. data/lib/rubyn_code/cli/commands/tokens.rb +76 -0
  45. data/lib/rubyn_code/cli/commands/undo.rb +17 -0
  46. data/lib/rubyn_code/cli/commands/version.rb +16 -0
  47. data/lib/rubyn_code/cli/daemon_runner.rb +129 -0
  48. data/lib/rubyn_code/cli/input_handler.rb +20 -23
  49. data/lib/rubyn_code/cli/renderer.rb +25 -27
  50. data/lib/rubyn_code/cli/repl.rb +161 -194
  51. data/lib/rubyn_code/cli/setup.rb +117 -0
  52. data/lib/rubyn_code/cli/spinner.rb +40 -40
  53. data/lib/rubyn_code/cli/stream_formatter.rb +29 -28
  54. data/lib/rubyn_code/cli/version_check.rb +94 -0
  55. data/lib/rubyn_code/config/RUBYN.md +14 -0
  56. data/lib/rubyn_code/config/defaults.rb +28 -19
  57. data/lib/rubyn_code/config/project_config.rb +7 -9
  58. data/lib/rubyn_code/config/settings.rb +3 -3
  59. data/lib/rubyn_code/context/RUBYN.md +20 -0
  60. data/lib/rubyn_code/context/auto_compact.rb +7 -7
  61. data/lib/rubyn_code/context/compactor.rb +2 -2
  62. data/lib/rubyn_code/context/context_collapse.rb +45 -0
  63. data/lib/rubyn_code/context/manager.rb +20 -3
  64. data/lib/rubyn_code/context/manual_compact.rb +7 -7
  65. data/lib/rubyn_code/context/micro_compact.rb +12 -12
  66. data/lib/rubyn_code/db/RUBYN.md +40 -0
  67. data/lib/rubyn_code/db/connection.rb +13 -13
  68. data/lib/rubyn_code/db/migrator.rb +67 -27
  69. data/lib/rubyn_code/db/schema.rb +6 -6
  70. data/lib/rubyn_code/debug.rb +74 -0
  71. data/lib/rubyn_code/hooks/RUBYN.md +17 -0
  72. data/lib/rubyn_code/hooks/built_in.rb +9 -9
  73. data/lib/rubyn_code/hooks/registry.rb +5 -5
  74. data/lib/rubyn_code/hooks/runner.rb +1 -1
  75. data/lib/rubyn_code/hooks/user_hooks.rb +16 -16
  76. data/lib/rubyn_code/learning/RUBYN.md +16 -0
  77. data/lib/rubyn_code/learning/extractor.rb +22 -22
  78. data/lib/rubyn_code/learning/injector.rb +17 -18
  79. data/lib/rubyn_code/learning/instinct.rb +18 -14
  80. data/lib/rubyn_code/llm/RUBYN.md +15 -0
  81. data/lib/rubyn_code/llm/client.rb +121 -55
  82. data/lib/rubyn_code/llm/message_builder.rb +19 -15
  83. data/lib/rubyn_code/llm/streaming.rb +80 -50
  84. data/lib/rubyn_code/mcp/RUBYN.md +21 -0
  85. data/lib/rubyn_code/mcp/client.rb +25 -24
  86. data/lib/rubyn_code/mcp/config.rb +7 -7
  87. data/lib/rubyn_code/mcp/sse_transport.rb +27 -26
  88. data/lib/rubyn_code/mcp/stdio_transport.rb +22 -19
  89. data/lib/rubyn_code/mcp/tool_bridge.rb +32 -32
  90. data/lib/rubyn_code/memory/RUBYN.md +17 -0
  91. data/lib/rubyn_code/memory/models.rb +3 -3
  92. data/lib/rubyn_code/memory/search.rb +17 -17
  93. data/lib/rubyn_code/memory/session_persistence.rb +49 -34
  94. data/lib/rubyn_code/memory/store.rb +17 -17
  95. data/lib/rubyn_code/observability/RUBYN.md +19 -0
  96. data/lib/rubyn_code/observability/budget_enforcer.rb +16 -15
  97. data/lib/rubyn_code/observability/cost_calculator.rb +3 -3
  98. data/lib/rubyn_code/observability/token_counter.rb +1 -1
  99. data/lib/rubyn_code/observability/usage_reporter.rb +35 -35
  100. data/lib/rubyn_code/output/RUBYN.md +11 -0
  101. data/lib/rubyn_code/output/diff_renderer.rb +6 -6
  102. data/lib/rubyn_code/output/formatter.rb +4 -4
  103. data/lib/rubyn_code/permissions/RUBYN.md +17 -0
  104. data/lib/rubyn_code/permissions/prompter.rb +8 -8
  105. data/lib/rubyn_code/protocols/RUBYN.md +14 -0
  106. data/lib/rubyn_code/protocols/interrupt_handler.rb +1 -1
  107. data/lib/rubyn_code/protocols/plan_approval.rb +9 -9
  108. data/lib/rubyn_code/protocols/shutdown_handshake.rb +9 -11
  109. data/lib/rubyn_code/skills/RUBYN.md +19 -0
  110. data/lib/rubyn_code/skills/catalog.rb +7 -7
  111. data/lib/rubyn_code/skills/document.rb +15 -15
  112. data/lib/rubyn_code/skills/loader.rb +6 -8
  113. data/lib/rubyn_code/sub_agents/RUBYN.md +12 -0
  114. data/lib/rubyn_code/sub_agents/runner.rb +15 -15
  115. data/lib/rubyn_code/sub_agents/summarizer.rb +1 -1
  116. data/lib/rubyn_code/tasks/RUBYN.md +13 -0
  117. data/lib/rubyn_code/tasks/dag.rb +12 -16
  118. data/lib/rubyn_code/tasks/manager.rb +24 -24
  119. data/lib/rubyn_code/tasks/models.rb +4 -4
  120. data/lib/rubyn_code/teams/RUBYN.md +14 -0
  121. data/lib/rubyn_code/teams/mailbox.rb +38 -18
  122. data/lib/rubyn_code/teams/manager.rb +19 -19
  123. data/lib/rubyn_code/teams/teammate.rb +3 -4
  124. data/lib/rubyn_code/tools/RUBYN.md +38 -0
  125. data/lib/rubyn_code/tools/background_run.rb +9 -11
  126. data/lib/rubyn_code/tools/base.rb +54 -3
  127. data/lib/rubyn_code/tools/bash.rb +16 -34
  128. data/lib/rubyn_code/tools/bundle_add.rb +10 -12
  129. data/lib/rubyn_code/tools/bundle_install.rb +9 -11
  130. data/lib/rubyn_code/tools/compact.rb +10 -9
  131. data/lib/rubyn_code/tools/db_migrate.rb +17 -15
  132. data/lib/rubyn_code/tools/edit_file.rb +12 -12
  133. data/lib/rubyn_code/tools/executor.rb +9 -4
  134. data/lib/rubyn_code/tools/git_commit.rb +29 -34
  135. data/lib/rubyn_code/tools/git_diff.rb +17 -18
  136. data/lib/rubyn_code/tools/git_log.rb +17 -19
  137. data/lib/rubyn_code/tools/git_status.rb +18 -20
  138. data/lib/rubyn_code/tools/glob.rb +7 -9
  139. data/lib/rubyn_code/tools/grep.rb +11 -9
  140. data/lib/rubyn_code/tools/load_skill.rb +7 -7
  141. data/lib/rubyn_code/tools/memory_search.rb +13 -12
  142. data/lib/rubyn_code/tools/memory_write.rb +14 -12
  143. data/lib/rubyn_code/tools/rails_generate.rb +16 -16
  144. data/lib/rubyn_code/tools/read_file.rb +8 -7
  145. data/lib/rubyn_code/tools/read_inbox.rb +5 -5
  146. data/lib/rubyn_code/tools/registry.rb +2 -2
  147. data/lib/rubyn_code/tools/review_pr.rb +55 -55
  148. data/lib/rubyn_code/tools/run_specs.rb +20 -19
  149. data/lib/rubyn_code/tools/schema.rb +9 -11
  150. data/lib/rubyn_code/tools/send_message.rb +10 -10
  151. data/lib/rubyn_code/tools/spawn_agent.rb +51 -23
  152. data/lib/rubyn_code/tools/spawn_teammate.rb +21 -21
  153. data/lib/rubyn_code/tools/task.rb +28 -28
  154. data/lib/rubyn_code/tools/web_fetch.rb +46 -31
  155. data/lib/rubyn_code/tools/web_search.rb +64 -66
  156. data/lib/rubyn_code/tools/write_file.rb +7 -6
  157. data/lib/rubyn_code/version.rb +1 -1
  158. data/lib/rubyn_code.rb +136 -105
  159. metadata +94 -21
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
- require_relative "registry"
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 = "glob"
10
- DESCRIPTION = "File pattern matching. Returns sorted list of file paths matching the glob pattern."
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: "Directory to search in (defaults to project root)" }
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?(".") && File.basename(f) == "." || File.basename(f) == ".." }
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 "base"
4
- require_relative "registry"
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 = "grep"
10
- DESCRIPTION = "Searches file contents using regular expressions. Returns matching lines with file paths and line numbers."
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: "Regular expression pattern to search for" },
13
- path: { type: :string, required: false, description: "File or directory to search in (defaults to project root)" },
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, description: "Maximum number of matching lines to return" }
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? { |b| b.zero? }
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 "base"
4
- require_relative "registry"
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 = "load_skill"
10
- DESCRIPTION = "Loads a skill document into the conversation context. Use /skill-name or provide the skill name."
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: "Name of the skill to load" }
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, ".rubyn", "skills"),
32
- File.join(Dir.home, ".rubyn", "skills")
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 "base"
4
- require_relative "registry"
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 = "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."
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: "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, description: "Filter by category: code_pattern, user_preference, project_convention, error_resolution, or decision" },
17
- limit: { type: :integer, required: false, description: "Maximum number of results to return (default 10)" }
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 "base"
4
- require_relative "registry"
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 = "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."
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: "The memory content to store" },
15
- tier: { type: :string, required: false, description: "Memory retention tier: short, medium (default), or long" },
16
- category: { type: :string, required: false, description: "Category: code_pattern, user_preference, project_convention, error_resolution, or decision" }
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 # Memory is internal — no user approval needed
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: "medium", category: nil)
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
- "#{record.category ? ", category: #{record.category}" : ''})."
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 "open3"
4
- require_relative "base"
5
- require_relative "registry"
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 = "rails_generate"
11
- DESCRIPTION = "Runs a Rails generator command. Validates that the project is a Rails application."
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, description: "Generator name (e.g. 'model', 'controller', 'migration')" },
14
- args: { type: :string, required: true, description: "Arguments for the generator (e.g. 'User name:string email:string')" }
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 = Open3.capture3(command, chdir: project_root)
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, "Gemfile")
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
- unless gemfile_content.match?(/['"]rails['"]/)
39
- raise Error, "Gemfile does not include Rails. This does not appear to be a Rails project."
40
- end
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? ? "(no output)" : parts.join("\n")
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 "base"
4
- require_relative "registry"
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 = "read_file"
10
- DESCRIPTION = "Reads a file from the filesystem. Returns file content with line numbers prepended."
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, description: "Path to the file to read (relative to project root or absolute)" },
13
- offset: { type: :integer, required: false, description: "Line number to start reading from (1-based)" },
14
- limit: { type: :integer, required: false, description: "Number of lines to read" }
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 "base"
4
- require_relative "registry"
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 = "read_inbox"
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: "The agent name whose inbox to read" }
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, "Agent name is required" if name.nil? || name.strip.empty?
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__, "*.rb")]
37
+ tool_files = Dir[File.join(__dir__, '*.rb')]
38
38
  tool_files.each do |file|
39
- basename = File.basename(file, ".rb")
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 "base"
4
- require_relative "registry"
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 = "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."
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: "Base branch to diff against (default: main)",
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: "main", focus: "all")
27
+ def execute(base_branch: 'main', focus: 'all')
28
28
  # Check git is available
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."
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("rev-parse --abbrev-ref HEAD").strip
35
- return "Error: Could not determine current branch." if current.empty?
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 > 0
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 > 0
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.select { |f| f.match?(/\.(rb|rake|gemspec|ru)$/) }
61
- erb_files = files_changed.select { |f| f.match?(/\.(erb|haml|slim)$/) }
62
- spec_files = files_changed.select { |f| f.match?(/_spec\.rb$|_test\.rb$/) }
63
- migration_files = files_changed.select { |f| f.include?("db/migrate") }
64
- config_files = files_changed.select { |f| f.match?(/config\/|\.yml$|\.yaml$/) }
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 << "## Summary"
68
+ review << ''
69
+ review << '## Summary'
70
70
  review << stat
71
- review << ""
72
- review << "## Commits"
71
+ review << ''
72
+ review << '## Commits'
73
73
  review << commit_log
74
- review << ""
75
- review << "## Files by Category"
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 << "## Full Diff"
94
+ review << '## Full Diff'
95
95
  review << diff
96
96
  end
97
97
 
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"
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 << "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"
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 "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."
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
- "Review all aspects: code quality, security, performance, testing, Rails conventions, " \
138
- "Ruby idioms, and architectural patterns."
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 "open3"
4
- require_relative "base"
5
- require_relative "registry"
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 = "run_specs"
11
- DESCRIPTION = "Runs RSpec or Minitest specs. Auto-detects which test framework is in use."
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: "Specific test file or directory to run" },
14
- format: { type: :string, required: false, default: "documentation", description: "Output format (default: 'documentation')" },
15
- fail_fast: { type: :boolean, required: false, description: "Stop on first failure" }
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: "documentation", fail_fast: false)
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 = Open3.capture3(command, chdir: project_root)
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, "Gemfile")
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, ".rspec"))
41
- return :rspec if File.directory?(File.join(project_root, "spec"))
42
- return :minitest if File.directory?(File.join(project_root, "test"))
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, "Could not detect test framework. Ensure RSpec or Minitest is configured."
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 = "bundle exec rspec"
51
+ cmd = 'bundle exec rspec'
51
52
  cmd += " --format #{format}" if format
52
- cmd += " --fail-fast" if fail_fast
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
- "bundle exec rails test"
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? ? "(no output)" : parts.join("\n")
70
+ parts.empty? ? '(no output)' : parts.join("\n")
70
71
  end
71
72
  end
72
73