rubycode 0.1.5 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50f110cea87f9ae98d0613a54a8feec231d1f050c62382b42428d4233bf871c0
4
- data.tar.gz: d9a0171878bdcbad488afd81146a6887a2ea64a8d361d5932f9e5e162202d36e
3
+ metadata.gz: 00132f001573596d0454dcb6765fc7c5fab4118e99d8755b67d0c7ff9140af81
4
+ data.tar.gz: 9f30d9d75328c0cfab381a02b1d457aaa67eef4f81cc5e535b8df25aae06e05a
5
5
  SHA512:
6
- metadata.gz: eecbe47b72a9ea6b36c905f5b8560a6f8611c381ec3de35d4a50db991dc6576442d04604c51f87ee45f253862c7771cfd70e644833f317d137c33e9d1f40bdf2
7
- data.tar.gz: 5c15e211f5605d8b986b49704b6d55ac3764364758efaca44d46f552a61ef6eecc993a8bf24eed55452776136859c361f91c1226c94cda7a32edf3f4ecc4c7d9
6
+ metadata.gz: 432f02b74ee9bdb44ddddc1d6aa5d8862eb7914081850e393775470dc79fa20d395a8b32d89539f3a280cb5c3c6e03221d75b5b4a266b4ac1ce033eddc1593b0
7
+ data.tar.gz: 35983e256acdd200562446e929f93217dcc9b6bbee6484043bba42ac227ee0e5e382aff1159c3921cb88e6387d89be7521d69d8c6bf4514c89b25fe2b5d29190
data/.rubocop.yml CHANGED
@@ -14,11 +14,21 @@ Style/OneClassPerFile:
14
14
 
15
15
  # Metrics - reasonable limits for maintainability
16
16
  Metrics/ClassLength:
17
- Max: 200
17
+ Max: 210
18
18
 
19
19
  Metrics/MethodLength:
20
20
  Max: 20
21
+ Exclude:
22
+ - "exe/**/*"
23
+ - "rubycode_cli.rb"
21
24
 
22
25
  Metrics/AbcSize:
23
26
  Exclude:
24
27
  - "test/**/*"
28
+ - "exe/**/*"
29
+ - "rubycode_cli.rb"
30
+
31
+ Metrics/CyclomaticComplexity:
32
+ Exclude:
33
+ - "exe/**/*"
34
+ - "rubycode_cli.rb"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.6] - 2026-03-08
4
+
5
+ ### Added
6
+ - **Plan Mode**: Interactive planning workflow with autonomous codebase exploration
7
+ - Type `plan mode` or `plan` to enter planning mode
8
+ - AI explores codebase using the explore tool
9
+ - User approval prompt after exploration: "Do you accept this plan?"
10
+ - Auto-approve automatically enabled for implementation if plan accepted
11
+ - Auto-approve disabled after implementation completes
12
+ - **Auto-Approve Commands**: Manual control over write operation approvals
13
+ - `auto-approve on` / `auto-approve write`: Enable auto-approval with confirmation prompt
14
+ - `auto-approve off`: Disable auto-approval
15
+ - `auto-approve` / `auto-approve status`: Check current auto-approve status
16
+ - **Explore Tool**: Autonomous codebase exploration agent (read-only)
17
+ - Spawns sub-agent with constrained toolset (bash, read, search, web_search, fetch, done)
18
+ - Configurable max iterations (default: 10, max: 15)
19
+ - Structured output format with summary, key files, code flow, and external resources
20
+ - Uses dedicated exploration prompt for systematic investigation
21
+
22
+ ### Changed
23
+ - **CLI Architecture**: Added plan_mode flag to ChatContext struct
24
+ - **Plan Mode Entry Message**: Updated to clearly explain the workflow
25
+ - **Process Flow**: Enhanced process_user_message to handle plan mode and auto-approve lifecycle
26
+ - **rubycode_cli.rb**: Synchronized with exe/rubycode_client to include all new features
27
+
28
+ ### Fixed
29
+ - **Special Command Handling**: Plan mode now properly handled as special command instead of being sent to AI
30
+
3
31
  ## [0.1.5] - 2026-03-08
4
32
 
5
33
  ### Added
