ares-runtime 2.0.3 → 2.0.5

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: c321feb80ab38315e30a032600325160b61fba7e2369fd731052c86ee4fa1341
4
- data.tar.gz: f7409e906b26c98ed91a1fd2c77747b22dc359662d769adcda7ee47002ba2ac4
3
+ metadata.gz: 24fc0fcd364ccd0c506af86b1757e8c29542184245ec22d5b6a3cb357af839b9
4
+ data.tar.gz: 44c8d7a31b0b3996613934510d64117634a3ac3614ae0ac34fdf67c8127e5ca3
5
5
  SHA512:
6
- metadata.gz: 3d7153a6fa16e1eccd05ced673a3527b1a13f50313066f517ea3cd240756ad23ee2feaeedb75a72cbab40e32a4444cb64625a742c5a8e0c0725534428b7d8482
7
- data.tar.gz: 1eef6e2f4c9dd99ae5e037da0e5adab6e30d3573d88f193ff6678d4d928f1f2340a6464185b8edfe2ee0e62d9085c33a9868184449d889579ac2bfebf845d5ef
6
+ metadata.gz: 8c3b8198c8f11b94ebfbd9845ec17de889afc98bf01870e4e1f874c5ca6d94a95b3021ba2c9237dad4ee40c26dcbb6946a43afa313b1c31f03df634bd22226af
7
+ data.tar.gz: add502d6a5cf0ac14ae20fc369ccf947253105b0904a6f5019ce2079fe109b25b5b6ab11cd44fa1a4b1fe62d02da1e9def57809b26766adc47e1d1dd2d90ef43
data/exe/ares CHANGED
@@ -7,16 +7,14 @@ require 'optparse'
7
7
  module Ares
8
8
  class CLI
9
9
  def self.start
10
- command = ARGV.shift
11
-
12
- case command
13
- when 'init' then init
14
- when 'config' then config
15
- when 'doctor' then doctor
16
- when 'version' then version
17
- when 'logs' then logs
10
+ case ARGV.first
11
+ when 'init' then ARGV.shift; init
12
+ when 'config' then ARGV.shift; config
13
+ when 'doctor' then ARGV.shift; doctor
14
+ when 'version' then ARGV.shift; version
15
+ when 'logs' then ARGV.shift; logs
18
16
  else
19
- run_task(command)
17
+ run_task
20
18
  end
21
19
  end
22
20
 
@@ -40,7 +38,7 @@ module Ares
40
38
  Ares::Runtime::LogsCLI.run
41
39
  end
42
40
 
