ai-cr 3.1.2__tar.gz → 3.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {ai_cr-3.1.2 → ai_cr-3.2.1}/PKG-INFO +24 -4
  2. {ai_cr-3.1.2 → ai_cr-3.2.1}/README.md +22 -2
  3. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/bootstrap.py +1 -1
  4. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/cli.py +34 -8
  5. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/cli_base.py +5 -1
  6. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/commands/deploy.py +4 -1
  7. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/commands/fix.py +4 -1
  8. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/commands/linear_comment.py +1 -1
  9. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/commands/repl.py +3 -1
  10. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/commands/version.py +1 -1
  11. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/config.toml +2 -26
  12. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/core.py +124 -87
  13. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/project_config.py +1 -0
  14. ai_cr-3.1.2/gito/tpl/questions/testing_guide.j2 → ai_cr-3.2.1/gito/tpl/answer.j2 +9 -6
  15. ai_cr-3.2.1/gito/tpl/partial/aux_files.j2 +8 -0
  16. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/tpl/questions/release_notes.j2 +2 -0
  17. ai_cr-3.2.1/gito/tpl/questions/test_cases.j2 +37 -0
  18. {ai_cr-3.1.2 → ai_cr-3.2.1}/pyproject.toml +2 -2
  19. {ai_cr-3.1.2 → ai_cr-3.2.1}/LICENSE +0 -0
  20. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/__init__.py +0 -0
  21. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/__main__.py +0 -0
  22. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/commands/__init__.py +0 -0
  23. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/commands/gh_post_review_comment.py +0 -0
  24. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/commands/gh_react_to_comment.py +0 -0
  25. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/constants.py +0 -0
  26. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/context.py +0 -0
  27. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/env.py +0 -0
  28. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/gh_api.py +0 -0
  29. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/issue_trackers.py +0 -0
  30. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/pipeline.py +0 -0
  31. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/pipeline_steps/__init__.py +0 -0
  32. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/pipeline_steps/jira.py +0 -0
  33. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/pipeline_steps/linear.py +0 -0
  34. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/report_struct.py +0 -0
  35. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/tpl/github_workflows/components/env-vars.j2 +0 -0
  36. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/tpl/github_workflows/components/installs.j2 +0 -0
  37. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/tpl/github_workflows/gito-code-review.yml.j2 +0 -0
  38. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/tpl/github_workflows/gito-react-to-comments.yml.j2 +0 -0
  39. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/tpl/questions/changes_summary.j2 +0 -0
  40. {ai_cr-3.1.2 → ai_cr-3.2.1}/gito/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ai-cr
3
- Version: 3.1.2
3
+ Version: 3.2.1
4
4
  Summary: AI code review tool that works with any language model provider. It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in.
5
5
  License: MIT
6
6
  Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Topic :: Software Development
18
18
  Requires-Dist: GitPython (>=3.1.44,<4.0.0)
19
- Requires-Dist: ai-microcore (==4.2.1)
19
+ Requires-Dist: ai-microcore (==4.3.0)
20
20
  Requires-Dist: anthropic (>=0.57.1,<0.58.0)
21
21
  Requires-Dist: ghapi (>=1.0.6,<1.1.0)
22
22
  Requires-Dist: google-generativeai (>=0.8.5,<0.9.0)
@@ -37,10 +37,22 @@ Description-Content-Type: text/markdown
37
37
  </p>
38
38
 
39
39
  **Gito** is an open-source **AI code reviewer** that works with any language model provider.
40
- It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in.
40
+ It detects issues in GitHub pull requests or local codebase changes—instantly, reliably, and without vendor lock-in.
41
41
 
42
42
  Get consistent, thorough code reviews in seconds—no waiting for human availability.
43
43
 