@@ -0,0 +1,375 @@
1
+ # Explore Tool Design for RubyCode
2
+
3
+ ## Overview
4
+ An intelligent codebase exploration tool that combines file discovery, content search, and automatic summarization.
5
+
6
+ ## How Claude Code's Explore Works
7
+
8
+ Claude Code's Explore agent:
9
+ 1. Takes a natural language query (e.g., "how does authentication work?")
10
+ 2. Automatically uses multiple tools (Glob, Grep, Read) in sequence
11
+ 3. Follows breadcrumbs and references
12
+ 4. Builds a mental map of the codebase
13
+ 5. Returns a comprehensive answer
14
+
15
+ ## Implementation Approach for RubyCode
16
+
17
+ ### Option 1: Single Explore Tool (Recommended)
18
+ Create a new `explore` tool that acts as an intelligent wrapper.
19
+
20
+ **Tool Definition** (`config/tools/explore.json`):
21
+ ```json
22
+ {
23
+ "type": "function",
24
+ "function": {
25
+ "name": "explore",
26
+ "description": "Intelligently explore the codebase to answer questions. Automatically finds relevant files, searches content, and builds context. Use this for questions like 'how does X work?', 'where is Y implemented?', 'what files handle Z?'",
27
+ "parameters": {
28
+ "type": "object",
29
+ "properties": {
30
+ "query": {
31
+ "type": "string",
32
+ "description": "Natural language question or exploration goal (e.g., 'how does user authentication work?')"
33
+ },
34
+ "thoroughness": {
35
+ "type": "string",
36
+ "enum": ["quick", "medium", "thorough"],
37
+ "description": "How deep to explore. 'quick' = 1-2 passes, 'medium' = 3-5 passes, 'thorough' = 5-10 passes. Default: 'medium'"
38
+ },
39
+ "focus_paths": {
40
+ "type": "array",
41
+ "items": {"type": "string"},
42
+ "description": "Optional array of directory paths to focus on (e.g., ['app/models', 'lib/auth'])"
43
+ }
44
+ },
45
+ "required": ["query"]
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ **Implementation** (`lib/rubycode/tools/explore.rb`):
52
+ ```ruby
53
+ module RubyCode
54
+ module Tools
55
+ class Explore < Base
56
+ def execute(params)
57
+ query = params["query"]
58
+ thoroughness = params["thoroughness"] || "medium"
59
+ focus_paths = params["focus_paths"] || ["."]
60
+
61
+ # Create a mini-agent that explores
62
+ explorer = Explorer.new(
63
+ root_path: context[:root_path],
64
+ query: query,
65
+ thoroughness: thoroughness,
66
+ focus_paths: focus_paths
67
+ )
68
+
69
+ result = explorer.run
70
+
71
+ CommandResult.new(
72
+ success: true,
73
+ output: result.summary,
74
+ metadata: {
75
+ files_examined: result.files_examined,
76
+ patterns_found: result.patterns_found
77
+ }
78
+ )
79
+ end
80
+ end
81
+ end
82
+ end
83
+ ```
84
+
85
+ **Explorer Engine** (`lib/rubycode/explorer.rb`):
86
+ ```ruby
87
+ module RubyCode
88
+ class Explorer
89
+ MAX_ITERATIONS = {
90
+ "quick" => 3,
91
+ "medium" => 7,
92
+ "thorough" => 15
93
+ }.freeze
94
+
95
+ def initialize(root_path:, query:, thoroughness:, focus_paths:)
96
+ @root_path = root_path
97
+ @query = query
98
+ @max_iterations = MAX_ITERATIONS[thoroughness]
99
+ @focus_paths = focus_paths
100
+ @examined_files = []
101
+ @findings = []
102
+ end
103
+
104
+ def run
105
+ # Phase 1: Keyword extraction
106
+ keywords = extract_keywords(@query)
107
+
108
+ # Phase 2: File discovery
109
+ candidate_files = discover_files(keywords)
110
+
111
+ # Phase 3: Content analysis
112
+ analyze_content(candidate_files, keywords)
113
+
114
+ # Phase 4: Follow references
115
+ follow_references(keywords)
116
+
117
+ # Phase 5: Summarize findings
118
+ summarize
119
+ end
120
+
121
+ private
122
+
123
+ def extract_keywords(query)
124
+ # Extract relevant keywords from query
125
+ # Remove stop words, extract technical terms
126
+ words = query.downcase.split(/\s+/)
127
+ stop_words = %w[how does what where is the a an in on at to for of with]
128
+ words - stop_words
129
+ end
130
+
131
+ def discover_files(keywords)
132
+ files = []
133
+
134
+ # Try file name patterns
135
+ keywords.each do |keyword|
136
+ # Try exact match
137
+ files += glob_search("**/*#{keyword}*")
138
+
139
+ # Try snake_case variant
140
+ snake = keyword.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
141
+ files += glob_search("**/*#{snake}*")
142
+
143
+ # Try camel_case variant
144
+ camel = keyword.split('_').map(&:capitalize).join
145
+ files += glob_search("**/*#{camel}*")
146
+ end
147
+
148
+ files.uniq
149
+ end
150
+
151
+ def analyze_content(files, keywords)
152
+ files.first(20).each do |file|
153
+ next if @examined_files.include?(file)
154
+
155
+ content = read_file(file)
156
+ next unless content
157
+
158
+ # Check if file is relevant
159
+ relevance_score = calculate_relevance(content, keywords)
160
+
161
+ if relevance_score > 0.3
162
+ @findings << {
163
+ file: file,
164
+ score: relevance_score,
165
+ snippets: extract_relevant_snippets(content, keywords)
166
+ }
167
+ end
168
+
169
+ @examined_files << file
170
+ end
171
+ end
172
+
173
+ def follow_references(keywords)
174
+ # Look for requires, includes, imports
175
+ @findings.each do |finding|
176
+ content = read_file(finding[:file])
177
+
178
+ # Extract references (require, include, etc.)
179
+ references = extract_references(content)
180
+
181
+ # Recursively explore referenced files
182
+ references.each do |ref|
183
+ ref_file = resolve_reference(ref)
184
+ next unless ref_file
185
+
186
+ analyze_content([ref_file], keywords)
187
+ end
188
+ end
189
+ end
190
+
191
+ def summarize
192
+ # Build a structured summary
193
+ summary = "# Exploration Results for: #{@query}\n\n"
194
+ summary += "Examined #{@examined_files.length} files\n\n"
195
+
196
+ # Group findings by relevance
197
+ top_findings = @findings.sort_by { |f| -f[:score] }.first(10)
198
+
199
+ summary += "## Most Relevant Files:\n\n"
200
+ top_findings.each do |finding|
201
+ summary += "**#{finding[:file]}** (relevance: #{(finding[:score] * 100).round}%)\n"
202
+ finding[:snippets].each do |snippet|
203
+ summary += " - Line #{snippet[:line]}: #{snippet[:text]}\n"
204
+ end
205
+ summary += "\n"
206
+ end
207
+
208
+ ExplorerResult.new(
209
+ summary: summary,
210
+ files_examined: @examined_files.length,
211
+ patterns_found: @findings.length
212
+ )
213
+ end
214
+
215
+ # Helper methods
216
+ def glob_search(pattern)
217
+ Dir.glob(File.join(@root_path, pattern)).select { |f| File.file?(f) }
218
+ end
219
+
220
+ def read_file(path)
221
+ File.read(path) rescue nil
222
+ end
223
+
224
+ def calculate_relevance(content, keywords)
225
+ matches = keywords.sum { |kw| content.scan(/#{Regexp.escape(kw)}/i).length }
226
+ matches.to_f / (content.length / 100.0)
227
+ end
228
+
229
+ def extract_relevant_snippets(content, keywords)
230
+ snippets = []
231
+ content.lines.each_with_index do |line, idx|
232
+ if keywords.any? { |kw| line.match?(/#{Regexp.escape(kw)}/i) }
233
+ snippets << { line: idx + 1, text: line.strip }
234
+ end
235
+ end
236
+ snippets.first(5)
237
+ end
238
+
239
+ def extract_references(content)
240
+ references = []
241
+
242
+ # Ruby requires
243
+ content.scan(/require\s+['"](.+?)['"]/) { references << $1 }
244
+ content.scan(/require_relative\s+['"](.+?)['"]/) { references << $1 }
245
+
246
+ references
247
+ end
248
+
249
+ def resolve_reference(ref)
250
+ # Try to find the actual file
251
+ possible_paths = [
252
+ "#{ref}.rb",
253
+ "lib/#{ref}.rb",
254
+ "app/#{ref}.rb"
255
+ ]
256
+
257
+ possible_paths.find { |p| File.exist?(File.join(@root_path, p)) }
258
+ end
259
+ end
260
+
261
+ ExplorerResult = Struct.new(:summary, :files_examined, :patterns_found, keyword_init: true)
262
+ end
263
+ ```
264
+
265
+ ### Option 2: Sub-Agent Approach (More Advanced)
266
+
267
+ Create a separate mini-agent that has access to a limited set of tools and runs autonomously:
268
+
269
+ ```ruby
270
+ class ExploreAgent
271
+ def initialize(query:, root_path:, thoroughness:)
272
+ @query = query
273
+ @root_path = root_path
274
+ @memory = Memory.new
275
+ @adapter = RubyCode.configuration.adapter
276
+ @max_iterations = thoroughness_to_iterations(thoroughness)
277
+ end
278
+
279
+ def run
280
+ # Give the sub-agent a focused prompt
281
+ prompt = build_exploration_prompt(@query)
282
+ @memory.add_message(role: "user", content: prompt)
283
+
284
+ # Run mini agent loop with limited tools
285
+ agent_loop = AgentLoop.new(
286
+ adapter: @adapter,
287
+ memory: @memory,
288
+ config: exploration_config,
289
+ system_prompt: exploration_system_prompt,
290
+ max_iterations: @max_iterations
291
+ )
292
+
293
+ agent_loop.run
294
+ end
295
+
296
+ private
297
+
298
+ def exploration_config
299
+ # Limited config for sub-agent
300
+ config = RubyCode::Configuration.new
301
+ config.root_path = @root_path
302
+ config.memory_window = 5 # Smaller window
303
+ config
304
+ end
305
+
306
+ def exploration_system_prompt
307
+ <<~PROMPT
308
+ You are an expert codebase explorer. Your goal is to thoroughly explore
309
+ the codebase to answer the user's question. You have access to these tools:
310
+
311
+ - bash: Run ls, find, tree commands to discover files
312
+ - search: Search file contents for patterns
313
+ - read: Read specific files
314
+ - done: Finish with your findings
315
+
316
+ Be thorough but efficient. Follow these steps:
317
+ 1. Identify key terms from the question
318
+ 2. Search for relevant files and code
319
+ 3. Read the most promising files
320
+ 4. Follow references and imports
321
+ 5. Summarize your findings
322
+
323
+ Use 'done' when you have a comprehensive answer.
324
+ PROMPT
325
+ end
326
+
327
+ def build_exploration_prompt(query)
328
+ "Explore the codebase to answer this question: #{query}\n\n" \
329
+ "Provide a comprehensive answer with file references and code snippets."
330
+ end
331
+ end
332
+ ```
333
+
334
+ ## Comparison
335
+
336
+ | Approach | Pros | Cons |
337
+ |----------|------|------|
338
+ | **Single Tool** | Simple, fast, deterministic | Less flexible, hardcoded logic |
339
+ | **Sub-Agent** | Intelligent, adaptable, can reason | Higher token cost, slower |
340
+
341
+ ## Recommendation
342
+
343
+ **Start with Option 1 (Single Tool)**, then evolve to Option 2 if needed:
344
+
345
+ 1. **Phase 1**: Implement basic explore tool with keyword extraction
346
+ 2. **Phase 2**: Add pattern matching and file scoring
347
+ 3. **Phase 3**: Add reference following
348
+ 4. **Phase 4**: Consider sub-agent for complex queries
349
+
350
+ ## Token Efficiency
351
+
352
+ The sub-agent approach uses more tokens but provides better results:
353
+
354
+ - **Single tool**: ~500-1000 tokens per exploration
355
+ - **Sub-agent**: ~5000-15000 tokens per exploration
356
+
357
+ You could make it configurable:
358
+
359
+ ```ruby
360
+ # Quick exploration (single tool)
361
+ explore(query: "find authentication", mode: "fast")
362
+
363
+ # Deep exploration (sub-agent)
364
+ explore(query: "how does authentication work?", mode: "deep")
365
+ ```
366
+
367
+ ## Next Steps
368
+
369
+ 1. Create `lib/rubycode/tools/explore.rb`
370
+ 2. Create `lib/rubycode/explorer.rb`
371
+ 3. Create `config/tools/explore.json`
372
+ 4. Add tests in `test/test_explore_tool.rb`
373
+ 5. Update README with explore tool documentation
374
+
375
+ Would you like me to implement the basic version (Option 1)?
@@ -0,0 +1,56 @@
1
+ # Codebase Explorer
2
+
3
+ You are an expert codebase explorer. Your goal is to thoroughly explore the codebase to answer the user's question.
4
+
5
+ **IMPORTANT: This is READ-ONLY exploration. You cannot modify any files.**
6
+
7
+ ## Available Tools
8
+
9
+ - **bash**: Run ls, find, tree, grep commands to discover files (read-only commands only)
10
+ - **read**: Read file contents with line numbers
11
+ - **search**: Search file contents for patterns using regex
12
+ - **web_search**: Search the internet for documentation, examples, best practices
13
+ - **fetch**: Fetch documentation from URLs
14
+ - **done**: Signal completion with your findings
15
+
16
+ ## Exploration Strategy
17
+
18
+ Follow these steps for thorough exploration:
19
+
20
+ 1. **Extract keywords** from the user's query
21
+ 2. **Discover files** using bash (find, ls, grep) and search tools
22
+ 3. **Read promising files** to understand implementation
23
+ 4. **Look up documentation** if needed using web_search or fetch
24
+ 5. **Follow references** and imports to related code
25
+ 6. **Synthesize answer** and call done with structured summary
26
+
27
+ ## Output Format
28
+
29
+ When you finish exploring, use the 'done' tool with this structured format:
30
+
31
+ ```markdown
32
+ ## Summary
33
+ [1-3 sentence answer to the user's question]
34
+
35
+ ## Key Files
36
+ - path/to/file.rb: [brief description of what this file does]
37
+ - path/to/other.rb: [brief description of what this file does]
38
+
39
+ ## Code Flow
40
+ [If applicable, explain how the components interact and data flows through the system]
41
+
42
+ ## External Resources
43
+ [If you used web_search/fetch, list relevant documentation URLs with brief descriptions]
44
+
45
+ ## Additional Notes
46
+ [Any important caveats, patterns, or recommendations]
47
+ ```
48
+
49
+ ## Important Reminders
50
+
51
+ - **READ-ONLY MODE**: You cannot write, update, or modify any files
52
+ - Be thorough but efficient with your iterations
53
+ - Actually read the files you identify as important
54
+ - If stuck, try web search for documentation
55
+ - Focus on answering the user's specific question, not general exploration
56
+ - Your findings will help the user plan their implementation
@@ -0,0 +1,22 @@
1
+ {
2
+ "type": "function",
3
+ "function": {
4
+ "name": "explore",
5
+ "description": "Autonomously explore the codebase to answer questions about architecture, implementation, and code flow. READ-ONLY exploration - no files will be modified. The agent will use bash, read, search, web_search, and fetch tools to thoroughly investigate the codebase and provide findings. Use this for questions like 'how does authentication work?', 'where is user management implemented?', 'what files handle routing?'",
6
+ "parameters": {
7
+ "type": "object",
8
+ "properties": {
9
+ "query": {
10
+ "type": "string",
11
+ "description": "The exploration question or goal (e.g., 'how does user authentication work in this codebase?')"
12
+ },
13
+ "max_iterations": {
14
+ "type": "integer",
15
+ "description": "Maximum number of exploration iterations (default: 10, max: 15). Higher values allow more thorough exploration but use more tokens.",
16
+ "default": 10
17
+ }
18
+ },
19
+ "required": ["query"]
20
+ }
21
+ }
22
+ }
data/exe/rubycode_client CHANGED
@@ -206,7 +206,7 @@ if adapter_info[:requires_key]
206
206
  end
207
207
  end
208
208
 
209
- ChatContext = Struct.new(:prompt, :client, :adapter, :model, :full_path, :debug_mode)
209
+ ChatContext = Struct.new(:prompt, :client, :adapter, :model, :full_path, :debug_mode, :plan_mode)
210
210
 
211
211
  def run_chat_loop(context)
212
212
  loop do
@@ -215,7 +215,7 @@ def run_chat_loop(context)
215
215
 
216
216
  next if handle_special_command(user_input, context)
217
217
 
218
- process_user_message(context.client, user_input)
218
+ process_user_message(context.client, user_input, context)
219
219
  end
220
220
  end
221
221
 
@@ -237,6 +237,32 @@ def handle_special_command(input, context)
237
237
  when "config"
238
238
  show_config_and_reconfigure(context)
239
239
  true
240
+ when "plan", "plan mode"
241
+ puts RubyCode::Views::Cli::PlanModeEnter.build
242
+ context.plan_mode = true
243
+ true
244
+ when "auto-approve on", "auto-approve write"
245
+ confirmed = context.prompt.yes?(
246
+ "⚠ Enable auto-approve for write/update operations?\n" \
247
+ "Files will be modified without confirmation.",
248
+ default: false
249
+ )
250
+
251
+ if confirmed
252
+ context.client.approval_handler.enable_auto_approve_write
253
+ puts RubyCode::Views::Cli::AutoApproveEnabled.build
254
+ else
255
+ puts "\nAuto-approve NOT enabled.\n\n"
256
+ end
257
+ true
258
+ when "auto-approve off"
259
+ context.client.approval_handler.disable_auto_approve_write
260
+ puts RubyCode::Views::Cli::AutoApproveDisabled.build
261
+ true
262
+ when "auto-approve", "auto-approve status"
263
+ status = context.client.approval_handler.auto_approve_write_enabled
264
+ puts RubyCode::Views::Cli::AutoApproveStatus.build(enabled: status)
265
+ true
240
266
  else
241
267
  false
242
268
  end
@@ -247,7 +273,8 @@ def show_config_and_reconfigure(context)
247
273
  adapter: context.adapter,
248
274
  model: context.model,
249
275
  directory: context.full_path,
250
- debug_mode: context.debug_mode
276
+ debug_mode: context.debug_mode,
277
+ auto_approve: context.client.approval_handler.auto_approve_write_enabled
251
278
  )
252
279
 
253
280
  return unless context.prompt.yes?(I18n.t("rubycode.setup.reconfigure"), default: false)
@@ -256,9 +283,40 @@ def show_config_and_reconfigure(context)
256
283
  setup_wizard(context.prompt)
257
284
  end
258
285
 
259
- def process_user_message(client, user_input)
260
- response = client.ask(prompt: user_input)
261
- puts RubyCode::Views::Cli::ResponseBox.build(response: response)
286
+ def process_user_message(client, user_input, context)
287
+ if context.plan_mode
288
+ # In plan mode - use explore tool then ask for approval
289
+ response = client.ask(prompt: "Use the explore tool with this query: #{user_input}")
290
+ puts RubyCode::Views::Cli::ResponseBox.build(response: response)
291
+
292
+ # Ask user if they accept the plan
293
+ accept_plan = context.prompt.yes?("\nDo you accept this plan and want to proceed with implementation?",
294
+ default: true)
295
+
296
+ if accept_plan
297
+ puts "\n✓ Plan accepted. Auto-approve enabled for implementation.\n"
298
+ # Enable auto-approve for implementation
299
+ client.approval_handler.enable_auto_approve_write
300
+
301
+ # Ask for implementation prompt
302
+ impl_prompt = context.prompt.ask("Describe what you want to implement:")
303
+ response = client.ask(prompt: impl_prompt)
304
+ puts RubyCode::Views::Cli::ResponseBox.build(response: response)
305
+
306
+ # Disable auto-approve after implementation
307
+ client.approval_handler.disable_auto_approve_write
308
+ else
309
+ puts "\n✗ Plan rejected. Returning to normal mode.\n"
310
+ end
311
+
312
+ # Exit plan mode after exploration
313
+ context.plan_mode = false
314
+ puts RubyCode::Views::Cli::PlanModeExit.build
315
+ else
316
+ # Normal mode
317
+ response = client.ask(prompt: user_input)
318
+ puts RubyCode::Views::Cli::ResponseBox.build(response: response)
319
+ end
262
320
  rescue Interrupt
263
321
  puts RubyCode::Views::Cli::InterruptMessage.build
264
322
  rescue StandardError => e
@@ -270,5 +328,6 @@ client = RubyCode::Client.new(tty_prompt: prompt)
270
328
  puts RubyCode::Views::Cli::ReadyMessage.build
271
329
 
272
330
  debug_mode = false # Debug mode not yet implemented
273
- context = ChatContext.new(prompt, client, adapter, model, full_path, debug_mode)
331
+ plan_mode = false # Plan mode flag
332
+ context = ChatContext.new(prompt, client, adapter, model, full_path, debug_mode, plan_mode)
274
333
  run_chat_loop(context)
@@ -11,6 +11,8 @@ module RubyCode
11
11
  MAX_TOOL_CALLS = 50
12
12
  MAX_CONSECUTIVE_RATE_LIMIT_ERRORS = 3
13
13
 
14
+ attr_reader :approval_handler
15
+
14
16
  def initialize(adapter:, memory:, config:, system_prompt:, options: {})
15
17
  @adapter = adapter
16
18
  @memory = memory
@@ -18,9 +20,11 @@ module RubyCode
18
20
  @system_prompt = system_prompt
19
21
  @read_files = options[:read_files]
20
22
  @tty_prompt = options[:tty_prompt]
23
+ @options = options
21
24
  @response_handler = Client::ResponseHandler.new(memory: @memory, config: @config)
22
25
  @display_formatter = Client::DisplayFormatter.new(config: @config)
23
- @approval_handler = Client::ApprovalHandler.new(tty_prompt: @tty_prompt, config: @config)
26
+ @approval_handler = options[:approval_handler] ||
27
+ Client::ApprovalHandler.new(tty_prompt: @tty_prompt, config: @config)
24
28
  @consecutive_rate_limit_errors = 0
25
29
  end
26
30
 
@@ -101,10 +105,20 @@ module RubyCode
101
105
  window_size: @config.memory_window,
102
106
  prune_tool_results: @config.prune_tool_results
103
107
  )
108
+
109
+ # Filter tools if allowed_tools is specified
110
+ tools_to_send = if @options[:allowed_tools]
111
+ Tools.definitions.select do |t|
112
+ @options[:allowed_tools].include?(t[:function][:name])
113
+ end
114
+ else
115
+ Tools.definitions
116
+ end
117
+
104
118
  @adapter.generate(
105
119
  messages: messages,
106
120
  system: @system_prompt,
107
- tools: Tools.definitions
121
+ tools: tools_to_send
108
122
  )
109
123
  end
110
124
 
@@ -4,9 +4,20 @@ module RubyCode
4
4
  class Client
5
5
  # Handles user approval prompts for tools
6
6
  class ApprovalHandler
7
+ attr_reader :auto_approve_write_enabled
8
+
7
9
  def initialize(tty_prompt:, config:)
8
10
  @prompt = tty_prompt
9
11
  @config = config
12
+ @auto_approve_write_enabled = false
13
+ end
14
+
15
+ def enable_auto_approve_write
16
+ @auto_approve_write_enabled = true
17
+ end
18
+
19
+ def disable_auto_approve_write
20
+ @auto_approve_write_enabled = false
10
21
  end
11
22
 
12
23
  def request_bash_approval(command, base_command, safe_commands)
@@ -25,6 +36,8 @@ module RubyCode
25
36
  end
26
37
 
27
38
  def request_write_approval(file_path, content)
39
+ return true if @auto_approve_write_enabled
40
+
28
41
  display = Views::WriteApproval.build(
29
42
  file_path: file_path,
30
43
  content: content
@@ -39,6 +52,8 @@ module RubyCode
39
52
  end
40
53
 
41
54
  def request_update_approval(file_path, old_string, new_string)
55
+ return true if @auto_approve_write_enabled
56
+
42
57
  display = Views::UpdateApproval.build(
43
58
  file_path: file_path,
44
59
  old_string: old_string,
@@ -5,7 +5,7 @@ require "set"
5
5
  module RubyCode
6
6
  # Main client that provides the public API for the agent
7
7
  class Client
8
- attr_reader :memory
8
+ attr_reader :memory, :approval_handler
9
9
 
10
10
  def initialize(tty_prompt: nil)
11
11
  @config = RubyCode.config
@@ -14,6 +14,10 @@ module RubyCode
14
14
  @memory.clear # Clear memory at start of each session to prevent payload size issues
15
15
  @read_files = Set.new
16
16
  @tty_prompt = tty_prompt
17
+ @approval_handler = Client::ApprovalHandler.new(
18
+ tty_prompt: @tty_prompt,
19
+ config: @config
20
+ )
17
21
  end
18
22
 
19
23
  def ask(prompt:)
@@ -25,7 +29,11 @@ module RubyCode
25
29
  memory: @memory,
26
30
  config: @config,
27
31
  system_prompt: system_prompt,
28
- options: { read_files: @read_files, tty_prompt: @tty_prompt }
32
+ options: {
33
+ read_files: @read_files,
34
+ tty_prompt: @tty_prompt,
35
+ approval_handler: @approval_handler
36
+ }
29
37
  ).run
30
38
  end
31
39
 
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCode
4
+ # Explorer engine that spawns a sub-agent for autonomous codebase exploration
5
+ class Explorer
6
+ MAX_ITERATIONS = 15
7
+ ALLOWED_TOOLS = %w[bash read search web_search fetch done].freeze
8
+
9
+ def initialize(adapter:, config:, query:, max_iterations: 10)
10
+ @adapter = adapter
11
+ @config = config
12
+ @query = query
13
+ @max_iterations = max_iterations.clamp(1, MAX_ITERATIONS)
14
+ @memory = Memory.new
15
+ end
16
+
17
+ def explore
18
+ # Add user query to memory
19
+ @memory.add_message(role: "user", content: @query)
20
+
21
+ # Build and run constrained agent loop
22
+ agent_loop = build_constrained_loop
23
+
24
+ puts "\n🔍 Exploring codebase...\n"
25
+ result = agent_loop.run
26
+
27
+ puts "\n✓ Exploration complete\n\n"
28
+
29
+ result
30
+ end
31
+
32
+ private
33
+
34
+ def build_constrained_loop
35
+ # Load exploration prompt
36
+ system_prompt = File.read(File.join(__dir__, "../../config/exploration_prompt.md"))
37
+
38
+ AgentLoop.new(
39
+ adapter: @adapter,
40
+ memory: @memory,
41
+ config: @config,
42
+ system_prompt: system_prompt,
43
+ options: {
44
+ max_iterations: @max_iterations,
45
+ allowed_tools: ALLOWED_TOOLS,
46
+ read_files: Set.new,
47
+ tty_prompt: TTY::Prompt.new,
48
+ approval_handler: create_explorer_approval_handler
49
+ }
50
+ )
51
+ end
52
+
53
+ def create_explorer_approval_handler
54
+ # Create approval handler for web_search and fetch (bash is still approved per safe-list)
55
+ Client::ApprovalHandler.new(
56
+ tty_prompt: TTY::Prompt.new,
57
+ config: @config
58
+ )
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCode
4
+ module Tools
5
+ # Explore tool - spawns a sub-agent for autonomous codebase exploration
6
+ class Explore < Base
7
+ def perform(params)
8
+ query = params["query"]
9
+ max_iterations = params["max_iterations"] || 10
10
+
11
+ # Build a fresh adapter for the sub-agent
12
+ adapter = build_adapter
13
+
14
+ # Create explorer and run
15
+ explorer = Explorer.new(
16
+ adapter: adapter,
17
+ config: context[:config] || RubyCode.config,
18
+ query: query,
19
+ max_iterations: max_iterations
20
+ )
21
+
22
+ explorer.explore
23
+
24
+ # Return the exploration result
25
+ end
26
+
27
+ private
28
+
29
+ def build_adapter
30
+ config = context[:config] || RubyCode.config
31
+
32
+ case config.adapter
33
+ when :ollama
34
+ Adapters::Ollama.new(config)
35
+ when :openrouter
36
+ Adapters::Openrouter.new(config)
37
+ when :deepseek
38
+ Adapters::Deepseek.new(config)
39
+ when :gemini
40
+ Adapters::Gemini.new(config)
41
+ when :openai
42
+ Adapters::Openai.new(config)
43
+ else
44
+ raise ToolError, "Unknown adapter: #{config.adapter}"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -9,6 +9,7 @@ require_relative "tools/update"
9
9
  require_relative "tools/done"
10
10
  require_relative "tools/web_search"
11
11
  require_relative "tools/fetch"
12
+ require_relative "tools/explore"
12
13
 
13
14
  module RubyCode
14
15
  # Collection of available tools for the AI agent
@@ -22,7 +23,8 @@ module RubyCode
22
23
  Update,
23
24
  Done,
24
25
  WebSearch,
25
- Fetch
26
+ Fetch,
27
+ Explore
26
28
  ].freeze
27
29
 
28
30
  def self.definitions
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyCode
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.6"
5
5
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Index file for adapter view components
4
+ require_relative "adapter/debug_delay"
5
+ require_relative "adapter/debug_request"
6
+ require_relative "adapter/debug_response"
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Cli
8
+ # Builds auto-approve disabled message
9
+ class AutoApproveDisabled
10
+ def self.build
11
+ pastel = Pastel.new
12
+ "\n#{pastel.green("✓")} Auto-approve disabled\n" \
13
+ "You will be prompted before file operations.\n\n"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Cli
8
+ # Builds auto-approve enabled message
9
+ class AutoApproveEnabled
10
+ def self.build
11
+ pastel = Pastel.new
12
+ "\n#{pastel.green("✓")} Auto-approve enabled for write and update operations\n" \
13
+ "#{pastel.yellow("⚠")} Files will be modified without confirmation.\n" \
14
+ "Use 'auto-approve off' to disable.\n\n"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Cli
8
+ # Builds auto-approve status message
9
+ class AutoApproveStatus
10
+ def self.build(enabled:)
11
+ pastel = Pastel.new
12
+ status = enabled ? pastel.green("ENABLED") : pastel.dim("DISABLED")
13
+ "\nAuto-approve status: #{status}\n\n"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -8,16 +8,21 @@ module RubyCode
8
8
  module Cli
9
9
  # Builds configuration table display
10
10
  class ConfigurationTable
11
- def self.build(directory:, model:, adapter: :ollama)
11
+ def self.build(directory:, model:, adapter: :ollama, debug_mode: false, auto_approve: false) # rubocop:disable Lint/UnusedMethodArgument
12
12
  pastel = Pastel.new
13
13
 
14
+ rows = [
15
+ ["Adapter", adapter.to_s.capitalize],
16
+ ["Model", model],
17
+ ["Directory", directory]
18
+ ]
19
+
20
+ # Add auto-approve status if enabled
21
+ rows << ["Auto-approve", "#{pastel.green("ENABLED")} #{pastel.yellow("⚠")}"] if auto_approve
22
+
14
23
  table = TTY::Table.new(
15
24
  header: [pastel.bold("Setting"), pastel.bold("Value")],
16
- rows: [
17
- ["Adapter", adapter.to_s.capitalize],
18
- ["Model", model],
19
- ["Directory", directory]
20
- ]
25
+ rows: rows
21
26
  )
22
27
 
23
28
  "\n#{table.render(:unicode, padding: [0, 1])}"
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Cli
8
+ # Builds plan mode entry message
9
+ class PlanModeEnter
10
+ def self.build
11
+ pastel = Pastel.new
12
+ "\n#{pastel.cyan("📋")} Entering Plan Mode\n" \
13
+ "Next: Describe what you want to explore and implement.\n" \
14
+ "The AI will explore the codebase, present a plan, and ask for your approval.\n\n"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module RubyCode
6
+ module Views
7
+ module Cli
8
+ # Builds plan mode exit message
9
+ class PlanModeExit
10
+ def self.build
11
+ pastel = Pastel.new
12
+ "\n#{pastel.green("✓")} Plan Mode complete. Returning to normal mode.\n\n"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -14,3 +14,8 @@ require_relative "cli/first_time_setup"
14
14
  require_relative "cli/api_key_missing"
15
15
  require_relative "cli/config_saved"
16
16
  require_relative "cli/restart_message"
17
+ require_relative "cli/plan_mode_enter"
18
+ require_relative "cli/plan_mode_exit"
19
+ require_relative "cli/auto_approve_enabled"
20
+ require_relative "cli/auto_approve_disabled"
21
+ require_relative "cli/auto_approve_status"
@@ -8,10 +8,8 @@ require_relative "views/update_approval"
8
8
  require_relative "views/skip_notification"
9
9
  require_relative "views/web_search_approval"
10
10
  require_relative "views/cli"
11
+ require_relative "views/adapter"
11
12
  require_relative "views/agent_loop"
12
13
  require_relative "views/formatter"
13
14
  require_relative "views/response_handler"
14
- require_relative "views/adapter/debug_request"
15
- require_relative "views/adapter/debug_response"
16
- require_relative "views/adapter/debug_delay"
17
15
  require_relative "views/agent_loop/token_summary"
data/lib/rubycode.rb CHANGED
@@ -20,6 +20,7 @@ require_relative "rubycode/adapters/openrouter"
20
20
  require_relative "rubycode/adapters/deepseek"
21
21
  require_relative "rubycode/adapters/gemini"
22
22
  require_relative "rubycode/adapters/openai"
23
+ require_relative "rubycode/explorer"
23
24
  require_relative "rubycode/tools"
24
25
  require_relative "rubycode/agent_loop"
25
26
  require_relative "rubycode/client"
data/rubycode_cli.rb CHANGED
@@ -206,7 +206,7 @@ if adapter_info[:requires_key]
206
206
  end
207
207
  end
208
208
 
209
- ChatContext = Struct.new(:prompt, :client, :adapter, :model, :full_path, :debug_mode)
209
+ ChatContext = Struct.new(:prompt, :client, :adapter, :model, :full_path, :debug_mode, :plan_mode)
210
210
 
211
211
  def run_chat_loop(context)
212
212
  loop do
@@ -215,7 +215,7 @@ def run_chat_loop(context)
215
215
 
216
216
  next if handle_special_command(user_input, context)
217
217
 
218
- process_user_message(context.client, user_input)
218
+ process_user_message(context.client, user_input, context)
219
219
  end
220
220
  end
221
221
 
@@ -237,6 +237,32 @@ def handle_special_command(input, context)
237
237
  when "config"
238
238
  show_config_and_reconfigure(context)
239
239
  true
240
+ when "plan", "plan mode"
241
+ puts RubyCode::Views::Cli::PlanModeEnter.build
242
+ context.plan_mode = true
243
+ true
244
+ when "auto-approve on", "auto-approve write"
245
+ confirmed = context.prompt.yes?(
246
+ "⚠ Enable auto-approve for write/update operations?\n" \
247
+ "Files will be modified without confirmation.",
248
+ default: false
249
+ )
250
+
251
+ if confirmed
252
+ context.client.approval_handler.enable_auto_approve_write
253
+ puts RubyCode::Views::Cli::AutoApproveEnabled.build
254
+ else
255
+ puts "\nAuto-approve NOT enabled.\n\n"
256
+ end
257
+ true
258
+ when "auto-approve off"
259
+ context.client.approval_handler.disable_auto_approve_write
260
+ puts RubyCode::Views::Cli::AutoApproveDisabled.build
261
+ true
262
+ when "auto-approve", "auto-approve status"
263
+ status = context.client.approval_handler.auto_approve_write_enabled
264
+ puts RubyCode::Views::Cli::AutoApproveStatus.build(enabled: status)
265
+ true
240
266
  else
241
267
  false
242
268
  end
@@ -247,7 +273,8 @@ def show_config_and_reconfigure(context)
247
273
  adapter: context.adapter,
248
274
  model: context.model,
249
275
  directory: context.full_path,
250
- debug_mode: context.debug_mode
276
+ debug_mode: context.debug_mode,
277
+ auto_approve: context.client.approval_handler.auto_approve_write_enabled
251
278
  )
252
279
 
253
280
  return unless context.prompt.yes?(I18n.t("rubycode.setup.reconfigure"), default: false)
@@ -256,9 +283,40 @@ def show_config_and_reconfigure(context)
256
283
  setup_wizard(context.prompt)
257
284
  end
258
285
 
259
- def process_user_message(client, user_input)
260
- response = client.ask(prompt: user_input)
261
- puts RubyCode::Views::Cli::ResponseBox.build(response: response)
286
+ def process_user_message(client, user_input, context)
287
+ if context.plan_mode
288
+ # In plan mode - use explore tool then ask for approval
289
+ response = client.ask(prompt: "Use the explore tool with this query: #{user_input}")
290
+ puts RubyCode::Views::Cli::ResponseBox.build(response: response)
291
+
292
+ # Ask user if they accept the plan
293
+ accept_plan = context.prompt.yes?("\nDo you accept this plan and want to proceed with implementation?",
294
+ default: true)
295
+
296
+ if accept_plan
297
+ puts "\n✓ Plan accepted. Auto-approve enabled for implementation.\n"
298
+ # Enable auto-approve for implementation
299
+ client.approval_handler.enable_auto_approve_write
300
+
301
+ # Ask for implementation prompt
302
+ impl_prompt = context.prompt.ask("Describe what you want to implement:")
303
+ response = client.ask(prompt: impl_prompt)
304
+ puts RubyCode::Views::Cli::ResponseBox.build(response: response)
305
+
306
+ # Disable auto-approve after implementation
307
+ client.approval_handler.disable_auto_approve_write
308
+ else
309
+ puts "\n✗ Plan rejected. Returning to normal mode.\n"
310
+ end
311
+
312
+ # Exit plan mode after exploration
313
+ context.plan_mode = false
314
+ puts RubyCode::Views::Cli::PlanModeExit.build
315
+ else
316
+ # Normal mode
317
+ response = client.ask(prompt: user_input)
318
+ puts RubyCode::Views::Cli::ResponseBox.build(response: response)
319
+ end
262
320
  rescue Interrupt
263
321
  puts RubyCode::Views::Cli::InterruptMessage.build
264
322
  rescue StandardError => e
@@ -270,5 +328,6 @@ client = RubyCode::Client.new(tty_prompt: prompt)
270
328
  puts RubyCode::Views::Cli::ReadyMessage.build
271
329
 
272
330
  debug_mode = false # Debug mode not yet implemented
273
- context = ChatContext.new(prompt, client, adapter, model, full_path, debug_mode)
331
+ plan_mode = false # Plan mode flag
332
+ context = ChatContext.new(prompt, client, adapter, model, full_path, debug_mode, plan_mode)
274
333
  run_chat_loop(context)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubycode
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonas Medeiros
@@ -232,14 +232,17 @@ files:
232
232
  - ".env.example"
233
233
  - ".rubocop.yml"
234
234
  - CHANGELOG.md
235
+ - EXPLORE_TOOL_DESIGN.md
235
236
  - LICENSE.txt
236
237
  - README.md
237
238
  - Rakefile
238
239
  - USAGE.md
240
+ - config/exploration_prompt.md
239
241
  - config/locales/en.yml
240
242
  - config/system_prompt.md
241
243
  - config/tools/bash.json
242
244
  - config/tools/done.json
245
+ - config/tools/explore.json
243
246
  - config/tools/fetch.json
244
247
  - config/tools/read.json
245
248
  - config/tools/search.json
@@ -268,6 +271,7 @@ files:
268
271
  - lib/rubycode/context_builder.rb
269
272
  - lib/rubycode/database.rb
270
273
  - lib/rubycode/errors.rb
274
+ - lib/rubycode/explorer.rb
271
275
  - lib/rubycode/models.rb
272
276
  - lib/rubycode/models/api_key.rb
273
277
  - lib/rubycode/models/base.rb
@@ -286,6 +290,7 @@ files:
286
290
  - lib/rubycode/tools/base.rb
287
291
  - lib/rubycode/tools/bash.rb
288
292
  - lib/rubycode/tools/done.rb
293
+ - lib/rubycode/tools/explore.rb
289
294
  - lib/rubycode/tools/fetch.rb
290
295
  - lib/rubycode/tools/read.rb
291
296
  - lib/rubycode/tools/search.rb
@@ -295,6 +300,7 @@ files:
295
300
  - lib/rubycode/value_objects.rb
296
301
  - lib/rubycode/version.rb
297
302
  - lib/rubycode/views.rb
303
+ - lib/rubycode/views/adapter.rb
298
304
  - lib/rubycode/views/adapter/debug_delay.rb
299
305
  - lib/rubycode/views/adapter/debug_request.rb
300
306
  - lib/rubycode/views/adapter/debug_response.rb
@@ -310,6 +316,9 @@ files:
310
316
  - lib/rubycode/views/bash_approval.rb
311
317
  - lib/rubycode/views/cli.rb
312
318
  - lib/rubycode/views/cli/api_key_missing.rb
319
+ - lib/rubycode/views/cli/auto_approve_disabled.rb
320
+ - lib/rubycode/views/cli/auto_approve_enabled.rb
321
+ - lib/rubycode/views/cli/auto_approve_status.rb
313
322
  - lib/rubycode/views/cli/config_saved.rb
314
323
  - lib/rubycode/views/cli/configuration_table.rb
315
324
  - lib/rubycode/views/cli/error_display.rb
@@ -318,6 +327,8 @@ files:
318
327
  - lib/rubycode/views/cli/first_time_setup.rb
319
328
  - lib/rubycode/views/cli/interrupt_message.rb
320
329
  - lib/rubycode/views/cli/memory_cleared_message.rb
330
+ - lib/rubycode/views/cli/plan_mode_enter.rb
331
+ - lib/rubycode/views/cli/plan_mode_exit.rb
321
332
  - lib/rubycode/views/cli/ready_message.rb
322
333
  - lib/rubycode/views/cli/response_box.rb
323
334
  - lib/rubycode/views/cli/restart_message.rb