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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0ceb8f061ed3a8172b46df1af17fa9961282858657aaf347215b42d5824acf7b
4
+ data.tar.gz: e345026c2323be4af4fa9ff2e00fb552685d9e9aec040a691a347cc063ce9d88
5
+ SHA512:
6
+ metadata.gz: 3fbbfb3540d90c6d5cf35bbe5e403ba0d4abc17fab630b464d9a03bd9748563cd3ed11192673ed5bca48e1d01c393b25bd5ad25324c4ccf7c3e4548695d336e0
7
+ data.tar.gz: 33f0bbba90317654a12835bd8328f6ef58a46b89b7d312aad6da46b76033495d708dc8da6a25772c7b8815811c0cc882878f0a58ac7cf418d0328af29821fe60
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release.
6
+
7
+ - Two-pass review of self-hosted GitLab merge requests: Generate finds
8
+ candidate findings, Critique filters out the weak ones.
9
+ - Gemini and local Ollama providers via RubyLLM, with a separate provider,
10
+ model and temperature per stage.
11
+ - Optional Jira issue context and per-project `review_instructions`.
12
+ - Path filtering and secret scrubbing before the diff reaches the LLM.
13
+ - A single updatable review note per merge request with `update` and `once`
14
+ modes, Retry detection in GitLab CI and a staleness check before posting.
15
+ - `.aireview.yml` and `.env` discovery walking up from the working directory.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Denis Levenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,362 @@
1
+ # aireview
2
+
3
+ `aireview` is a local CLI tool that reviews GitLab merge requests with the help
4
+ of LLMs. It uses a two-pass review pipeline: the first pass finds candidate
5
+ findings, the second one critiques them and drops the weak or invalid ones.
6
+
7
+ The tool supports self-hosted GitLab and self-hosted Jira only. GitLab.com and
8
+ Jira Cloud are not supported.
9
+
10
+ MVP flow:
11
+
12
+ 1. Takes a GitLab merge request URL.
13
+ 2. Fetches the MR metadata and changes from GitLab.
14
+ 3. Filters out ignored paths and scrubs secrets from the diffs.
15
+ 4. Optionally enriches the prompt with context from a Jira issue.
16
+ 5. Runs the Generate pass through RubyLLM to get an MR summary and candidate
17
+ findings.
18
+ 6. Optionally runs the Critique pass, which returns a verdict for every
19
+ candidate id.
20
+ 7. Renders the final markdown review to stdout or posts it back to the merge
21
+ request.
22
+
23
+ ## Requirements
24
+
25
+ - Ruby 3.1.3 or newer (CI runs the tests on 3.1, 3.3, 3.4 and 4.0)
26
+ - Bundler 2.3.26 for the repository checkout; `gem install` needs no specific Bundler
27
+ - A GitLab personal access token
28
+ - An API key for a remote LLM provider; a local Ollama needs no key
29
+ - Optionally, a Jira login and password
30
+
31
+ ## Installation
32
+
33
+ From RubyGems:
34
+
35
+ ```bash
36
+ gem install aireview
37
+ aireview --help
38
+ ```
39
+
40
+ From a checkout of the repository:
41
+
42
+ ```bash
43
+ bundle _2.3.26_ install
44
+ bundle _2.3.26_ exec bin/aireview --help
45
+ ```
46
+
47
+ The examples below use `bundle _2.3.26_ exec bin/aireview`; with the gem
48
+ installed, replace it with plain `aireview`.
49
+
50
+ ## Configuration
51
+
52
+ Secrets live in environment variables or in a local `.env` file. In `.env` the
53
+ Generate and Critique models are set explicitly:
54
+
55
+ ```bash
56
+ GITLAB_URL=https://gitlab.company.com
57
+ GITLAB_TOKEN=glpat-xxx
58
+ JIRA_URL=https://jira.company.com
59
+ JIRA_LOGIN=user
60
+ JIRA_PASSWORD=xxx
61
+ GEMINI_API_KEY=xxx
62
+ LLM_PROVIDER=gemini
63
+ LLM_TEMPERATURE=0
64
+ LLM_TIMEOUT=60
65
+ LLM_HTTP_PROXY=http://127.0.0.1:8888
66
+ LLM_GENERATE_PROVIDER=gemini
67
+ LLM_GENERATE_MODEL=gemini-3.7-flash
68
+ LLM_GENERATE_TEMPERATURE=0.3
69
+ LLM_CRITIQUE_PROVIDER=gemini
70
+ LLM_CRITIQUE_MODEL=gemini-3.8-flash
71
+ LLM_CRITIQUE_TEMPERATURE=0
72
+ REVIEW_LANGUAGE=ru
73
+ REVIEW_MODE=update
74
+ ```
75
+
76
+ At the moment only the `gemini` and `ollama` providers are supported. The
77
+ provider and the model of each stage are set through `LLM_GENERATE_PROVIDER`,
78
+ `LLM_GENERATE_MODEL`, `LLM_CRITIQUE_PROVIDER` and `LLM_CRITIQUE_MODEL`.
79
+ `LLM_PROVIDER` stays the shared default when a stage has no provider of its
80
+ own.
81
+
82
+ `REVIEW_LANGUAGE` (or `review_language` in `.aireview.yml`) sets the language
83
+ of the review: both the LLM answers and the headings of the rendered report.
84
+ `en` is the default; `ru` is supported as well.
85
+
86
+ If only the LLM traffic has to go through a proxy, set `LLM_HTTP_PROXY` or
87
+ `llm.http_proxy`. That configures RubyLLM only; requests to GitLab and Jira
88
+ keep going directly.
89
+
90
+ ### Local Ollama
91
+
92
+ Install Ollama following the
93
+ [official guide](https://docs.ollama.com/quickstart), then pull a local model,
94
+ [`qwen2.5-coder:7b`](https://ollama.com/library/qwen2.5-coder:7b):
95
+
96
+ ```bash
97
+ ollama pull qwen2.5-coder:7b
98
+ ```
99
+
100
+ If the service did not start on its own, start it separately and leave it
101
+ running during the review:
102
+
103
+ ```bash
104
+ ollama serve
105
+ ```
106
+
107
+ For a decent result use a stronger model for Critique than for Generate. For
108
+ example, to generate through a local Ollama and critique through Gemini,
109
+ configure `.env` like this:
110
+
111
+ ```bash
112
+ LLM_GENERATE_PROVIDER=ollama
113
+ LLM_GENERATE_MODEL=qwen2.5-coder:7b
114
+ LLM_GENERATE_TEMPERATURE=0
115
+ LLM_CRITIQUE_PROVIDER=gemini
116
+ LLM_CRITIQUE_MODEL=gemini-3.8-flash
117
+ LLM_CRITIQUE_TEMPERATURE=0
118
+ OLLAMA_API_BASE=http://localhost:11434/v1
119
+ GEMINI_API_KEY=xxx
120
+ LLM_TIMEOUT=300
121
+ ```
122
+
123
+ Ollama models that have been verified:
124
+
125
+ - `qwen2.5-coder:7b`
126
+ - `qwen2.5-coder:14b`
127
+ - `qwen3:8b`
128
+ - `qwen3:14b`
129
+ - `gpt-oss:20b`
130
+
131
+ The context window size is configured on the machine that runs Ollama. For a
132
+ permanent setting on Linux, open the service configuration:
133
+
134
+ ```bash
135
+ sudo systemctl edit ollama.service
136
+ ```
137
+
138
+ Add the setting:
139
+
140
+ ```ini
141
+ [Service]
142
+ Environment="OLLAMA_CONTEXT_LENGTH=8192"
143
+ ```
144
+
145
+ Then apply it and restart Ollama:
146
+
147
+ ```bash
148
+ sudo systemctl daemon-reload
149
+ sudo systemctl restart ollama
150
+ ```
151
+
152
+ When the server is started by hand, the context can only be set for the current
153
+ process:
154
+
155
+ ```bash
156
+ OLLAMA_CONTEXT_LENGTH=8192 ollama serve
157
+ ```
158
+
159
+ A local Ollama needs no API key. The providers can be swapped by changing
160
+ `LLM_GENERATE_PROVIDER`, `LLM_CRITIQUE_PROVIDER` and the corresponding models.
161
+ To run both stages locally, set `ollama` in both provider variables. The
162
+ address with `/v1` matches the
163
+ [Ollama configuration in RubyLLM](https://rubyllm.com/configuration/#provider-configuration).
164
+ `LLM_TIMEOUT` sets the timeout of every LLM request in seconds; for a slow
165
+ local model it can be raised.
166
+
167
+ Project rules live in `.aireview.yml`. In YAML the `generate.model` and
168
+ `critique.model` settings are required for each stage and are not inherited
169
+ from the base `llm` settings. `llm.provider` is used as the default when
170
+ `generate.provider` or `critique.provider` is not set:
171
+
172
+ ```yaml
173
+ ignore_paths:
174
+ - db/migrate/**
175
+ - vendor/**
176
+ - node_modules/**
177
+ - "*.lock"
178
+
179
+ secret_patterns:
180
+ - 'api_key\s*=\s*["'\''].*["'\'']'
181
+ - 'SECRET_[A-Z_]+'
182
+
183
+ secret_files:
184
+ - .env
185
+ - .env.*
186
+ - config/secrets.yml
187
+ - config/credentials/*.key
188
+ - spec/fixtures/cassettes/*.yml
189
+ - spec/fixtures/cassettes/**/*.yml
190
+ - spec/cassettes/*.yml
191
+ - spec/cassettes/**/*.yml
192
+ - test/fixtures/cassettes/*.yml
193
+ - test/fixtures/cassettes/**/*.yml
194
+
195
+ review_instructions: |
196
+ This is a Rails project. Pay attention to:
197
+ - N+1 queries
198
+ - strong params
199
+ - missing tests for new logic
200
+ Ignore style — that is what the linters are for.
201
+
202
+ ollama_api_base: http://localhost:11434/v1
203
+
204
+ llm:
205
+ provider: gemini
206
+ temperature: 0
207
+ timeout: 60
208
+ http_proxy: http://127.0.0.1:8888
209
+ generate:
210
+ provider: gemini
211
+ model: gemini-3.7-flash
212
+ temperature: 0.3
213
+ critique:
214
+ provider: ollama
215
+ model: qwen2.5-coder:7b
216
+ temperature: 0
217
+ ```
218
+
219
+ ## Usage
220
+
221
+ ```bash
222
+ bundle _2.3.26_ exec bin/aireview review https://gitlab.company.com/team/project/-/merge_requests/123
223
+ bundle _2.3.26_ exec bin/aireview review https://gitlab.company.com/team/project/-/merge_requests/123 --post
224
+ bundle _2.3.26_ exec bin/aireview review https://gitlab.company.com/team/project/-/merge_requests/123 --no-jira
225
+ bundle _2.3.26_ exec bin/aireview review https://gitlab.company.com/team/project/-/merge_requests/123 --dry-run --verbose
226
+ bundle _2.3.26_ exec bin/aireview review https://gitlab.company.com/team/project/-/merge_requests/123 --generate-model gemini-3.7-flash --critique-model gemini-3.8-flash
227
+ bundle _2.3.26_ exec bin/aireview review https://gitlab.company.com/team/project/-/merge_requests/123 --generate-model gemini-3.7-flash --generate-temperature 0.3 --critique-model gemini-3.8-flash --critique-temperature 0
228
+ bundle _2.3.26_ exec bin/aireview review https://gitlab.company.com/team/project/-/merge_requests/123 --no-critique
229
+ ```
230
+
231
+ - `--generate-model MODEL` overrides the model for the Generate pass only.
232
+ - `--critique-model MODEL` overrides the model for the Critique pass only.
233
+ - `--generate-temperature VALUE` overrides the temperature for the Generate pass only.
234
+ - `--critique-temperature VALUE` overrides the temperature for the Critique pass only.
235
+ - `--config PATH` points at a specific `.aireview.yml`.
236
+ - `--no-jira` turns off the Jira enrichment even when the MR carries an issue key.
237
+ - `--dry-run` prints the LLM settings and the Generate prompt, plus the Critique prompt unless `--no-critique` is given.
238
+ - `--no-critique` skips the second pass and renders the Generate candidates directly.
239
+ - `--review-mode MODE` sets the behaviour when a review has already been published: `update` or `once`.
240
+ - `--force` reviews again even when a review for this state of the MR is already published.
241
+
242
+ ### A single comment per merge request
243
+
244
+ With `--post` the review is not published as a new note every time; it updates
245
+ its own note instead. A hidden marker `<!-- aireview:key=... -->` is embedded
246
+ into the comment body, and the review uses it to find its own note among the
247
+ others. The note is looked up across all pages of the discussion and only among
248
+ the ones written by the same user the token belongs to. When there is no note
249
+ with the marker, the tool picks up its own older note that starts with
250
+ `**aireview review**`. Its key is unknown: on the next allowed run it will be
251
+ updated and get a marker. In `once` mode an old review also counts as existing —
252
+ updating it needs a Retry. Duplicates that have already piled up are not
253
+ removed automatically.
254
+
255
+ The key is a hash of the assembled prompts together with the providers, models
256
+ and temperatures of both stages. So the diff, the merge request description, the
257
+ Jira issue context, `review_instructions`, `ignore_paths` and the response
258
+ language all end up in it on their own: change any of those and the key differs,
259
+ so the review runs again.
260
+
261
+ The behaviour is set by `review_mode` (or `REVIEW_MODE`, or `--review-mode`):
262
+
263
+ - `update` (the default) — when the key matches the previous review, no requests
264
+ are sent to the LLM at all; when the diff or the settings have changed, the
265
+ review runs again and **overwrites** the previous comment.
266
+ - `once` — the review is done once, and later pushes to the MR do not repeat it.
267
+ To update the review by hand, press **Retry** on the job in GitLab: if the key
268
+ has changed, the LLM runs again and updates the previous comment. If nothing
269
+ changed, no requests are sent to the LLM. The current state of the MR is
270
+ checked, including when the job is restarted from an old pipeline. Note that
271
+ `.aireview.yml` (models, `review_instructions`, `ignore_paths`) is taken from
272
+ the checkout of the restarted pipeline. The `[skip review]` check in the
273
+ example below uses the `CI_MERGE_REQUEST_TITLE` of that pipeline, not the
274
+ fresh title from the API.
275
+
276
+ Detecting a Retry needs `CI_PROJECT_ID`, `CI_JOB_ID` and access for the
277
+ `GITLAB_TOKEN` to the
278
+ [Jobs API](https://docs.gitlab.com/api/jobs/#list-all-jobs-by-pipeline).
279
+ The tool checks whether an earlier attempt of the same job exists in the same
280
+ pipeline. An automatic retry from the GitLab `retry:` setting counts as a repeat
281
+ attempt too. Outside of GitLab CI, `once` mode keeps skipping an existing
282
+ review. The `--force` flag repeats the review even when nothing has changed.
283
+
284
+ While the review is running, the merge request can move on. Before publishing,
285
+ `aireview` re-reads it and compares `sha`, the target branch, `diff_refs`, the
286
+ title and the description: if any of those changed, the result is not published —
287
+ a comment must not end up holding a review of a diff that is no longer current.
288
+ The review text is printed to stdout before the publishing attempt, so it stays
289
+ in the job log. The previous comment is kept until the end of the next run.
290
+
291
+ ## GitLab CI
292
+
293
+ For merge request pipelines `aireview` can run in a separate CI job, letting
294
+ GitLab substitute the current MR URL:
295
+
296
+ ```yaml
297
+ workflow:
298
+ rules:
299
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
300
+
301
+ stages:
302
+ - review
303
+
304
+ aireview:
305
+ stage: review
306
+ image:
307
+ name: registry.gitlab.example.com/your-group/aireview:latest
308
+ entrypoint: [""]
309
+ variables:
310
+ MR_URL: "$CI_MERGE_REQUEST_PROJECT_URL/-/merge_requests/$CI_MERGE_REQUEST_IID"
311
+ REVIEW_LANGUAGE: "ru"
312
+ LLM_PROVIDER: "gemini"
313
+ LLM_GENERATE_MODEL: "gemini-3.7-flash"
314
+ LLM_CRITIQUE_MODEL: "gemini-3.8-flash"
315
+ LLM_TIMEOUT: "60"
316
+ LLM_HTTP_PROXY: "http://127.0.0.1:8888"
317
+ script:
318
+ - bundle _2.3.26_ exec bin/aireview review "$MR_URL" --verbose
319
+ retry:
320
+ max: 1
321
+ when:
322
+ - runner_system_failure
323
+ - stuck_or_timeout_failure
324
+ ```
325
+
326
+ Set secrets such as `GITLAB_TOKEN`, `GEMINI_API_KEY` and the optional Jira
327
+ credentials in the GitLab CI/CD variables. If the job should publish the result
328
+ back to the merge request, add `--post` to the review command.
329
+
330
+ For runners where the LLM provider is only reachable over WireGuard, bring up a
331
+ local HTTP proxy (`wireproxy`, for instance) before `aireview` starts and point
332
+ `LLM_HTTP_PROXY` at it. That avoids setting global `https_proxy`/`no_proxy`, so
333
+ GitLab and Jira stay on direct connections while RubyLLM goes through the
334
+ tunnel.
335
+
336
+ ## Docker
337
+
338
+ ```bash
339
+ docker build -t aireview .
340
+ docker run --rm --env-file .env -v "$PWD/.aireview.yml:/app/.aireview.yml:ro" aireview \
341
+ review https://gitlab.company.com/team/project/-/merge_requests/123
342
+ ```
343
+
344
+ ## Testing
345
+
346
+ ```bash
347
+ bundle _2.3.26_ exec rspec
348
+ bundle _2.3.26_ exec rspec spec/config_spec.rb
349
+ bundle _2.3.26_ exec rspec spec/secret_scrubber_spec.rb
350
+ ```
351
+
352
+ ## Notes
353
+
354
+ - The CLI looks for `.aireview.yml` and `.env` walking up from the current working directory, so the project config can be kept in the repository root even when the tool is run from `aireview/`.
355
+
356
+ ## Changelog
357
+
358
+ See [CHANGELOG.md](CHANGELOG.md).
359
+
360
+ ## License
361
+
362
+ [MIT](LICENSE)
data/bin/aireview ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+
6
+ require 'aireview'
7
+
8
+ exit Aireview::CLI.start(ARGV)
@@ -0,0 +1,31 @@
1
+ ignore_paths:
2
+ - db/migrate/**
3
+ - vendor/**
4
+ - node_modules/**
5
+ - "*.lock"
6
+
7
+ secret_patterns:
8
+ - 'api_key\s*=\s*["'\''].*["'\'']'
9
+ - 'SECRET_[A-Z_]+'
10
+
11
+ # update — обновлять свой комментарий, когда дифф или настройки изменились,
12
+ # once — один раз автоматически; Retry джоба обновляет ревью при изменениях.
13
+ review_mode: update
14
+
15
+ secret_files:
16
+ - .env
17
+ - .env.*
18
+ - config/secrets.yml
19
+ - config/credentials/*.key
20
+
21
+ review_instructions: |
22
+ Это Rails-проект. Обращай внимание на:
23
+ - N+1 запросы
24
+ - strong params
25
+ - отсутствие тестов для новой логики
26
+ Игнорируй стилистику — для этого есть линтеры.
27
+
28
+ llm:
29
+ provider: gemini
30
+ model: gemini-3.7-flash
31
+ temperature: 0.2