aireview 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE +21 -0
- data/README.md +362 -0
- data/bin/aireview +8 -0
- data/config/.aireview.yml.example +31 -0
- data/lib/aireview/cli.rb +377 -0
- data/lib/aireview/config.rb +356 -0
- data/lib/aireview/context_builder.rb +111 -0
- data/lib/aireview/diff_fetcher.rb +47 -0
- data/lib/aireview/errors.rb +8 -0
- data/lib/aireview/gitlab_client.rb +166 -0
- data/lib/aireview/jira_client.rb +99 -0
- data/lib/aireview/mr_parser.rb +30 -0
- data/lib/aireview/output_schemas.rb +57 -0
- data/lib/aireview/prompts/critique.txt +57 -0
- data/lib/aireview/prompts/generate.txt +58 -0
- data/lib/aireview/publisher.rb +64 -0
- data/lib/aireview/review_marker.rb +65 -0
- data/lib/aireview/review_pipeline.rb +373 -0
- data/lib/aireview/review_renderer.rb +150 -0
- data/lib/aireview/review_schemas.rb +54 -0
- data/lib/aireview/reviewer.rb +280 -0
- data/lib/aireview/secret_scrubber.rb +70 -0
- data/lib/aireview/utils.rb +18 -0
- data/lib/aireview/version.rb +4 -0
- data/lib/aireview.rb +23 -0
- metadata +119 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative 'errors'
|
|
3
|
+
require_relative 'output_schemas'
|
|
4
|
+
require_relative 'utils'
|
|
5
|
+
require 'timeout'
|
|
6
|
+
|
|
7
|
+
module Aireview
|
|
8
|
+
class Reviewer
|
|
9
|
+
MAX_TRANSIENT_RETRIES = 2
|
|
10
|
+
TRANSIENT_RETRY_BASE_DELAY = 2.0
|
|
11
|
+
TRANSIENT_RETRY_JITTER_RANGE = 2.0..5.0
|
|
12
|
+
PROVIDER_RETRY_DELAY_MULTIPLIER_RANGE = 2.0..2.4
|
|
13
|
+
OVERLOADED_RETRY_DELAY_RANGE = 90.0..150.0
|
|
14
|
+
RETRY_WAIT_LOG_FORMAT = 'LLM %<stage>s request will sleep %<delay>.1fs before retry%<source>s ' \
|
|
15
|
+
'(attempt %<next_attempt>d/%<max_attempts>d, model=%<model>s)'
|
|
16
|
+
|
|
17
|
+
def initialize(config:, logger: Logger.new($stderr))
|
|
18
|
+
@config = config
|
|
19
|
+
@logger = logger
|
|
20
|
+
@llm_contexts = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def generate(system_prompt:, user_prompt:)
|
|
24
|
+
call_llm(
|
|
25
|
+
stage: 'generate',
|
|
26
|
+
system: system_prompt,
|
|
27
|
+
user: user_prompt,
|
|
28
|
+
options: {
|
|
29
|
+
model: @config.generate_model,
|
|
30
|
+
temperature: @config.generate_temperature,
|
|
31
|
+
provider: @config.generate_provider,
|
|
32
|
+
schema: GenerateOutputSchema
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def critique(system_prompt:, user_prompt:)
|
|
38
|
+
call_llm(
|
|
39
|
+
stage: 'critique',
|
|
40
|
+
system: system_prompt,
|
|
41
|
+
user: user_prompt,
|
|
42
|
+
options: {
|
|
43
|
+
model: @config.critique_model,
|
|
44
|
+
temperature: @config.critique_temperature,
|
|
45
|
+
provider: @config.critique_provider,
|
|
46
|
+
schema: CritiqueOutputSchema
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def call_llm(stage:, system:, user:, options:)
|
|
54
|
+
require 'ruby_llm'
|
|
55
|
+
|
|
56
|
+
@config.require_llm_configuration!
|
|
57
|
+
context = llm_context(stage, options[:provider])
|
|
58
|
+
response = request_with_retries(stage: stage, model: options[:model]) do
|
|
59
|
+
perform_llm_request(
|
|
60
|
+
context: context,
|
|
61
|
+
stage: stage,
|
|
62
|
+
system: system,
|
|
63
|
+
user: user,
|
|
64
|
+
options: options
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
response.content
|
|
68
|
+
rescue LoadError => e
|
|
69
|
+
@logger.error("LLM #{stage} setup failed: #{e.message}")
|
|
70
|
+
raise ConfigError, "Missing dependency: #{e.message}"
|
|
71
|
+
rescue Timeout::Error
|
|
72
|
+
@logger.warn(
|
|
73
|
+
"LLM #{stage} request timed out after #{@config.llm_timeout} seconds (model=#{options[:model]})"
|
|
74
|
+
)
|
|
75
|
+
raise ApiError, "LLM request timed out after #{@config.llm_timeout} seconds. " \
|
|
76
|
+
"Try again later or switch model via --generate-model/--critique-model."
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def perform_llm_request(context:, stage:, system:, user:, options:)
|
|
80
|
+
model = options[:model]
|
|
81
|
+
temperature = options[:temperature]
|
|
82
|
+
@logger.info("LLM #{stage} request started (model=#{model}, temperature=#{temperature})")
|
|
83
|
+
chat = build_chat(context: context, stage: stage, model: model, provider: options[:provider])
|
|
84
|
+
chat = configure_reasoning(chat: chat, model: model, provider: options[:provider])
|
|
85
|
+
.with_temperature(temperature.to_f)
|
|
86
|
+
.with_schema(options[:schema])
|
|
87
|
+
chat.with_instructions(system)
|
|
88
|
+
response = Timeout.timeout(@config.llm_timeout.to_f) { chat.ask(user) }
|
|
89
|
+
@logger.info("LLM #{stage} request completed (model=#{model})")
|
|
90
|
+
response
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def configure_reasoning(chat:, model:, provider:)
|
|
94
|
+
return chat unless provider == 'ollama' && model.start_with?('gpt-oss:')
|
|
95
|
+
|
|
96
|
+
chat.with_thinking(effort: :low)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_chat(context:, stage:, model:, provider:)
|
|
100
|
+
context.chat(model: model, provider: provider.to_sym)
|
|
101
|
+
rescue RubyLLM::ModelNotFoundError
|
|
102
|
+
@logger.warn(
|
|
103
|
+
"LLM #{stage}: model not found in RubyLLM registry; " \
|
|
104
|
+
"using fallback with incomplete model metadata " \
|
|
105
|
+
"(model=#{model}, provider=#{provider})"
|
|
106
|
+
)
|
|
107
|
+
context.chat(model: model, provider: provider.to_sym, assume_model_exists: true)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def request_with_retries(stage:, model:)
|
|
111
|
+
attempt = 0
|
|
112
|
+
|
|
113
|
+
begin
|
|
114
|
+
attempt += 1
|
|
115
|
+
yield
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
raise unless ruby_llm_api_error?(e)
|
|
118
|
+
|
|
119
|
+
@logger.warn("LLM #{stage} request failed (model=#{model}): #{e.class}: #{e.message}")
|
|
120
|
+
raise ApiError, llm_error_message(e) unless retry_llm_request?(e, attempt)
|
|
121
|
+
|
|
122
|
+
wait_before_retry(error: e, attempt: attempt, stage: stage, model: model)
|
|
123
|
+
retry
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def retry_llm_request?(error, attempt)
|
|
128
|
+
transient_llm_error?(error) && attempt <= MAX_TRANSIENT_RETRIES
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def wait_before_retry(error:, attempt:, stage:, model:)
|
|
132
|
+
retry_delay = transient_retry_delay(error: error, attempt: attempt)
|
|
133
|
+
@logger.warn(
|
|
134
|
+
format(
|
|
135
|
+
RETRY_WAIT_LOG_FORMAT,
|
|
136
|
+
stage: stage,
|
|
137
|
+
delay: retry_delay[:delay],
|
|
138
|
+
source: retry_delay_source(retry_delay),
|
|
139
|
+
next_attempt: attempt + 1,
|
|
140
|
+
max_attempts: MAX_TRANSIENT_RETRIES + 1,
|
|
141
|
+
model: model
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
sleep_started_at = monotonic_time
|
|
145
|
+
sleep(retry_delay[:delay])
|
|
146
|
+
log_retry_wait_completed(stage, model, monotonic_time - sleep_started_at)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def log_retry_wait_completed(stage, model, waited)
|
|
150
|
+
@logger.info(
|
|
151
|
+
format(
|
|
152
|
+
'LLM %<stage>s retry wait completed after %<waited>.1fs (model=%<model>s)',
|
|
153
|
+
stage: stage,
|
|
154
|
+
waited: waited,
|
|
155
|
+
model: model
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def ruby_llm_api_error?(error)
|
|
161
|
+
defined?(RubyLLM::Error) && error.is_a?(RubyLLM::Error)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def transient_llm_error?(error)
|
|
165
|
+
error.is_a?(RubyLLM::RateLimitError) ||
|
|
166
|
+
error.is_a?(RubyLLM::ServiceUnavailableError) ||
|
|
167
|
+
error.is_a?(RubyLLM::OverloadedError)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def monotonic_time
|
|
171
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def transient_retry_delay(error:, attempt:)
|
|
175
|
+
provider_delay = retry_after_seconds(error.message)
|
|
176
|
+
if provider_delay
|
|
177
|
+
multiplier = rand(PROVIDER_RETRY_DELAY_MULTIPLIER_RANGE)
|
|
178
|
+
return {
|
|
179
|
+
delay: provider_delay * multiplier,
|
|
180
|
+
provider_delay: provider_delay,
|
|
181
|
+
multiplier: multiplier,
|
|
182
|
+
strategy: :provider_hint
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if overloaded_llm_error?(error)
|
|
187
|
+
return {
|
|
188
|
+
delay: rand(OVERLOADED_RETRY_DELAY_RANGE),
|
|
189
|
+
provider_delay: nil,
|
|
190
|
+
multiplier: nil,
|
|
191
|
+
strategy: :overloaded
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
base_delay = TRANSIENT_RETRY_BASE_DELAY * (2**(attempt - 1))
|
|
196
|
+
{
|
|
197
|
+
delay: base_delay + rand(TRANSIENT_RETRY_JITTER_RANGE),
|
|
198
|
+
provider_delay: nil,
|
|
199
|
+
multiplier: nil,
|
|
200
|
+
strategy: :fallback
|
|
201
|
+
}
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def retry_delay_source(retry_delay)
|
|
205
|
+
return ' (overloaded backoff)' if retry_delay[:strategy] == :overloaded
|
|
206
|
+
return '' unless retry_delay[:provider_delay]
|
|
207
|
+
|
|
208
|
+
format(
|
|
209
|
+
' (provider retry hint %<provider_delay>.1fs, multiplier %<multiplier>.2fx)',
|
|
210
|
+
provider_delay: retry_delay[:provider_delay],
|
|
211
|
+
multiplier: retry_delay[:multiplier]
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def overloaded_llm_error?(error)
|
|
216
|
+
error.is_a?(RubyLLM::ServiceUnavailableError) ||
|
|
217
|
+
error.is_a?(RubyLLM::OverloadedError)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def retry_after_seconds(message)
|
|
221
|
+
match = message.to_s.match(/retry\s+(?:in|after)\s+(\d+(?:\.\d+)?)\s*(?:s|sec|secs|second|seconds)\b/i)
|
|
222
|
+
match[1].to_f if match
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def llm_error_message(error)
|
|
226
|
+
case error
|
|
227
|
+
when RubyLLM::ServiceUnavailableError, RubyLLM::OverloadedError
|
|
228
|
+
"LLM service is temporarily unavailable or overloaded: #{error.message}. " \
|
|
229
|
+
"Try again later or switch model via --generate-model/--critique-model."
|
|
230
|
+
when RubyLLM::RateLimitError
|
|
231
|
+
"LLM rate limit exceeded: #{error.message}. " \
|
|
232
|
+
"Try again later or switch model via --generate-model/--critique-model."
|
|
233
|
+
when RubyLLM::ContextLengthExceededError
|
|
234
|
+
"LLM context limit exceeded: #{error.message}. Try reducing the MR diff or ignore more paths."
|
|
235
|
+
else
|
|
236
|
+
"LLM API request failed: #{error.message}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def llm_context(stage, provider)
|
|
241
|
+
@llm_contexts[stage] ||= build_llm_context(provider.to_s)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def build_llm_context(provider)
|
|
245
|
+
api_key = @config.provider_api_key(provider) unless provider == 'ollama'
|
|
246
|
+
|
|
247
|
+
RubyLLM.context do |ruby_config|
|
|
248
|
+
configure_http_proxy(ruby_config)
|
|
249
|
+
ruby_config.request_timeout = @config.llm_timeout.to_f
|
|
250
|
+
configure_provider(ruby_config, provider, api_key)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def configure_http_proxy(ruby_config)
|
|
255
|
+
return unless Aireview::Utils.present?(@config.llm_http_proxy)
|
|
256
|
+
|
|
257
|
+
ruby_config.http_proxy = @config.llm_http_proxy
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def configure_provider(ruby_config, provider, api_key)
|
|
261
|
+
case provider
|
|
262
|
+
when 'gemini', 'openai', 'openrouter'
|
|
263
|
+
configure_remote_provider(ruby_config, provider, api_key)
|
|
264
|
+
when 'anthropic'
|
|
265
|
+
ruby_config.anthropic_api_key = api_key
|
|
266
|
+
when 'ollama'
|
|
267
|
+
ruby_config.ollama_api_base = @config.ollama_api_base
|
|
268
|
+
else
|
|
269
|
+
raise ConfigError, "Unsupported LLM provider: #{provider.inspect}"
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def configure_remote_provider(ruby_config, provider, api_key)
|
|
274
|
+
ruby_config.public_send("#{provider}_api_key=", api_key)
|
|
275
|
+
return unless Aireview::Utils.present?(@config.llm_api_base)
|
|
276
|
+
|
|
277
|
+
ruby_config.public_send("#{provider}_api_base=", @config.llm_api_base)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative 'utils'
|
|
3
|
+
|
|
4
|
+
module Aireview
|
|
5
|
+
class SecretScrubber
|
|
6
|
+
REDACTED = '[REDACTED]'
|
|
7
|
+
|
|
8
|
+
BUILTIN_RULES = [
|
|
9
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----/m, REDACTED],
|
|
10
|
+
[/\bAKIA[0-9A-Z]{16}\b/, REDACTED],
|
|
11
|
+
[/\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{20,}\b/, REDACTED],
|
|
12
|
+
[/\b(?:gh[ps]|glpat)_[A-Za-z0-9_-]{20,}\b/, REDACTED],
|
|
13
|
+
[/(Authorization:\s*Bearer\s+)[A-Za-z0-9._-]+/i, '\1[REDACTED]'],
|
|
14
|
+
[/(Bearer\s+)[A-Za-z0-9._-]{20,}/i, '\1[REDACTED]'],
|
|
15
|
+
[/(\baws_secret_access_key\b\s*[:=]\s*["']?)[A-Za-z0-9\/+=]{20,}(["']?)/i, '\1[REDACTED]\2'],
|
|
16
|
+
[/(\b(?:api[_-]?key|token|secret|password|passwd|client_secret)\w*\s*[:=]\s*["']?)[^"'\s]+(["']?)/i,
|
|
17
|
+
'\1[REDACTED]\2'],
|
|
18
|
+
[/(x-api-key:\s*)[A-Za-z0-9._-]+/i, '\1[REDACTED]']
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
def initialize(secret_patterns:, secret_files:, logger: Logger.new($stderr))
|
|
22
|
+
@logger = logger
|
|
23
|
+
@secret_files = Array(secret_files).compact
|
|
24
|
+
@custom_rules = compile_patterns(secret_patterns)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def scrub_changes(changes)
|
|
28
|
+
Array(changes).map { |change| scrub_change(change) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def scrub_change(change)
|
|
32
|
+
path = change['new_path'] || change['old_path']
|
|
33
|
+
scrubbed = change.dup
|
|
34
|
+
|
|
35
|
+
scrubbed['diff'] =
|
|
36
|
+
if secret_file?(path)
|
|
37
|
+
"[REDACTED: secret file #{path}]"
|
|
38
|
+
else
|
|
39
|
+
scrub_text(change['diff'].to_s)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
scrubbed
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def scrub_text(text)
|
|
46
|
+
(BUILTIN_RULES + @custom_rules).reduce(text.to_s) do |memo, (pattern, replacement)|
|
|
47
|
+
memo.gsub(pattern, replacement)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def compile_patterns(patterns)
|
|
54
|
+
Array(patterns).compact.filter_map do |pattern|
|
|
55
|
+
[Regexp.new(pattern, Regexp::IGNORECASE), REDACTED]
|
|
56
|
+
rescue RegexpError => e
|
|
57
|
+
@logger.warn("Skipping invalid secret pattern #{pattern.inspect}: #{e.message}")
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def secret_file?(path)
|
|
63
|
+
return false if Aireview::Utils.blank?(path)
|
|
64
|
+
|
|
65
|
+
@secret_files.any? do |pattern|
|
|
66
|
+
File.fnmatch?(pattern, path, File::FNM_DOTMATCH | File::FNM_EXTGLOB)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Aireview
|
|
3
|
+
module Utils
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def blank?(value)
|
|
7
|
+
value.nil? || value.to_s.strip.empty?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def present?(value)
|
|
11
|
+
!blank?(value)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def presence(value)
|
|
15
|
+
present?(value) ? value.to_s.strip : nil
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/aireview.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
require 'uri'
|
|
7
|
+
require 'yaml'
|
|
8
|
+
|
|
9
|
+
require_relative 'aireview/version'
|
|
10
|
+
require_relative 'aireview/errors'
|
|
11
|
+
require_relative 'aireview/utils'
|
|
12
|
+
require_relative 'aireview/config'
|
|
13
|
+
require_relative 'aireview/mr_parser'
|
|
14
|
+
require_relative 'aireview/diff_fetcher'
|
|
15
|
+
require_relative 'aireview/secret_scrubber'
|
|
16
|
+
require_relative 'aireview/context_builder'
|
|
17
|
+
require_relative 'aireview/reviewer'
|
|
18
|
+
require_relative 'aireview/review_pipeline'
|
|
19
|
+
require_relative 'aireview/gitlab_client'
|
|
20
|
+
require_relative 'aireview/jira_client'
|
|
21
|
+
require_relative 'aireview/review_marker'
|
|
22
|
+
require_relative 'aireview/publisher'
|
|
23
|
+
require_relative 'aireview/cli'
|
metadata
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: aireview
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Denis Levenko
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-11 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: dotenv
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: faraday
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.14'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.14'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: ruby_llm
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - '='
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 1.16.0
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - '='
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 1.16.0
|
|
55
|
+
description: 'Reviews self-hosted GitLab merge requests with a two-pass LLM pipeline:
|
|
56
|
+
the first pass finds candidate findings, the second one critiques them and drops
|
|
57
|
+
the weak ones. Supports Gemini and local Ollama, optional Jira context and posting
|
|
58
|
+
a single updatable review note back to the merge request.'
|
|
59
|
+
email:
|
|
60
|
+
executables:
|
|
61
|
+
- aireview
|
|
62
|
+
extensions: []
|
|
63
|
+
extra_rdoc_files: []
|
|
64
|
+
files:
|
|
65
|
+
- CHANGELOG.md
|
|
66
|
+
- LICENSE
|
|
67
|
+
- README.md
|
|
68
|
+
- bin/aireview
|
|
69
|
+
- config/.aireview.yml.example
|
|
70
|
+
- lib/aireview.rb
|
|
71
|
+
- lib/aireview/cli.rb
|
|
72
|
+
- lib/aireview/config.rb
|
|
73
|
+
- lib/aireview/context_builder.rb
|
|
74
|
+
- lib/aireview/diff_fetcher.rb
|
|
75
|
+
- lib/aireview/errors.rb
|
|
76
|
+
- lib/aireview/gitlab_client.rb
|
|
77
|
+
- lib/aireview/jira_client.rb
|
|
78
|
+
- lib/aireview/mr_parser.rb
|
|
79
|
+
- lib/aireview/output_schemas.rb
|
|
80
|
+
- lib/aireview/prompts/critique.txt
|
|
81
|
+
- lib/aireview/prompts/generate.txt
|
|
82
|
+
- lib/aireview/publisher.rb
|
|
83
|
+
- lib/aireview/review_marker.rb
|
|
84
|
+
- lib/aireview/review_pipeline.rb
|
|
85
|
+
- lib/aireview/review_renderer.rb
|
|
86
|
+
- lib/aireview/review_schemas.rb
|
|
87
|
+
- lib/aireview/reviewer.rb
|
|
88
|
+
- lib/aireview/secret_scrubber.rb
|
|
89
|
+
- lib/aireview/utils.rb
|
|
90
|
+
- lib/aireview/version.rb
|
|
91
|
+
homepage: https://github.com/DenisDenis9331/aireview
|
|
92
|
+
licenses:
|
|
93
|
+
- MIT
|
|
94
|
+
metadata:
|
|
95
|
+
homepage_uri: https://github.com/DenisDenis9331/aireview
|
|
96
|
+
source_code_uri: https://github.com/DenisDenis9331/aireview
|
|
97
|
+
changelog_uri: https://github.com/DenisDenis9331/aireview/blob/main/CHANGELOG.md
|
|
98
|
+
bug_tracker_uri: https://github.com/DenisDenis9331/aireview/issues
|
|
99
|
+
rubygems_mfa_required: 'true'
|
|
100
|
+
post_install_message:
|
|
101
|
+
rdoc_options: []
|
|
102
|
+
require_paths:
|
|
103
|
+
- lib
|
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: 3.1.3
|
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0'
|
|
114
|
+
requirements: []
|
|
115
|
+
rubygems_version: 3.3.26
|
|
116
|
+
signing_key:
|
|
117
|
+
specification_version: 4
|
|
118
|
+
summary: Local GitLab merge request review CLI powered by LLMs
|
|
119
|
+
test_files: []
|