43
- def self.run_task(first_arg)
41
+ def self.run_task
44
42
  options = {
45
43
  dry_run: false,
46
44
  git: false,
@@ -50,9 +48,10 @@ module Ares
50
48
  OptionParser.new do |opts|
51
49
  opts.banner = 'Usage: ares [options] "task description"'
52
50
 
53
- opts.on('-d', '--dry-run') { options[:dry_run] = true }
54
- opts.on('-g', '--git') { options[:git] = true }
55
- opts.on('--tui') { options[:tui] = true }
51
+ opts.on('-d', '--dry-run') { options[:dry_run] = true }
52
+ opts.on('-g', '--git') { options[:git] = true }
53
+ opts.on('--tui') { options[:tui] = true }
54
+ opts.on('--fail-fast') { options[:fail_fast] = true }
56
55
  end.parse!
57
56
 
58
57
  if options[:tui]
@@ -60,7 +59,7 @@ module Ares
60
59
  exit
61
60
  end
62
61
 
63
- task = ([first_arg] + ARGV).compact.join(' ').strip
62
+ task = ARGV.join(' ').strip
64
63
 
65
64
  if task.empty?
66
65
  puts 'No task provided.'
@@ -10,52 +10,55 @@ module Ares
10
10
  class BaseAdapter
11
11
  DEFAULT_TIMEOUT = 30
12
12
 
13
- # Template Method: The core algorithm skeleton
14
13
  def call(prompt, model = nil, **options)
15
14
  cmd = build_command(prompt, model, **options)
16
-
17
- output, status = execute_with_timeout(cmd, prompt, timeout_seconds)
18
-
19
- if should_retry?(status, output)
20
- cmd = build_retry_command(cmd, prompt, **options)
21
- output, status = execute_with_timeout(cmd, prompt, timeout_seconds)
22
- end
15
+ output, status = execute_with_retry(cmd, prompt, options)
23
16
 
24
17
  handle_errors(status, output)
25
-
26
18
  output
27
19
  end
28
20
 
29
21
  protected
30
22
 
31
- def execute_with_timeout(cmd, prompt, timeout)
32
- Timeout.timeout(timeout) do
33
- Open3.capture2e(*cmd, stdin_data: prompt)
34
- end
23
+ def execute_with_retry(cmd, prompt, options)
24
+ output, status = run_with_timeout(cmd, prompt)
25
+ return [output, status] unless should_retry?(status, output)
26
+
27
+ run_with_timeout(build_retry_command(cmd, prompt, **options), prompt)
28
+ end
29
+
30
+ def run_with_timeout(cmd, prompt)
31
+ Timeout.timeout(timeout_seconds) { run_command(cmd, prompt) }
35
32
  rescue Timeout::Error => e
36
- raise "#{adapter_name} timed out after #{timeout}s: #{e.message}"
33
+ raise "#{adapter_name} timed out after #{timeout_seconds}s: #{e.message}"
34
+ end
35
+
36
+ def run_command(cmd, prompt)
37
+ return Open3.capture2e(*cmd, stdin_data: prompt) if pipes_prompt_to_stdin?
38
+
39
+ Open3.capture2e(*cmd)
37
40
  end
38
41
 
39
42
  def handle_errors(status, output)
40
43
  raise "#{adapter_name} command failed: #{output}" unless status.success?
41
44
  end
42
45
 
43
- # Subclasses MUST implement this
44
46
  def build_command(prompt, model, **options)
45
47
  raise NotImplementedError, "#{self.class} must implement #build_command"
46
48
  end
47
49
 
48
- # Hook: Override in subclasses for complex retry logic
50
+ def pipes_prompt_to_stdin?
51
+ true
52
+ end
53
+
49
54
  def should_retry?(_status, _output)
50
55
  false
51
56
  end
52
57
 
53
- # Hook: Customize the command for the retry attempt
54
58
  def build_retry_command(cmd, _prompt, **_options)
55
59
  cmd
56
60
  end
57
61
 
58
- # Hook: Override for specific adapter timeouts
59
62
  def timeout_seconds
60
63
  DEFAULT_TIMEOUT
61
64
  end
@@ -12,14 +12,21 @@ module Ares
12
12
 
13
13
  protected
14
14
 
15
- def build_command(_prompt, _model, resume: true, cloud: false, **_options)
15
+ def build_command(prompt, _model, resume: true, cloud: false, **_options)
16
+ # Force strict non-conversational behavior for Cursor Agent
17
+ agent_prompt = "ACT AS AN AUTONOMOUS AGENT. PERFORM THE FOLLOWING TASK. DO NOT CHAT.\nTASK: #{prompt}"
16
18
  # --trust --yolo ensures no interactive prompts in headless mode
17
- cmd = ['agent', '-p', '-', '--trust', '--yolo']
19
+ cmd = ['agent', agent_prompt, '--print', '--trust', '--yolo']
18
20
  cmd << '-c' if cloud
19
21
  cmd << '--continue' if resume && !cloud
20
22
  cmd
21
23
  end
22
24
 
25
+
26
+ def pipes_prompt_to_stdin?
27
+ false
28
+ end
29
+
23
30
  def should_retry?(status, output)
24
31
  !status.success? && output.include?('No previous chats found')
25
32
  end
@@ -27,6 +34,10 @@ module Ares
27
34
  def build_retry_command(cmd, _prompt, **_options)
28
35
  cmd.dup.tap { |c| c.delete('--continue') }
29
36
  end
37
+
38
+ def timeout_seconds
39
+ 300 # Increased timeout for complex agent tasks
40
+ end
30
41
  end
31
42
  end
32
43
  end
@@ -15,7 +15,8 @@ module Ares
15
15
  end
16
16
 
17
17
  def call(prompt, model = nil, schema: nil)
18
- model ||= best_available_model
18
+ available = @client.list_model_names
19
+ model = available.include?(model) ? model : best_available_model(available)
19
20
 
20
21
  options = { prompt: prompt, model: model }
21
22
  options[:schema] = schema if schema
@@ -25,8 +26,8 @@ module Ares
25
26
 
26
27
  private
27
28
 
28
- def best_available_model
29
- available = @client.list_model_names
29
+ def best_available_model(available = nil)
30
+ available ||= @client.list_model_names
30
31
  return 'qwen3:latest' if available.include?('qwen3:latest')
31
32
  return 'qwen3:8b' if available.include?('qwen3:8b')
32
33
 
@@ -9,14 +9,13 @@ module Ares
9
9
  module Runtime
10
10
  # Subsystem Facade that initializes and bundles core dependencies for the Router.
11
11
  class CoreSubsystem
12
- attr_reader :logger, :planner, :selector, :tiny_processor, :ollama_healthy
12
+ attr_reader :logger, :planner, :tiny_processor, :ollama_healthy
13
13
 
14
14
  def initialize
15
15
  @logger = TaskLogger.new
16
16
  @ollama_healthy = initialize_ollama
17
17
 
18
18
  @planner = OllamaPlanner.new(healthy: @ollama_healthy)
19
- @selector = ModelSelector.new
20
19
  @tiny_processor = TinyTaskProcessor.new(healthy: @ollama_healthy)
21
20
  end
22
21
 
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fix_applicator'
4
+
5
+ module Ares
6
+ module Runtime
7
+ # Runs diagnostic commands (tests, syntax, lint) and escalates failures to AI fix.
8
+ class DiagnosticRunner
9
+ def initialize(core:, spinner:)
10
+ @core = core
11
+ @spinner = spinner
12
+ end
13
+
14
+ def run_tests(options = {})
15
+ run_loop('bundle exec rspec', options.merge(type: :test, title: 'Running tests'))
16
+ end
17
+
18
+ def run_syntax_check(options = {})
19
+ cmd = "ruby -e 'Dir.glob(\"{lib,bin,exe,spec}/**/*.rb\").each { |f| (puts \"Checking \#{f}\"; system(\"ruby -c \#{f}\")) or exit(1) }'"
20
+ run_loop(cmd, options.merge(type: :syntax, title: 'Checking syntax'))
21
+ end
22
+
23
+ def run_lint(options = {})
24
+ run_loop('bundle exec rubocop -A', options.merge(type: :lint, title: 'Running RuboCop'))
25
+ end
26
+
27
+ def run_loop(command, options)
28
+ title = options[:title] || 'Running verification'
29
+ result = run_with_spinner(command, title)
30
+
31
+ return report_success(title) if result[:exit_status].zero?
32
+
33
+ summary = parse_summary(result[:output], title, options[:type] || :test)
34
+ print_summary(summary, options[:type] || :test, title: title)
35
+
36
+ return skip_escalation if options[:dry_run]
37
+
38
+ applicator = FixApplicator.new(core: @core, spinner: @spinner, diagnostic_runner: self)
39
+ result = applicator.escalate(
40
+ type: options[:type] || :test,
41
+ summary: summary,
42
+ verify_command: command,
43
+ fix_first_only: !!options[:fix_first_only]
44
+ )
45
+
46
+ return result if options[:fail_fast]
47
+ end
48
+
49
+ def print_summary(summary, type, title: nil)
50
+ header = title || "Diagnostic Summary (#{type.to_s.upcase})"
51
+ table = TTY::Table.new(header: %w[Attribute Value])
52
+ table << ['Failed Items', Array(summary['failed_items'] || summary['failed_tests']).join("\n")]
53
+ table << ['Error Summary', summary['error_summary']]
54
+ puts "\n--- #{header} ---"
55
+ puts table.render(:unicode, multiline: true)
56
+ end
57
+
58
+ private
59
+
60
+ def run_with_spinner(command, title)
61
+ @spinner.update(title: "#{title}...")
62
+ result = nil
63
+ @spinner.run { result = TerminalRunner.run(command) }
64
+ result
65
+ end
66
+
67
+ def parse_summary(output, title, type)
68
+ @spinner.update(title: "#{title} failed. Summarizing diagnostic output...")
69
+ summary = nil
70
+ @spinner.run do
71
+ parsed = DiagnosticParser.parse(output, type: type)
72
+ if parsed['files'].empty? && parsed['failed_items'].empty?
73
+ @spinner.update(title: "#{title} failed. LLM Fallback (Slow)...")
74
+ summary = @core.tiny_processor.summarize_output(output, type: type)
75
+ else
76
+ summary = parsed
77
+ end
78
+ end
79
+ summary
80
+ rescue StandardError => e
81
+ @spinner.update(title: "#{title} failed. Error in fast-path: #{e.message}. LLM Fallback...")
82
+ @spinner.run { @core.tiny_processor.summarize_output(output, type: type) }
83
+ end
84
+
85
+ def report_success(title)
86
+ puts "#{title} passed! ✅"
87
+ true
88
+ end
89
+
90
+ def skip_escalation
91
+ puts 'Dry run: skipping escalation.'
92
+ false
93
+ end
94
+ end
95
+ end
96
+ end
@@ -9,57 +9,30 @@ module Ares
9
9
  module Runtime
10
10
  # Chain of Responsibility Handler linking CLI engines for automated fallback.
11
11
  class EngineChain
12
+ CAPABLE_ENGINES = %w[claude codex cursor ollama].freeze
13
+
12
14
  attr_accessor :next_handler
13
15
  attr_reader :engine_name
14
16
 
17
+ def self.build_fallback(initial_engine)
18
+ initial = initial_engine.to_s
19
+ fallback_order = ([initial] + (CAPABLE_ENGINES - [initial])).uniq
20
+ chain = build(fallback_order)
21
+ { chain: chain, size: fallback_order.size }
22
+ end
23
+
15
24
  def initialize(engine_name)
16
25
  @engine_name = engine_name
17
26
  @next_handler = nil
18
27
  @adapter = get_adapter(engine_name)
19
28
  end
20
29
 
21
- # Standard execution chain
22
30
  def call(prompt, options, attempt: 1, total: 1)
23
- if attempt > 1
24
- puts "Falling back to #{@engine_name} (attempt #{attempt}/#{total})..."
25
- else
26
- puts "Executing task via #{@engine_name} (attempt #{attempt}/#{total})..."
27
- end
28
-
29
- begin
30
- QuotaManager.increment_usage(@engine_name)
31
-
32
- @adapter.call(prompt, options[:model], **adapter_options(options))
33
- rescue StandardError => e
34
- puts "\n⚠️ #{@engine_name} failed: #{e.message.split("\n").first}"
35
-
36
- raise 'All available AI engines failed to execute the task.' unless @next_handler
37
-
38
- @next_handler.call(prompt, options, attempt: attempt + 1, total: total)
39
- end
31
+ execute_with_fallback(prompt, options, attempt, total, mode: :task)
40
32
  end
41
33
 
42
- # Specialized call for fix escalation
43
- def call_fix(prompt, options, attempt: 1, total: 1, &checkpoint_block)
44
- if attempt > 1
45
- puts "Falling back to #{@engine_name} for fix (attempt #{attempt}/#{total})..."
46
- else
47
- puts "Applying fix via #{@engine_name} (attempt #{attempt}/#{total})..."
48
- end
49
-
50
- checkpoint_block&.call(@engine_name)
51
-
52
- begin
53
- QuotaManager.increment_usage(@engine_name)
54
-
55
- @adapter.call(prompt, options[:model], **adapter_options(options))
56
- rescue StandardError => e
57
- puts "\n⚠️ #{@engine_name} failed during fix: #{e.message.split("\n").first}"
58
-
59
- raise 'All available AI engines failed to apply the fix.' unless @next_handler
60
-
61
- @next_handler.call_fix(prompt, options, attempt: attempt + 1, total: total, &checkpoint_block)
62
- end
34
+ def call_fix(prompt, options, attempt: 1, total: 1, &block)
35
+ execute_with_fallback(prompt, options, attempt, total, mode: :fix, &block)
63
36
  end
64
37
 
65
38
  # Factory method to build the chain
@@ -80,6 +53,42 @@ module Ares
80
53
 
81
54
  private
82
55
 
56
+ def execute_with_fallback(prompt, options, attempt, total, mode:, &block)
57
+ puts status_message(attempt, total, mode)
58
+ block&.call(@engine_name)
59
+
60
+ QuotaManager.increment_usage(@engine_name)
61
+ opts = adapter_options(options)
62
+ opts[:schema] = options[:schema] if options[:schema]
63
+ @adapter.call(prompt, options[:model], **opts)
64
+ rescue StandardError => e
65
+ failed_msg = mode == :fix ? "#{@engine_name} failed during fix:" : "#{@engine_name} failed:"
66
+ puts "\n❌ #{failed_msg} #{e.message.split("\n").first}"
67
+ raise all_engines_failed_message(mode) unless @next_handler
68
+
69
+ next_method = mode == :fix ? :call_fix : :call
70
+ @next_handler.public_send(next_method, prompt, options, attempt: attempt + 1, total: total, &block)
71
+ end
72
+
73
+ def status_message(attempt, total, mode)
74
+ action = if attempt > 1
75
+ 'Falling back to'
76
+ elsif mode == :fix
77
+ 'Applying fix via'
78
+ else
79
+ 'Executing task via'
80
+ end
81
+ "#{action} #{@engine_name} (attempt #{attempt}/#{total})..."
82
+ end
83
+
84
+ def all_engines_failed_message(mode)
85
+ if mode == :fix
86
+ 'All available AI engines failed to apply the fix.'
87
+ else
88
+ 'All available AI engines failed to execute the task.'
89
+ end
90
+ end
91
+
83
92
  def get_adapter(engine)
84
93
  case engine
85
94
  when 'claude' then Ares::Runtime::ClaudeAdapter.new
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ares
4
+ module Runtime
5
+ # Escalates diagnostic failures to AI engines and applies patches.
6
+ class FixApplicator
7
+ MAX_LINT_ITERATIONS = 20
8
+
9
+ def initialize(core:, spinner:, diagnostic_runner:)
10
+ @core = core
11
+ @spinner = spinner
12
+ @diagnostic_runner = diagnostic_runner
13
+ end
14
+
15
+ def escalate(type:, summary:, verify_command:, fix_first_only: false)
16
+ type == :lint ? escalate_lint_iteratively(summary, verify_command) : escalate_once(summary, type, verify_command)
17
+ end
18
+
19
+ private
20
+
21
+ def escalate_lint_iteratively(summary, verify_command)
22
+ current_summary = summary
23
+
24
+ MAX_LINT_ITERATIONS.times do |iteration|
25
+ puts "\n--- Fix iteration #{iteration + 1}/#{MAX_LINT_ITERATIONS} ---" if iteration.positive?
26
+
27
+ success = escalate_once(current_summary, :lint, verify_command, fix_first_only: true)
28
+ return true if success
29
+
30
+ verify_result = rerun_and_summarize(verify_command)
31
+ return false if verify_result[:exit_status].zero?
32
+
33
+ current_summary = verify_result[:summary]
34
+ @diagnostic_runner.print_summary(current_summary, :lint, title: 'Remaining offenses')
35
+ end
36
+
37
+ puts "\nReached max iterations (#{MAX_LINT_ITERATIONS}). Some offenses may remain."
38
+ false
39
+ end
40
+
41
+ def escalate_once(summary, type, verify_command, fix_first_only: false)
42
+ selection = ModelSelector.select({ 'task_type' => 'refactor', 'risk_level' => 'medium' })
43
+ puts "Selected Engine for fix: #{selection[:engine]} (#{selection[:model] || 'default'})"
44
+
45
+ result = apply_fix_with_fallbacks(build_fix_prompt(summary, type, fix_first_only), selection)
46
+ return false unless result
47
+
48
+ apply_patches(result) if result['patches']&.any?
49
+
50
+ verify_result = run_verify(verify_command)
51
+ handle_verification(verify_result, type)
52
+ end
53
+
54
+ def rerun_and_summarize(verify_command)
55
+ @spinner.update(title: 'Re-running RuboCop...')
56
+ verify_result = nil
57
+ @spinner.run { verify_result = TerminalRunner.run(verify_command) }
58
+
59
+ return { exit_status: verify_result[:exit_status], summary: nil } if verify_result[:exit_status].zero?
60
+
61
+ @spinner.update(title: 'Summarizing remaining offenses...')
62
+ summary = nil
63
+ @spinner.run { summary = @core.tiny_processor.summarize_output(verify_result[:output], type: :lint) }
64
+ { exit_status: verify_result[:exit_status], summary: summary }
65
+ end
66
+
67
+ def build_fix_prompt(summary, type, fix_first_only)
68
+ builder = PromptBuilder.new
69
+ .add_context(ContextLoader.load)
70
+ .add_diagnostic(type, summary['failed_items'] || summary['failed_tests'], summary['error_summary'])
71
+ .add_instruction("TASK: Fix the #{type} failures identified above.")
72
+
73
+ builder.add_instruction('Fix ONLY the first offense listed.') if fix_first_only
74
+
75
+ builder.add_instruction("You MUST provide JSON with 'explanation' and 'patches' (with 'file' and 'content' fields).")
76
+ .add_files(summary['files'])
77
+ .build
78
+ end
79
+
80
+ def apply_fix_with_fallbacks(fix_prompt, selection)
81
+ fallback = EngineChain.build_fallback(selection[:engine] || :claude)
82
+ adapter_opts = { model: selection[:model], fork_session: true, resume: true }
83
+
84
+ raw = fallback[:chain].call_fix(fix_prompt, adapter_opts, total: fallback[:size]) do |engine|
85
+ @spinner.update(title: checkpoint_message(engine))
86
+ end
87
+ parse_json(raw)
88
+ end
89
+
90
+ def parse_json(raw)
91
+ json_str = raw.match(/```(?:json)?\s*(.*?)\s*```/m)&.captures&.first || raw
92
+ JSON.parse(json_str)
93
+ rescue JSON::ParserError => e
94
+ puts "\n⚠️ Failed to parse valid JSON from the AI engine's response."
95
+ puts "--- Raw Output ---\n#{raw}\n----------------"
96
+ raise e
97
+ end
98
+
99
+ def apply_patches(result)
100
+ result['patches'].each do |patch|
101
+ path = File.expand_path(patch['file'], Dir.pwd)
102
+ FileUtils.mkdir_p(File.dirname(path))
103
+ File.write(path, patch['content'])
104
+ puts "Applied fix to #{patch['file']} ✅"
105
+ end
106
+ end
107
+
108
+ def run_verify(verify_command)
109
+ @spinner.update(title: 'Verifying fix...')
110
+ result = nil
111
+ @spinner.run { result = TerminalRunner.run(verify_command) }
112
+ result
113
+ end
114
+
115
+ def handle_verification(verify_result, type)
116
+ if verify_result[:exit_status].zero?
117
+ puts "Fix successful! #{type.to_s.capitalize} issues resolved. ✅"
118
+ true
119
+ else
120
+ puts "Fix failed. #{type.to_s.capitalize} issues still persist. ❌"
121
+ false
122
+ end
123
+ end
124
+
125
+ def checkpoint_message(engine)
126
+ case engine.to_s
127
+ when 'claude' then 'Leveraging Claude auto-checkpoint...'
128
+ when 'codex' then 'Leveraging Codex session persistence...'
129
+ else "Ensuring state persistence for #{engine}..."
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -6,7 +6,7 @@ module Ares
6
6
  CONFIDENCE_THRESHOLD = 0.7
7
7
 
8
8
  def self.select(plan)
9
- @config = ConfigManager.load_models
9
+ config = ConfigManager.load_models
10
10
 
11
11
  task_type = plan['task_type'] || 'refactor'
12
12
  confidence = plan['confidence'] || 1.0
@@ -15,7 +15,7 @@ module Ares
15
15
  return { engine: :claude, model: 'opus' } if confidence < CONFIDENCE_THRESHOLD || plan['risk_level'] == 'high'
16
16
 
17
17
  # Use string key lookup as ConfigManager returns keys as strings sometimes
18
- rule = @config[task_type.to_sym] || @config[:refactor]
18
+ rule = config[task_type.to_sym] || config[:refactor]
19
19
  engine = rule[:engine].to_sym
20
20
 
21
21
  # Safety: restrict Ollama from code-modifying tasks if configured incorrectly
@@ -8,8 +8,8 @@ require_relative '../ollama_client_factory'
8
8
  module Ares
9
9
  module Runtime
10
10
  class OllamaPlanner
11
- PLANNER_TIMEOUT = 30 # Longer once health is verified
12
- HARD_TIMEOUT = 35
11
+ PLANNER_TIMEOUT = 60 # Longer once health is verified
12
+ HARD_TIMEOUT = 65
13
13
 
14
14
  def initialize(healthy: true)
15
15
  @healthy = healthy
@@ -1,8 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'diagnostic_runner'
4
+
3
5
  module Ares
4
6
  module Runtime
5
7
  class Router
8
+ SHORTCUT_PATTERNS = {
9
+ /\A(run\s+|check\s+)?(test|rspec|fix|diagnostic)(s|ing)?\s*\z/i => :run_test_diagnostic,
10
+ /\A(run\s+|check\s+)?(syntax|compile)(\s+check)?\s*\z/i => :run_syntax_check,
11
+ /\A(run\s+|check\s+)?(lint|format|style)(ting|ing|s)?\s*\z/i => :run_lint
12
+ }.freeze
13
+
6
14
  def initialize
7
15
  @core = CoreSubsystem.new
8
16
  end
@@ -12,6 +20,7 @@ module Ares
12
20
  check_quota!
13
21
 
14
22
  @spinner = TTY::Spinner.new('[:spinner] :title', format: :dots)
23
+ @diagnostic = DiagnosticRunner.new(core: @core, spinner: @spinner)
15
24
 
16
25
  shortcut_result = match_shortcut_task(task, options)
17
26
  return shortcut_result if shortcut_result
@@ -28,112 +37,15 @@ module Ares
28
37
  private
29
38
 
30
39
  def run_test_diagnostic(options)
31
- run_diagnostic_loop('bundle exec rspec', options.merge(type: :test, title: 'Running tests'))
40
+ @diagnostic.run_tests(options)
32
41
  end
33
42
 
34
43
  def run_syntax_check(options)
35
- # Check syntax for all ruby files in lib, bin, spec individually to catch all errors
36
- cmd = "ruby -e 'Dir.glob(\"{lib,bin,exe,spec}/**/*.rb\").each { |f| (puts \"Checking \#{f}\"; system(\"ruby -c \#{f}\")) or exit(1) }'"
37
- run_diagnostic_loop(cmd, options.merge(type: :syntax, title: 'Checking syntax'))
38
- end
39
-
40
- def run_diagnostic_loop(command, options)
41
- title = options[:title] || 'Running verification'
42
- @spinner.update(title: "#{title}...")
43
- result = nil
44
- @spinner.run { result = TerminalRunner.run(command) }
45
-
46
- if result[:exit_status].zero?
47
- puts "#{title} passed! ✅"
48
- return true
49
- end
50
-
51
- type = options[:type] || :test
52
- @spinner.update(title: "#{title} failed. Summarizing diagnostic output...")
53
- summary = nil
54
-
55
- @spinner.run do
56
- # Try fast-path regex/JSON parsing first
57
- summary = DiagnosticParser.parse(result[:output], type: type)
58
-
59
- # Fallback to LLM only if fast-path failed to identify files
60
- if summary['files'].empty? || summary['failed_items'].empty?
61
- @spinner.update(title: "#{title} failed. LLM Fallback (Slow)...")
62
- summary = @core.tiny_processor.summarize_output(result[:output], type: type)
63
- end
64
- rescue StandardError => e
65
- @spinner.update(title: "#{title} failed. Error in fast-path: #{e.message}. LLM Fallback...")
66
- summary = @core.tiny_processor.summarize_output(result[:output], type: type)
67
- end
68
-
69
- puts "\n--- Diagnostic Summary (#{type.to_s.upcase}) ---"
70
- table = TTY::Table.new(header: %w[Attribute Value])
71
- table << ['Failed Items', Array(summary['failed_items'] || summary['failed_tests']).join("\n")]
72
- table << ['Error Summary', summary['error_summary']]
73
- puts table.render(:unicode, multiline: true)
74
-
75
- if options[:dry_run]
76
- puts 'Dry run: skipping escalation.'
77
- return false
78
- end
79
-
80
- # Escalate to executor for fix
81
- if type == :lint
82
- escalate_lint_one_at_a_time(summary, options.merge(verify_command: command))
83
- else
84
- escalate_to_executor(summary, options.merge(verify_command: command))
85
- end
44
+ @diagnostic.run_syntax_check(options)
86
45
  end
87
46
 
88
- def escalate_lint_one_at_a_time(summary, options)
89
- max_iterations = 20
90
- current_summary = summary
91
-
92
- max_iterations.times do |iteration|
93
- puts "\n--- Fix iteration #{iteration + 1}/#{max_iterations} ---" if iteration.positive?
94
-
95
- success = escalate_to_executor(current_summary, options.merge(fix_first_only: true))
96
- return true if success
97
-
98
- @spinner.update(title: 'Re-running RuboCop...')
99
- verify_result = nil
100
- @spinner.run { verify_result = TerminalRunner.run(options[:verify_command]) }
101
-
102
- return false if verify_result[:exit_status].zero?
103
-
104
- @spinner.update(title: 'Summarizing remaining offenses...')
105
- current_summary = nil
106
- @spinner.run { current_summary = @core.tiny_processor.summarize_output(verify_result[:output], type: :lint) }
107
-
108
- puts "\n--- Remaining offenses ---"
109
- table = TTY::Table.new(header: %w[Attribute Value])
110
- table << ['Failed Items', Array(current_summary['failed_items']).join("\n")]
111
- table << ['Error Summary', current_summary['error_summary']]
112
- puts table.render(:unicode, multiline: true)
113
- end
114
-
115
- puts "\nReached max iterations (#{max_iterations}). Some offenses may remain."
116
- false
117
- end
118
-
119
- def escalate_to_executor(summary, options)
120
- type = options[:type] || :test
121
- verify_command = options[:verify_command] || 'bundle exec rspec'
122
- fix_prompt = generate_fix_prompt(summary, options)
123
-
124
- selection = ModelSelector.select({ 'task_type' => 'refactor', 'risk_level' => 'medium' })
125
- puts "Selected Engine for fix: #{selection[:engine]} (#{selection[:model] || 'default'})"
126
-
127
- result = apply_fix_with_fallbacks(fix_prompt, selection)
128
- return false unless result
129
-
130
- apply_patches(result) if result['patches']&.any?
131
-
132
- @spinner.update(title: 'Verifying fix...')
133
- verify_result = nil
134
- @spinner.run { verify_result = TerminalRunner.run(verify_command) }
135
-
136
- handle_verification_result(verify_result, type)
47
+ def run_lint(options)
48
+ @diagnostic.run_lint(options)
137
49
  end
138
50
 
139
51
  def check_quota!
@@ -144,27 +56,20 @@ module Ares
144
56
  end
145
57
 
146
58
  def match_shortcut_task(task, options)
147
- if task.match?(/\A(run\s+|check\s+)?(test|rspec|fix|diagnostic)(s|ing)?\s*\z/i)
148
- return run_test_diagnostic(options)
59
+ SHORTCUT_PATTERNS.each do |pattern, method|
60
+ return send(method, options) if task.match?(pattern)
149
61
  end
150
- return run_syntax_check(options) if task.match?(/\A(run\s+|check\s+)?(syntax|compile)(\s+check)?\s*\z/i)
151
- return run_lint(options) if task.match?(/\A(run\s+|check\s+)?(lint|format|style)(ting|ing|s)?\s*\z/i)
152
-
153
62
  nil
154
63
  end
155
64
 
156
65
  def plan_task(task)
157
- plan = nil
158
66
  @spinner.update(title: 'Planning task...')
159
- @spinner.run { plan = @core.planner.plan(task) }
160
- plan
67
+ @spinner.run { @core.planner.plan(task) }
161
68
  end
162
69
 
163
70
  def select_model_for_plan(plan)
164
- selection = nil
165
71
  @spinner.update(title: 'Selecting optimal model...')
166
- @spinner.run { selection = ModelSelector.select(plan) }
167
- selection
72
+ @spinner.run { ModelSelector.select(plan) }
168
73
  end
169
74
 
170
75
  def handle_low_confidence(selection, plan)
@@ -188,16 +93,15 @@ module Ares
188
93
 
189
94
  def execute_engine_task(task, plan, selection, options)
190
95
  puts "Engine Selected: #{selection[:engine]} (#{selection[:model] || 'default'})"
191
- return if options[:dry_run] && puts('--- DRY RUN MODE ---')
96
+ if options[:dry_run]
97
+ puts '--- DRY RUN MODE ---'
98
+ return
99
+ end
192
100
 
193
101
  @core.logger.log_task(task, plan, selection)
194
102
  GitManager.create_branch(@core.logger.task_id, task) if options[:git]
195
103
 
196
- capable_engines = %w[claude codex cursor]
197
- initial_engine = selection[:engine]&.to_s || 'claude'
198
- fallback_chain = ([initial_engine] + (capable_engines - [initial_engine])).uniq
199
-
200
- chain = EngineChain.build(fallback_chain)
104
+ fallback = EngineChain.build_fallback(selection[:engine] || :claude)
201
105
  prompt = PromptBuilder.new
202
106
  .add_context(ContextLoader.load)
203
107
  .add_task(task)
@@ -205,92 +109,13 @@ module Ares
205
109
 
206
110
  adapter_opts = { model: selection[:model], fork_session: true, resume: true, cloud: options[:cloud] }
207
111
 
208
- output = chain.call(prompt, adapter_opts, total: fallback_chain.size)
112
+ output = fallback[:chain].call(prompt, adapter_opts, total: fallback[:size])
209
113
  @core.logger.log_result(output)
210
114
 
211
115
  GitManager.commit_changes(@core.logger.task_id, task) if options[:git]
212
116
  puts output
213
117
  end
214
118
 
215
- def generate_fix_prompt(summary, options)
216
- type = options[:type] || :test
217
-
218
- builder = PromptBuilder.new
219
- .add_context(ContextLoader.load)
220
- .add_diagnostic(type, summary['failed_items'] || summary['failed_tests'], summary['error_summary'])
221
- .add_instruction("TASK: Fix the #{type} failures identifying above.")
222
-
223
- builder.add_instruction('Fix ONLY the first offense listed.') if options[:fix_first_only]
224
-
225
- builder.add_instruction("You MUST provide JSON with 'explanation' and 'patches' (with 'file' and 'content' fields).")
226
- .add_files(summary['files'])
227
- .build
228
- end
229
-
230
- def apply_fix_with_fallbacks(fix_prompt, selection)
231
- capable_engines = %w[claude codex cursor]
232
- initial_engine = selection[:engine]&.to_s || 'claude'
233
- fallback_chain = ([initial_engine] + (capable_engines - [initial_engine])).uniq
234
-
235
- chain = EngineChain.build(fallback_chain)
236
- adapter_opts = { model: selection[:model], fork_session: true, resume: true }
237
-
238
- raw = chain.call_fix(fix_prompt, adapter_opts, total: fallback_chain.size) do |current_engine|
239
- create_checkpoint(current_engine)
240
- end
241
- parse_robust_json(raw)
242
- end
243
-
244
- def parse_robust_json(raw)
245
- json_str = raw.match(/```(?:json)?\s*(.*?)\s*```/m)&.captures&.first || raw
246
- begin
247
- JSON.parse(json_str)
248
- rescue JSON::ParserError => e
249
- puts "\n⚠️ Failed to parse valid JSON from the AI engine's response."
250
- puts "--- Raw Output ---\n#{raw}\n----------------"
251
- raise e
252
- end
253
- end
254
-
255
- def apply_patches(result)
256
- result['patches'].each do |patch|
257
- path = File.expand_path(patch['file'], Dir.pwd)
258
- FileUtils.mkdir_p(File.dirname(path))
259
- File.write(path, patch['content'])
260
- puts "Applied fix to #{patch['file']} ✅"
261
- end
262
- end
263
-
264
- def handle_verification_result(verify_result, type)
265
- if verify_result[:exit_status].zero?
266
- puts "Fix successful! #{type.to_s.capitalize} issues resolved. ✅"
267
- true
268
- else
269
- puts "Fix failed. #{type.to_s.capitalize} issues still persist. ❌"
270
- false
271
- end
272
- end
273
-
274
- def create_checkpoint(engine)
275
- # Checkpointing logic varies by engine.
276
- # For now, we ensure a git-based safety net if not in a git repo or if native fails.
277
- case engine
278
- when :claude
279
- # Claude Code does automatic checkpointing on every prompt.
280
- @spinner.update(title: 'Leveraging Claude auto-checkpoint...')
281
- when :codex
282
- # Codex sessions are automatically persisted.
283
- @spinner.update(title: 'Leveraging Codex session persistence...')
284
- else
285
- # Fallback to git stash or similar if we wanted a hard checkpoint,
286
- # but since we are often on a task branch, git is our checkpoint.
287
- @spinner.update(title: "Ensuring state persistence for #{engine}...")
288
- end
289
- end
290
-
291
- def run_lint(options)
292
- run_diagnostic_loop('bundle exec rubocop -A', options.merge(type: :lint, title: 'Running RuboCop'))
293
- end
294
119
  end
295
120
  end
296
121
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ares
4
4
  module Runtime
5
- VERSION = '2.0.3'
5
+ VERSION = '2.0.5'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ares-runtime
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.3
4
+ version: 2.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shubham
@@ -192,8 +192,10 @@ files:
192
192
  - lib/ares/runtime/context_loader.rb
193
193
  - lib/ares/runtime/core_subsystem.rb
194
194
  - lib/ares/runtime/diagnostic_parser.rb
195
+ - lib/ares/runtime/diagnostic_runner.rb
195
196
  - lib/ares/runtime/doctor.rb
196
197
  - lib/ares/runtime/engine_chain.rb
198
+ - lib/ares/runtime/fix_applicator.rb
197
199
  - lib/ares/runtime/git_manager.rb
198
200
  - lib/ares/runtime/initializer.rb
199
201
  - lib/ares/runtime/logs_cli.rb