44
+ ## 📋 Table of Contents
45
+ - [Why Gito?](#-why-gito)
46
+ - [Perfect For](#-perfect-for)
47
+ - [Quickstart](#-quickstart)
48
+ - [1. Review Pull Requests via GitHub Actions](#1-review-pull-requests-via-github-actions)
49
+ - [2. Running Code Analysis Locally](#2-running-code-analysis-locally)
50
+ - [Configuration](#-configuration)
51
+ - [Documentation](#-documentation)
52
+ - [Development Setup](#-development-setup)
53
+ - [Contributing](#-contributing)
54
+ - [License](#-license)
55
+
44
56
  ## ✨ Why Gito?
45
57
 
46
58
  - [⚡] **Lightning Fast:** Get detailed code reviews in seconds, not days — powered by parallelized LLM processing
@@ -106,7 +118,7 @@ jobs:
106
118
  code-review-report.json
107
119
  ```
108
120
 
109
- > ⚠️ Make sure to add `LLM_API_KEY` to your repositorys GitHub secrets.
121
+ > ⚠️ Make sure to add `LLM_API_KEY` to your repository's GitHub secrets.
110
122
 
111
123
  💪 Done!
112
124
  PRs to your repository will now receive AI code reviews automatically. ✨
@@ -180,6 +192,14 @@ See default configuration [here](https://github.com/Nayjest/Gito/blob/main/gito/
180
192
 
181
193
  More details can be found in [📖 Configuration Cookbook](https://github.com/Nayjest/Gito/blob/main/documentation/config_cookbook.md)
182
194
 
195
+ ## 📚 Documentation
196
+
197
+ - [Command Line Reference](https://github.com/Nayjest/Gito/blob/main/documentation/command_line_reference.md)
198
+ - [Configuration Cookbook](https://github.com/Nayjest/Gito/blob/main/documentation/config_cookbook.md)
199
+ - [GitHub Setup Guide](https://github.com/Nayjest/Gito/blob/main/documentation/github_setup.md)
200
+ - [Troubleshooting](https://github.com/Nayjest/Gito/blob/main/documentation/troubleshooting.md)
201
+
202
+
183
203
  ## 💻 Development Setup
184
204
 
185
205
  Install dependencies:
@@ -8,10 +8,22 @@
8
8
  </p>
9
9
 
10
10
  **Gito** is an open-source **AI code reviewer** that works with any language model provider.
11
- It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in.
11
+ It detects issues in GitHub pull requests or local codebase changes—instantly, reliably, and without vendor lock-in.
12
12
 
13
13
  Get consistent, thorough code reviews in seconds—no waiting for human availability.
14
14
 
15
+ ## 📋 Table of Contents
16
+ - [Why Gito?](#-why-gito)
17
+ - [Perfect For](#-perfect-for)
18
+ - [Quickstart](#-quickstart)
19
+ - [1. Review Pull Requests via GitHub Actions](#1-review-pull-requests-via-github-actions)
20
+ - [2. Running Code Analysis Locally](#2-running-code-analysis-locally)
21
+ - [Configuration](#-configuration)
22
+ - [Documentation](#-documentation)
23
+ - [Development Setup](#-development-setup)
24
+ - [Contributing](#-contributing)
25
+ - [License](#-license)
26
+
15
27
  ## ✨ Why Gito?
16
28
 
17
29
  - [⚡] **Lightning Fast:** Get detailed code reviews in seconds, not days — powered by parallelized LLM processing
@@ -77,7 +89,7 @@ jobs:
77
89
  code-review-report.json
78
90
  ```
79
91
 
80
- > ⚠️ Make sure to add `LLM_API_KEY` to your repositorys GitHub secrets.
92
+ > ⚠️ Make sure to add `LLM_API_KEY` to your repository's GitHub secrets.
81
93
 
82
94
  💪 Done!
83
95
  PRs to your repository will now receive AI code reviews automatically. ✨
@@ -151,6 +163,14 @@ See default configuration [here](https://github.com/Nayjest/Gito/blob/main/gito/
151
163
 
152
164
  More details can be found in [📖 Configuration Cookbook](https://github.com/Nayjest/Gito/blob/main/documentation/config_cookbook.md)
153
165
 
166
+ ## 📚 Documentation
167
+
168
+ - [Command Line Reference](https://github.com/Nayjest/Gito/blob/main/documentation/command_line_reference.md)
169
+ - [Configuration Cookbook](https://github.com/Nayjest/Gito/blob/main/documentation/config_cookbook.md)
170
+ - [GitHub Setup Guide](https://github.com/Nayjest/Gito/blob/main/documentation/github_setup.md)
171
+ - [Troubleshooting](https://github.com/Nayjest/Gito/blob/main/documentation/troubleshooting.md)
172
+
173
+
154
174
  ## 💻 Development Setup
155
175
 
156
176
  Install dependencies:
@@ -35,7 +35,7 @@ def bootstrap(verbosity: int = 1):
35
35
  log_levels_by_verbosity = {
36
36
  0: logging.CRITICAL,
37
37
  1: logging.INFO,
38
- 2: logging.DEBUG,
38
+ 2: logging.INFO,
39
39
  3: logging.DEBUG,
40
40
  }
41
41
  Env.verbosity = verbosity
@@ -47,14 +47,31 @@ def main():
47
47
  app()
48
48
 
49
49
 
50
- @app.callback(invoke_without_command=True)
50
+ @app.callback(
51
+ invoke_without_command=True,
52
+ help="\bGito is an open-source AI code reviewer that works with any language model provider."
53
+ "\nIt detects issues in GitHub pull requests or local codebase changes"
54
+ "—instantly, reliably, and without vendor lock-in."
55
+ )
51
56
  def cli(
52
57
  ctx: typer.Context,
53
- verbose: bool = typer.Option(default=None),
54
58
  verbosity: int = typer.Option(
55
59
  None,
56
60
  '--verbosity', '-v',
57
- help="Set verbosity level (0-3, default 1)"
61
+ show_default=False,
62
+ help="\b"
63
+ "Set verbosity level. Supported values: 0-3. Default: 1."
64
+ "\n [ 0 ]: no additional output, "
65
+ "\n [ 1 ]: normal mode, shows warnings, shortened LLM requests and logging.INFO"
66
+ "\n [ 2 ]: verbose mode, show full LLM requests"
67
+ "\n [ 3 ]: very verbose mode, also debug information"
68
+ ),
69
+ verbose: bool = typer.Option(
70
+ default=None,
71
+ help="\b"
72
+ "--verbose is equivalent to -v2, "
73
+ "\n--no-verbose is equivalent to -v0. "
74
+ "\n(!) Can't be used together with -v or --verbosity."
58
75
  ),
59
76
  ):
60
77
  if verbose is not None and verbosity is not None:
@@ -71,7 +88,7 @@ def cli(
71
88
 
72
89
 
73
90
  @app_no_subcommand.command(name="review", help="Perform code review")
74
- @app.command(name="review", help="Perform code review")
91
+ @app.command(name="review", help="Perform a code review of the target codebase changes.")
75
92
  @app.command(name="run", hidden=True)
76
93
  def cmd_review(
77
94
  refs: str = arg_refs(),
@@ -120,7 +137,7 @@ def cmd_review(
120
137
  )
121
138
 
122
139
 
123
- @app.command(name="ask", help="Answer questions about codebase changes")
140
+ @app.command(name="ask", help="Answer questions about the target codebase changes.")
124
141
  @app.command(name="answer", hidden=True)
125
142
  @app.command(name="talk", hidden=True)
126
143
  def cmd_answer(
@@ -140,6 +157,10 @@ def cmd_answer(
140
157
  default=None,
141
158
  help="GitHub Pull Request number"
142
159
  ),
160
+ aux_files: list[str] = typer.Option(
161
+ default=None,
162
+ help="Auxiliary files that might be helpful"
163
+ )
143
164
  ):
144
165
  _what, _against = args_to_target(refs, what, against)
145
166
  pr = pr or os.getenv("PR_NUMBER_FROM_WORKFLOW_DISPATCH")
@@ -157,6 +178,7 @@ def cmd_answer(
157
178
  prompt_file=prompt_file,
158
179
  use_pipeline=use_pipeline,
159
180
  pr=pr,
181
+ aux_files=aux_files,
160
182
  )
161
183
  if post_to == 'linear':
162
184
  logging.info("Posting answer to Linear...")
@@ -164,12 +186,12 @@ def cmd_answer(
164
186
  return out
165
187
 
166
188
 
167
- @app.command(help="Configure LLM for local usage interactively")
189
+ @app.command(help="Configure LLM for local usage interactively.")
168
190
  def setup():
169
191
  mc.interactive_setup(HOME_ENV_PATH)
170
192
 
171
193
 
172
- @app.command(name="render")
194
+ @app.command(name="render", help="Render and display code review report.")
173
195
  @app.command(name="report", hidden=True)
174
196
  def render(
175
197
  format: str = typer.Argument(default=Report.Format.CLI),
@@ -183,7 +205,11 @@ def render(
183
205
  Report.load(file_name=source).to_cli(report_format=format)
184
206
 
185
207
 
186
- @app.command(help="List files in the diff. Might be useful to check what will be reviewed.")
208
+ @app.command(
209
+ help="\bList files in the changeset. "
210
+ "\nMight be useful to check what will be reviewed if run `gito review` "
211
+ "with current CLI arguments and options."
212
+ )
187
213
  def files(
188
214
  refs: str = arg_refs(),
189
215
  what: str = arg_what(),
@@ -30,7 +30,11 @@ def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
30
30
  def arg_refs() -> typer.Argument:
31
31
  return typer.Argument(
32
32
  default=None,
33
- help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
33
+ help=(
34
+ "Git refs to review, [what]..[against] (e.g., 'HEAD..HEAD~1'). "
35
+ "If omitted, the current index (including added but not committed files) "
36
+ "will be compared to the repository’s main branch."
37
+ ),
34
38
  )
35
39
 
36
40
 
@@ -12,7 +12,10 @@ from ..cli_base import app
12
12
  from ..gh_api import gh_api
13
13
 
14
14
 
15
- @app.command(name="deploy", help="Deploy Gito workflows to GitHub Actions")
15
+ @app.command(
16
+ name="deploy",
17
+ help="\bCreate and configure Gito GitHub Actions for current repository.\naliases: init"
18
+ )
16
19
  @app.command(name="init", hidden=True)
17
20
  def deploy(
18
21
  api_type: ApiType = None,
@@ -15,7 +15,10 @@ from ..constants import JSON_REPORT_FILE_NAME
15
15
  from ..report_struct import Report, Issue
16
16
 
17
17
 
18
- @app.command(help="Fix an issue from the code review report")
18
+ @app.command(
19
+ help="Fix an issue from the code review report "
20
+ "(latest code review results will be used by default)"
21
+ )
19
22
  def fix(
20
23
  issue_number: int = typer.Argument(..., help="Issue number to fix"),
21
24
  report_path: Optional[str] = typer.Option(
@@ -29,7 +29,7 @@ def post_linear_comment(issue_key, text, api_key):
29
29
  return response.json()
30
30
 
31
31
 
32
- @app.command()
32
+ @app.command(help="Post a comment with specified text to the associated Linear issue.")
33
33
  def linear_comment(
34
34
  text: str = typer.Argument(None),
35
35
  refs: str = arg_refs(),
@@ -23,6 +23,8 @@ from ..utils import *
23
23
  from ..gh_api import *
24
24
 
25
25
 
26
- @app.command(help="python REPL")
26
+ @app.command(
27
+ help="Python REPL with core functionality loaded for quick testing/debugging and exploration."
28
+ )
27
29
  def repl():
28
30
  code.interact(local=globals())
@@ -2,7 +2,7 @@ from ..cli_base import app
2
2
  from ..env import Env
3
3
 
4
4
 
5
- @app.command(name='version', help='Show the version of gito.bot')
5
+ @app.command(name='version', help='Show Gito version.')
6
6
  def version():
7
7
  print(Env.gito_version)
8
8
  return Env.gito_version
@@ -242,32 +242,8 @@ If there are concerns about how thoroughly the code covers the requirements and
242
242
  {{ summary_requirements -}}
243
243
  """
244
244
  answer_github_comments = true
245
- answer_prompt = """
246
- {{ self_id }}
247
- ----TASK----
248
- Answer the following user question:
249
- --USER QUESTION--
250
- {{ question }}
251
- ----
252
- ----RELATED CODEBASE CHANGES----
253
- {% for part in diff %}{{ part }}\n{% endfor %}
254
-
255
- ----FULL FILE CONTENT AFTER APPLYING CHANGES----
256
- {% for file, file_lines in all_file_lines.items() %}
257
- --FILE: {{ file }}--
258
- {{ file_lines }}
259
- {% endfor %}
260
-
261
- {%- if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
262
- ----ASSOCIATED ISSUE----
263
- # {{ pipeline_out.associated_issue.title }}
264
- {{ pipeline_out.associated_issue.description }}
265
- URL: {{ pipeline_out.associated_issue.url }}
266
- {%- endif -%}{{ '\n' }}
267
-
268
- ----ANSWERING INSTRUCTIONS----
269
- {{ answering_instructions }}
270
- """
245
+ answer_prompt = "tpl:answer.j2"
246
+ aux_files = []
271
247
  [pipeline_steps.jira]
272
248
  call="gito.pipeline_steps.jira.fetch_associated_issue"
273
249
  envs=["local","gh-action"]
@@ -121,85 +121,88 @@ def get_diff(
121
121
  if review_subject_is_index(what):
122
122
  what = None # working copy
123
123
  if use_merge_base:
124
- if review_subject_is_index(what):
125
- try:
126
- current_ref = repo.active_branch.name
127
- except TypeError:
128
- # In detached HEAD state, use HEAD directly
129
- current_ref = "HEAD"
130
- logging.info(
131
- "Detected detached HEAD state, using HEAD as current reference"
132
- )
133
- else:
134
- current_ref = what
135
- merge_base = repo.merge_base(current_ref or repo.active_branch.name, against)[0]
136
- logging.info(
137
- f"Merge base({ui.green(current_ref)},{ui.yellow(against)})"
138
- f" --> {ui.cyan(merge_base.hexsha)}"
139
- )
140
- # if branch is already an ancestor of "against", merge_base == branch ⇒ it’s been merged
141
- if merge_base.hexsha == repo.commit(current_ref or repo.active_branch.name).hexsha:
142
- # @todo: check case: reviewing working copy index in main branch #103
143
- logging.info(
144
- f"Branch is already merged. ({ui.green(current_ref)} vs {ui.yellow(against)})"
145
- )
146
- merge_sha = repo.git.log(
147
- '--merges',
148
- '--ancestry-path',
149
- f'{current_ref}..{against}',
150
- '-n',
151
- '1',
152
- '--pretty=format:%H'
153
- ).strip()
154
- if merge_sha:
155
- logging.info(f"Merge commit is {ui.cyan(merge_sha)}")
156
- merge_commit = repo.commit(merge_sha)
157
-
158
- other_merge_parent = None
159
- for parent in merge_commit.parents:
160
- logging.info(f"Checking merge parent: {parent.hexsha[:8]}")
161
- if parent.hexsha == merge_base.hexsha:
162
- logging.info(f"merge parent is {ui.cyan(parent.hexsha[:8])}, skipping")
163
- continue
164
- if not commit_in_branch(repo, parent, against):
165
- logging.warning(f"merge parent is not in {against}, skipping")
166
- continue
167
- logging.info(f"Found other merge parent: {ui.cyan(parent.hexsha[:8])}")
168
- other_merge_parent = parent
169
- break
170
- if other_merge_parent:
171
- first_common_ancestor = repo.merge_base(other_merge_parent, merge_base)[0]
172
- # for gito remote (feature_branch vs origin/main)
173
- # the same merge base appears in first_common_ancestor again
174
- if first_common_ancestor.hexsha == merge_base.hexsha:
175
- if merge_base.parents:
176
- first_common_ancestor = repo.merge_base(
177
- other_merge_parent, merge_base.parents[0]
178
- )[0]
179
- else:
180
- logging.error(
181
- "merge_base has no parents, "
182
- "using merge_base as first_common_ancestor"
183
- )
124
+ try:
125
+ if review_subject_is_index(what):
126
+ try:
127
+ current_ref = repo.active_branch.name
128
+ except TypeError:
129
+ # In detached HEAD state, use HEAD directly
130
+ current_ref = "HEAD"
184
131
  logging.info(
185
- f"{what} will be compared to "
186
- f"first common ancestor of {what} and {against}: "
187
- f"{ui.cyan(first_common_ancestor.hexsha[:8])}"
132
+ "Detected detached HEAD state, using HEAD as current reference"
188
133
  )
189
- against = first_common_ancestor.hexsha
190
- else:
191
- logging.error(f"Can't find other merge parent for {merge_sha}")
192
134
  else:
193
- logging.error(
194
- f"No merge‐commit found for {current_ref!r}→{against!r}; "
195
- "falling back to merge‐base diff"
196
- )
197
- else:
198
- # normal case: branch not yet merged
199
- against = merge_base.hexsha
135
+ current_ref = what
136
+ merge_base = repo.merge_base(current_ref or repo.active_branch.name, against)[0]
200
137
  logging.info(
201
- f"Using merge base: {ui.cyan(merge_base.hexsha[:8])} ({merge_base.summary})"
138
+ f"Merge base({ui.green(current_ref)},{ui.yellow(against)})"
139
+ f" --> {ui.cyan(merge_base.hexsha)}"
202
140
  )
141
+ # if branch is already an ancestor of "against", merge_base == branch ⇒ it’s been merged
142
+ if merge_base.hexsha == repo.commit(current_ref or repo.active_branch.name).hexsha:
143
+ # @todo: check case: reviewing working copy index in main branch #103
144
+ logging.info(
145
+ f"Branch is already merged. ({ui.green(current_ref)} vs {ui.yellow(against)})"
146
+ )
147
+ merge_sha = repo.git.log(
148
+ '--merges',
149
+ '--ancestry-path',
150
+ f'{current_ref}..{against}',
151
+ '-n',
152
+ '1',
153
+ '--pretty=format:%H'
154
+ ).strip()
155
+ if merge_sha:
156
+ logging.info(f"Merge commit is {ui.cyan(merge_sha)}")
157
+ merge_commit = repo.commit(merge_sha)
158
+
159
+ other_merge_parent = None
160
+ for parent in merge_commit.parents:
161
+ logging.info(f"Checking merge parent: {parent.hexsha[:8]}")
162
+ if parent.hexsha == merge_base.hexsha:
163
+ logging.info(f"merge parent is {ui.cyan(parent.hexsha[:8])}, skipping")
164
+ continue
165
+ if not commit_in_branch(repo, parent, against):
166
+ logging.warning(f"merge parent is not in {against}, skipping")
167
+ continue
168
+ logging.info(f"Found other merge parent: {ui.cyan(parent.hexsha[:8])}")
169
+ other_merge_parent = parent
170
+ break
171
+ if other_merge_parent:
172
+ first_common_ancestor = repo.merge_base(other_merge_parent, merge_base)[0]
173
+ # for gito remote (feature_branch vs origin/main)
174
+ # the same merge base appears in first_common_ancestor again
175
+ if first_common_ancestor.hexsha == merge_base.hexsha:
176
+ if merge_base.parents:
177
+ first_common_ancestor = repo.merge_base(
178
+ other_merge_parent, merge_base.parents[0]
179
+ )[0]
180
+ else:
181
+ logging.error(
182
+ "merge_base has no parents, "
183
+ "using merge_base as first_common_ancestor"
184
+ )
185
+ logging.info(
186
+ f"{what} will be compared to "
187
+ f"first common ancestor of {what} and {against}: "
188
+ f"{ui.cyan(first_common_ancestor.hexsha[:8])}"
189
+ )
190
+ against = first_common_ancestor.hexsha
191
+ else:
192
+ logging.error(f"Can't find other merge parent for {merge_sha}")
193
+ else:
194
+ logging.warning(
195
+ f"No merge‐commit found for {current_ref!r}→{against!r}; "
196
+ "falling back to merge‐base diff"
197
+ )
198
+ else:
199
+ # normal case: branch not yet merged
200
+ against = merge_base.hexsha
201
+ logging.info(
202
+ f"Using merge base: {ui.cyan(merge_base.hexsha[:8])} ({merge_base.summary})"
203
+ )
204
+ except Exception as e:
205
+ logging.error(f"Error finding merge base: {e}")
203
206
  logging.info(
204
207
  f"Making diff: {ui.green(what or 'INDEX')} vs {ui.yellow(against)}"
205
208
  )
@@ -243,18 +246,20 @@ def filter_diff(
243
246
  return files
244
247
 
245
248
 
246
- def file_lines(repo: Repo, file: str, max_tokens: int = None, use_local_files: bool = False) -> str:
249
+ def read_file(repo: Repo, file: str, use_local_files: bool = False) -> str:
247
250
  if use_local_files:
248
251
  file_path = Path(repo.working_tree_dir) / file
249
252
  try:
250
- text = file_path.read_text(encoding='utf-8')
253
+ return file_path.read_text(encoding='utf-8')
251
254
  except (FileNotFoundError, UnicodeDecodeError) as e:
252
255
  logging.warning(f"Could not read file {file} from working directory: {e}")
253
- text = repo.tree()[file].data_stream.read().decode('utf-8')
254
- else:
255
- # Read from HEAD (committed version)
256
- text = repo.tree()[file].data_stream.read().decode('utf-8')
257
256
 
257
+ # Read from HEAD (committed version)
258
+ return repo.tree()[file].data_stream.read().decode('utf-8')
259
+
260
+
261
+ def file_lines(repo: Repo, file: str, max_tokens: int = None, use_local_files: bool = False) -> str:
262
+ text = read_file(repo=repo, file=file, use_local_files=use_local_files)
258
263
  lines = [f"{i + 1}: {line}\n" for i, line in enumerate(text.splitlines())]
259
264
  if max_tokens:
260
265
  lines, removed_qty = mc.tokenizing.fit_to_token_size(lines, max_tokens)
@@ -265,6 +270,22 @@ def file_lines(repo: Repo, file: str, max_tokens: int = None, use_local_files: b
265
270
  return "".join(lines)
266
271
 
267
272
 
273
+ def read_files(repo: Repo, files: list[str], max_tokens: int = None) -> dict:
274
+ out = dict()
275
+ total_tokens = 0
276
+ for file in files:
277
+ content = read_file(repo=repo, file=file, use_local_files=True)
278
+ total_tokens += mc.tokenizing.num_tokens_from_string(file)
279
+ total_tokens += mc.tokenizing.num_tokens_from_string(content)
280
+ if max_tokens and total_tokens > max_tokens:
281
+ logging.warning(
282
+ f"Skipping file {file} due to exceeding max_tokens limit ({max_tokens})"
283
+ )
284
+ continue
285
+ out[file] = content
286
+ return out
287
+
288
+
268
289
  def make_cr_summary(ctx: Context, **kwargs) -> str:
269
290
  return (
270
291
  mc.prompt(
@@ -428,6 +449,7 @@ def answer(
428
449
  use_pipeline: bool = True,
429
450
  prompt_file: str = None,
430
451
  pr: str | int = None,
452
+ aux_files: list[str] = None,
431
453
  ) -> str | None:
432
454
  try:
433
455
  repo, config, diff, lines = _prepare(
@@ -454,18 +476,33 @@ def answer(
454
476
  steps=config.pipeline_steps
455
477
  )
456
478
  pipe.run()
479
+
480
+ if aux_files or config.aux_files:
481
+ aux_files_dict = read_files(
482
+ repo,
483
+ (aux_files or list()) + config.aux_files,
484
+ config.max_code_tokens // 2
485
+ )
486
+ else:
487
+ aux_files_dict = dict()
488
+
489
+ if not prompt_file and config.answer_prompt.startswith("tpl:"):
490
+ prompt_file = str(config.answer_prompt)[4:]
491
+
457
492
  if prompt_file:
458
493
  prompt_func = partial(mc.tpl, prompt_file)
459
494
  else:
460
495
  prompt_func = partial(mc.prompt, config.answer_prompt)
496
+ prompt = prompt_func(
497
+ question=question,
498
+ diff=diff,
499
+ all_file_lines=lines,
500
+ pipeline_out=ctx.pipeline_out,
501
+ aux_files=aux_files_dict,
502
+ **config.prompt_vars,
503
+ )
461
504
  response = mc.llm(
462
- prompt_func(
463
- question=question,
464
- diff=diff,
465
- all_file_lines=lines,
466
- pipeline_out=ctx.pipeline_out,
467
- **config.prompt_vars,
468
- ),
505
+ prompt,
469
506
  callback=make_streaming_function() if Env.verbosity == 0 else None,
470
507
  )
471
508
  return response
@@ -32,6 +32,7 @@ class ProjectConfig:
32
32
  Defines the keyword or mention tag that triggers bot actions
33
33
  when referenced in code review comments.
34
34
  """
35
+ aux_files: list[str] = field(default_factory=list)
35
36
  pipeline_steps: dict[str, dict | PipelineStep] = field(default_factory=dict)
36
37
  collapse_previous_code_review_comments: bool = field(default=True)
37
38
  """
@@ -1,11 +1,9 @@
1
1
  {{ self_id }}
2
2
  ----TASK----
3
- Create a concise comment for QA specialists that explains how to test the code-base changes.
4
- Include:
5
- - User journeys that could be disrupted by failures
6
- - Relevant security scenarios and edge cases
7
- - Potential risk areas introduced by the changes, with suggestions for focused testing to mitigate them
8
-
3
+ Answer the following user question:
4
+ --USER QUESTION--
5
+ {{ question }}
6
+ ----
9
7
  ----RELATED CODEBASE CHANGES----
10
8
  {% for part in diff %}{{ part }}\n{% endfor %}
11
9
 
@@ -15,9 +13,14 @@ Include:
15
13
  {{ file_lines }}
16
14
  {% endfor %}
17
15
 
16
+ {% include "partial/aux_files.j2" %}
17
+
18
18
  {%- if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
19
19
  ----ASSOCIATED ISSUE----
20
20
  # {{ pipeline_out.associated_issue.title }}
21
21
  {{ pipeline_out.associated_issue.description }}
22
22
  URL: {{ pipeline_out.associated_issue.url }}
23
23
  {%- endif -%}{{ '\n' }}
24
+
25
+ ----ANSWERING INSTRUCTIONS----
26
+ {{ answering_instructions }}
@@ -0,0 +1,8 @@
1
+ {% if aux_files %}
2
+ ----AUXILIARY INFORMATION----
3
+ * Use if helpful for the task.
4
+ {% for file, text in aux_files.items() %}
5
+ --FILE: {{ file }}--
6
+ {{ text.strip() }}
7
+ {%- endfor -%}
8
+ {%- endif -%}
@@ -16,6 +16,8 @@ Avoid internal technical jargon or developer-specific details.
16
16
  {{ file_lines }}
17
17
  {% endfor %}
18
18
 
19
+ {%- include "partial/aux_files.j2" -%}
20
+
19
21
  {%- if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
20
22
  ----ASSOCIATED ISSUE----
21
23
  # {{ pipeline_out.associated_issue.title }}
@@ -0,0 +1,37 @@
1
+ {{ self_id }}
2
+ ----TASK----
3
+ Create a concise list of manual QA test cases for the newly introduced codebase changes.
4
+ The purpose of this document is to identify user journeys that could be disrupted by failures,
5
+ cover security scenarios and edge cases, highlight potential risk areas introduced by the changes,
6
+ and suggest focused testing to mitigate them.
7
+
8
+ - Split the test cases into two categories:
9
+ - Critical
10
+ - Extended Coverage
11
+
12
+ - Structure test cases using Markdown headers or bullet-point lists.
13
+
14
+ - If the context includes the necessary details, describe the steps as thoroughly as possible—like for a junior QA
15
+ seeing this project for the first time.
16
+ If there are no details on how to perform a specific action, keep it abstract.
17
+ It is extremely important not to invent information.
18
+
19
+ - Number test cases sequentially (C-01, C-02, E-01, E-02, etc.).
20
+
21
+ ----RELATED CODEBASE CHANGES----
22
+ {% for part in diff %}{{ part }}\n{% endfor %}
23
+
24
+ ----FULL FILE CONTENT AFTER APPLYING CHANGES----
25
+ {% for file, file_lines in all_file_lines.items() %}
26
+ --FILE: {{ file }}--
27
+ {{ file_lines }}
28
+ {% endfor %}
29
+
30
+ {%- include "partial/aux_files.j2" -%}
31
+
32
+ {%- if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
33
+ ----ASSOCIATED ISSUE----
34
+ # {{ pipeline_out.associated_issue.title }}
35
+ {{ pipeline_out.associated_issue.description }}
36
+ URL: {{ pipeline_out.associated_issue.url }}
37
+ {%- endif -%}{{ '\n' }}
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ai-cr"
3
- version = "3.1.2"
3
+ version = "3.2.1"
4
4
  description = "AI code review tool that works with any language model provider. It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in."
5
5
  authors = ["Nayjest <mail@vitaliy.in>"]
6
6
  readme = "README.md"
@@ -21,7 +21,7 @@ packages = [
21
21
 
22
22
  [tool.poetry.dependencies]
23
23
  python = "^3.11"
24
- ai-microcore = "4.2.1"
24
+ ai-microcore = "4.3.0"
25
25
  GitPython = "^3.1.44"
26
26
  unidiff = "^0.7.5"
27
27
  google-generativeai = "^0.8.5"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes