github-code-review 3.2.0__tar.gz → 3.4.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. {github_code_review-3.2.0 → github_code_review-3.4.1}/PKG-INFO +17 -14
  2. {github_code_review-3.2.0 → github_code_review-3.4.1}/README.md +9 -9
  3. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/bootstrap.py +6 -1
  4. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/cli.py +30 -4
  5. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/cli_base.py +11 -1
  6. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/config.toml +16 -1
  7. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/constants.py +1 -0
  8. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/core.py +173 -89
  9. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/env.py +1 -0
  10. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/gh_api.py +3 -3
  11. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/project_config.py +4 -0
  12. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/report_struct.py +55 -29
  13. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/utils.py +31 -3
  14. {github_code_review-3.2.0 → github_code_review-3.4.1}/pyproject.toml +8 -7
  15. {github_code_review-3.2.0 → github_code_review-3.4.1}/LICENSE +0 -0
  16. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/__init__.py +0 -0
  17. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/__main__.py +0 -0
  18. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/commands/__init__.py +0 -0
  19. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/commands/deploy.py +0 -0
  20. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/commands/fix.py +0 -0
  21. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/commands/gh_post_review_comment.py +0 -0
  22. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/commands/gh_react_to_comment.py +0 -0
  23. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/commands/linear_comment.py +0 -0
  24. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/commands/repl.py +0 -0
  25. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/commands/version.py +0 -0
  26. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/context.py +0 -0
  27. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/issue_trackers.py +0 -0
  28. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/pipeline.py +0 -0
  29. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/pipeline_steps/__init__.py +0 -0
  30. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/pipeline_steps/jira.py +0 -0
  31. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/pipeline_steps/linear.py +0 -0
  32. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/tpl/answer.j2 +0 -0
  33. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/tpl/github_workflows/components/env-vars.j2 +0 -0
  34. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/tpl/github_workflows/components/installs.j2 +0 -0
  35. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/tpl/github_workflows/gito-code-review.yml.j2 +0 -0
  36. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/tpl/github_workflows/gito-react-to-comments.yml.j2 +0 -0
  37. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/tpl/partial/aux_files.j2 +0 -0
  38. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/tpl/questions/changes_summary.j2 +0 -0
  39. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/tpl/questions/release_notes.j2 +0 -0
  40. {github_code_review-3.2.0 → github_code_review-3.4.1}/gito/tpl/questions/test_cases.j2 +0 -0
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: github-code-review
3
- Version: 3.2.0
3
+ Version: 3.4.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
+ License-File: LICENSE
6
7
  Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
7
8
  Author: Nayjest
8
9
  Author-email: mail@vitaliy.in
@@ -14,14 +15,16 @@ Classifier: Programming Language :: Python :: 3
14
15
  Classifier: Programming Language :: Python :: 3.11
15
16
  Classifier: Programming Language :: Python :: 3.12
16
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
17
19
  Classifier: Topic :: Software Development
18
20
  Requires-Dist: GitPython (>=3.1.44,<4.0.0)
19
- Requires-Dist: ai-microcore (==4.3.0)
20
- Requires-Dist: anthropic (>=0.57.1,<0.58.0)
21
+ Requires-Dist: ai-microcore (>=4.5,<4.6)
22
+ Requires-Dist: anthropic (>=0.57.1,<1)
21
23
  Requires-Dist: ghapi (>=1.0.6,<1.1.0)
22
24
  Requires-Dist: google-generativeai (>=0.8.5,<0.9.0)
23
25
  Requires-Dist: jira (>=3.8.0,<4.0.0)
24
- Requires-Dist: typer (>=0.16.0,<0.17.0)
26
+ Requires-Dist: pydantic (>=2.12.3,<3.0.0)
27
+ Requires-Dist: typer (>=0.16.0,<0.21)
25
28
  Requires-Dist: unidiff (>=0.7.5,<0.8.0)
26
29
  Project-URL: Homepage, https://github.com/Nayjest/Gito
27
30
  Project-URL: Repository, https://github.com/Nayjest/Gito
@@ -99,7 +102,7 @@ jobs:
99
102
  uses: actions/setup-python@v5
100
103
  with: { python-version: "3.13" }
101
104
  - name: Install AI Code Review tool
102
- run: pip install gito.bot~=3.0
105
+ run: pip install gito.bot~=3.4
103
106
  - name: Run AI code analysis
104
107
  env:
105
108
  LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
@@ -130,15 +133,15 @@ See [GitHub Setup Guide](https://github.com/Nayjest/Gito/blob/main/documentation
130
133
 
131
134
  **Prerequisites:** [Python](https://www.python.org/downloads/) 3.11 / 3.12 / 3.13
132
135
 
133
- **Step1:** Install [gito.bot](https://github.com/Nayjest/Gito) using [pip](https://en.wikipedia.org/wiki/Pip_(package_manager)).
136
+ **Step 1:** Install [gito.bot](https://github.com/Nayjest/Gito) using [pip](https://en.wikipedia.org/wiki/Pip_(package_manager)).
134
137
  ```bash
135
138
  pip install gito.bot
136
139
  ```
137
140
 
138
141
  > **Troubleshooting:**
139
- > pip may be also available via cli as `pip3` depending on your Python installation.
142
+ > pip may also be available via cli as `pip3` depending on your Python installation.
140
143
 
141
- **Step2:** Perform initial setup
144
+ **Step 2:** Perform initial setup
142
145
 
143
146
  The following command will perform one-time setup using an interactive wizard.
144
147
  You will be prompted to enter LLM configuration details (API type, API key, etc).
@@ -149,15 +152,15 @@ gito setup
149
152
  ```
150
153
 
151
154
  > **Troubleshooting:**
152
- > On some systems, `gito` command may not became available immediately after installation.
155
+ > On some systems, `gito` command may not become available immediately after installation.
153
156
  > Try restarting your terminal or running `python -m gito` instead.
154
157
 
155
158
 
156
159
  #### Perform your first AI code review locally
157
160
 
158
- **Step1:** Navigate to your repository root directory.
159
- **Step2:** Switch to the branch you want to review.
160
- **Step3:** Run following command
161
+ **Step 1:** Navigate to your repository root directory.
162
+ **Step 2:** Switch to the branch you want to review.
163
+ **Step 3:** Run following command
161
164
  ```bash
162
165
  gito review
163
166
  ```
@@ -231,5 +234,5 @@ See [CONTRIBUTING.md](https://github.com/Nayjest/Gito/blob/main/CONTRIBUTING.md)
231
234
 
232
235
  Licensed under the [MIT License](https://github.com/Nayjest/Gito/blob/main/LICENSE).
233
236
 
234
- © 2025 [Vitalii Stepanenko](mailto:mail@vitaliy.in)
237
+ © 2025–2026 [Vitalii Stepanenko](mailto:mail@vitaliy.in)
235
238
 
@@ -70,7 +70,7 @@ jobs:
70
70
  uses: actions/setup-python@v5
71
71
  with: { python-version: "3.13" }
72
72
  - name: Install AI Code Review tool
73
- run: pip install gito.bot~=3.0
73
+ run: pip install gito.bot~=3.4
74
74
  - name: Run AI code analysis
75
75
  env:
76
76
  LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
@@ -101,15 +101,15 @@ See [GitHub Setup Guide](https://github.com/Nayjest/Gito/blob/main/documentation
101
101
 
102
102
  **Prerequisites:** [Python](https://www.python.org/downloads/) 3.11 / 3.12 / 3.13
103
103
 
104
- **Step1:** Install [gito.bot](https://github.com/Nayjest/Gito) using [pip](https://en.wikipedia.org/wiki/Pip_(package_manager)).
104
+ **Step 1:** Install [gito.bot](https://github.com/Nayjest/Gito) using [pip](https://en.wikipedia.org/wiki/Pip_(package_manager)).
105
105
  ```bash
106
106
  pip install gito.bot
107
107
  ```
108
108
 
109
109
  > **Troubleshooting:**
110
- > pip may be also available via cli as `pip3` depending on your Python installation.
110
+ > pip may also be available via cli as `pip3` depending on your Python installation.
111
111
 
112
- **Step2:** Perform initial setup
112
+ **Step 2:** Perform initial setup
113
113
 
114
114
  The following command will perform one-time setup using an interactive wizard.
115
115
  You will be prompted to enter LLM configuration details (API type, API key, etc).
@@ -120,15 +120,15 @@ gito setup
120
120
  ```
121
121
 
122
122
  > **Troubleshooting:**
123
- > On some systems, `gito` command may not became available immediately after installation.
123
+ > On some systems, `gito` command may not become available immediately after installation.
124
124
  > Try restarting your terminal or running `python -m gito` instead.
125
125
 
126
126
 
127
127
  #### Perform your first AI code review locally
128
128
 
129
- **Step1:** Navigate to your repository root directory.
130
- **Step2:** Switch to the branch you want to review.
131
- **Step3:** Run following command
129
+ **Step 1:** Navigate to your repository root directory.
130
+ **Step 2:** Switch to the branch you want to review.
131
+ **Step 3:** Run following command
132
132
  ```bash
133
133
  gito review
134
134
  ```
@@ -202,4 +202,4 @@ See [CONTRIBUTING.md](https://github.com/Nayjest/Gito/blob/main/CONTRIBUTING.md)
202
202
 
203
203
  Licensed under the [MIT License](https://github.com/Nayjest/Gito/blob/main/LICENSE).
204
204
 
205
- © 2025 [Vitalii Stepanenko](mailto:mail@vitaliy.in)
205
+ © 2025–2026 [Vitalii Stepanenko](mailto:mail@vitaliy.in)
@@ -13,6 +13,7 @@ from .env import Env
13
13
 
14
14
 
15
15
  def setup_logging(log_level: int = logging.INFO):
16
+ """Setup custom CLI logging format with colored output."""
16
17
  class CustomFormatter(logging.Formatter):
17
18
  def format(self, record):
18
19
  dt = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S")
@@ -23,7 +24,11 @@ def setup_logging(log_level: int = logging.INFO):
23
24
  if record.levelno >= logging.ERROR:
24
25
  message = mc.ui.red(message)
25
26
  level_name = mc.ui.red(level_name)
26
- return f"{dt} {level_name}: {message}"
27
+
28
+ formatted_message = f"{dt} {level_name}: {message}"
29
+ if record.exc_info:
30
+ formatted_message += "\n" + self.formatException(record.exc_info)
31
+ return formatted_message
27
32
 
28
33
  handler = logging.StreamHandler()
29
34
  handler.setFormatter(CustomFormatter())
@@ -7,6 +7,7 @@ import textwrap
7
7
  import microcore as mc
8
8
  import typer
9
9
  from git import Repo
10
+ from gito.constants import REFS_VALUE_ALL
10
11
 
11
12
  from .core import review, get_diff, filter_diff, answer
12
13
  from .cli_base import (
@@ -24,6 +25,7 @@ from .constants import HOME_ENV_PATH, GITHUB_MD_REPORT_FILE_NAME
24
25
  from .bootstrap import bootstrap
25
26
  from .utils import no_subcommand, extract_gh_owner_repo, remove_html_comments
26
27
  from .gh_api import resolve_gh_token
28
+ from .project_config import ProjectConfig
27
29
 
28
30
  # Import fix command to register it
29
31
  from .commands import fix, gh_react_to_comment, repl, deploy, version # noqa
@@ -97,6 +99,7 @@ def cmd_review(
97
99
  filters: str = arg_filters(),
98
100
  merge_base: bool = typer.Option(default=True, help="Use merge base for comparison"),
99
101
  url: str = typer.Option("", "--url", help="Git repository URL"),
102
+ path: str = typer.Option("", "--path", help="Git repository path"),
100
103
  post_comment: bool = typer.Option(default=False, help="Post review comment to GitHub"),
101
104
  pr: int = typer.Option(
102
105
  default=None,
@@ -106,8 +109,17 @@ def cmd_review(
106
109
  in the github actions PR is resolved from the environment)
107
110
  """)
108
111
  ),
109
- out: str = arg_out()
112
+ out: str = arg_out(),
113
+ all: bool = typer.Option(default=False, help="Review all codebase"),
110
114
  ):
115
+ if all:
116
+ if refs and refs != REFS_VALUE_ALL:
117
+ raise typer.BadParameter(
118
+ "The --all option overrides the refs argument. "
119
+ "Please remove the refs argument if you want to review all codebase."
120
+ )
121
+ refs = REFS_VALUE_ALL
122
+ merge_base = False
111
123
  _what, _against = args_to_target(refs, what, against)
112
124
  pr = pr or os.getenv("PR_NUMBER_FROM_WORKFLOW_DISPATCH")
113
125
  with get_repo_context(url, _what) as (repo, out_folder):
@@ -160,6 +172,11 @@ def cmd_answer(
160
172
  aux_files: list[str] = typer.Option(
161
173
  default=None,
162
174
  help="Auxiliary files that might be helpful"
175
+ ),
176
+ save_to: str = typer.Option(
177
+ help="Save answer to file",
178
+ default=None,
179
+ show_default=False
163
180
  )
164
181
  ):
165
182
  _what, _against = args_to_target(refs, what, against)
@@ -183,6 +200,11 @@ def cmd_answer(
183
200
  if post_to == 'linear':
184
201
  logging.info("Posting answer to Linear...")
185
202
  linear_comment(remove_html_comments(out))
203
+ if save_to:
204
+ with open(save_to, "w", encoding="utf-8") as f:
205
+ f.write(out)
206
+ logging.info(f"Answer saved to {mc.utils.file_link(save_to)}")
207
+
186
208
  return out
187
209
 
188
210
 
@@ -191,8 +213,8 @@ def setup():
191
213
  mc.interactive_setup(HOME_ENV_PATH)
192
214
 
193
215
 
194
- @app.command(name="render", help="Render and display code review report.")
195
- @app.command(name="report", hidden=True)
216
+ @app.command(name="report", help="Render and display code review report.")
217
+ @app.command(name="render", hidden=True)
196
218
  def render(
197
219
  format: str = typer.Argument(default=Report.Format.CLI),
198
220
  source: str = typer.Option(
@@ -223,11 +245,15 @@ def files(
223
245
  try:
224
246
  patch_set = get_diff(repo=repo, what=_what, against=_against, use_merge_base=merge_base)
225
247
  patch_set = filter_diff(patch_set, filters)
248
+ cfg = ProjectConfig.load_for_repo(repo)
249
+ if cfg.exclude_files:
250
+ patch_set = filter_diff(patch_set, cfg.exclude_files, exclude=True)
226
251
  print(
227
252
  f"Changed files: "
228
253
  f"{mc.ui.green(_what or 'INDEX')} vs "
229
254
  f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
230
- f"{' filtered by ' + mc.ui.cyan(filters) if filters else ''}"
255
+ f"{' filtered by ' + mc.ui.cyan(filters) if filters else ''} --> "
256
+ f"{mc.ui.cyan(len(patch_set or []))} file(s)."
231
257
  )
232
258
 
233
259
  for patch in patch_set:
@@ -5,10 +5,15 @@ import tempfile
5
5
  import microcore as mc
6
6
  import typer
7
7
  from git import Repo
8
- from gito.utils import parse_refs_pair
8
+ from gito.constants import REFS_VALUE_ALL
9
+
10
+ from .utils import parse_refs_pair
11
+ from .env import Env
9
12
 
10
13
 
11
14
  def args_to_target(refs, what, against) -> tuple[str | None, str | None]:
15
+ if refs == REFS_VALUE_ALL:
16
+ return REFS_VALUE_ALL, None
12
17
  _what, _against = parse_refs_pair(refs)
13
18
  if _what:
14
19
  if what:
@@ -73,6 +78,8 @@ app = typer.Typer(pretty_exceptions_show_locals=False)
73
78
 
74
79
  @contextlib.contextmanager
75
80
  def get_repo_context(url: str, branch: str):
81
+ if branch == REFS_VALUE_ALL:
82
+ branch = None
76
83
  """Context manager for handling both local and remote repositories."""
77
84
  if url:
78
85
  with tempfile.TemporaryDirectory() as temp_dir:
@@ -81,10 +88,13 @@ def get_repo_context(url: str, branch: str):
81
88
  f"Cloning [{mc.ui.green(url)}] to {mc.utils.file_link(temp_dir)} ..."
82
89
  )
83
90
  repo = Repo.clone_from(url, branch=branch, to_path=temp_dir)
91
+ prev_folder = Env.working_folder
92
+ Env.working_folder = temp_dir
84
93
  try:
85
94
  yield repo, temp_dir
86
95
  finally:
87
96
  repo.close()
97
+ Env.working_folder = prev_folder
88
98
  else:
89
99
  logging.info("get_repo_context: Using local repo...")
90
100
  repo = Repo(".")
@@ -37,6 +37,12 @@ report_template_md = """
37
37
  {%- endfor -%}
38
38
  {{ "\n" }}
39
39
  {%- endfor -%}
40
+ {%- if report.processing_warnings -%}
41
+ {{- "\n\n" }}## Processing Warnings
42
+ {%- for warning in report.processing_warnings -%}
43
+ {{- "\n" }} - {{ warning.message -}}
44
+ {%- endfor -%}
45
+ {%- endif -%}
40
46
  {{- HTML_CR_COMMENT_MARKER -}}
41
47
  """
42
48
  report_template_cli = """
@@ -109,6 +115,13 @@ report_template_cli = """
109
115
  {%- endfor -%}
110
116
  {{ "\n" }}
111
117
  {%- endfor -%}
118
+ {%- if report.processing_warnings -%}
119
+ {{- "\n" }}
120
+ {{- "\n" }}{{ Style.BRIGHT }}⚠️ PROCESSING WARNINGS{{ Style.RESET_ALL -}}
121
+ {%- for warning in report.processing_warnings -%}
122
+ {{- "\n" }} {{ Fore.YELLOW }}- {{ warning.message }}{{ Style.RESET_ALL -}}
123
+ {%- endfor -%}
124
+ {%- endif -%}
112
125
  """
113
126
  retries = 3
114
127
  prompt = """
@@ -184,7 +197,7 @@ Confidence scale:
184
197
  - 1 — Highest, 100% confidence that code requires changes in any context
185
198
  - 2 — Very High
186
199
  - 3 — High
187
- - 4 — Medium - Should not be reported
200
+ - 4 — Medium, Should not be reported
188
201
 
189
202
  (!) - If no issues found according to the criteria, respond with empty list: []
190
203
  """
@@ -204,12 +217,14 @@ Summarize the code review in one sentence.
204
217
  --Issues Detected by You--
205
218
  {{ issues | tojson(indent=2) }}
206
219
  ---
220
+ {% if awards -%}
207
221
  If the code changes include exceptional achievements, you may also present an award to the author in the summary text.
208
222
  - (!) Only give awards to initial codebase authors, NOT to reviewers.
209
223
  - (!) If you give an award, place the hidden <!-- award --> HTML comment on its own line immediately before the award text.
210
224
  --Available Awards--
211
225
  {{ awards }}
212
226
  ---
227
+ {%- endif %}
213
228
  {% if pipeline_out.associated_issue and pipeline_out.associated_issue.title %}
214
229
  ----SUBTASK----
215
230
  Include one sentence about how the code changes address the requirements of the associated issue listed below.
@@ -12,3 +12,4 @@ EXECUTABLE = "gito"
12
12
  TEXT_ICON_URL = 'https://raw.githubusercontent.com/Nayjest/Gito/main/press-kit/logo/gito-bot-1_64top.png' # noqa: E501
13
13
  HTML_TEXT_ICON = f'<a href="https://github.com/Nayjest/Gito"><img src="{TEXT_ICON_URL}" align="left" width=64 height=50 title="Gito v{Env.gito_version}"/></a>' # noqa: E501
14
14
  HTML_CR_COMMENT_MARKER = '<!-- GITO_COMMENT:CODE_REVIEW_REPORT -->'
15
+ REFS_VALUE_ALL = '!all'
@@ -1,3 +1,6 @@
1
+ """
2
+ Gito core business logic.
3
+ """
1
4
  import os
2
5
  import fnmatch
3
6
  import logging
@@ -6,7 +9,6 @@ from pathlib import Path
6
9
  from functools import partial
7
10
 
8
11
  import microcore as mc
9
- from gito.gh_api import gh_api
10
12
  from microcore import ui
11
13
  from git import Repo, Commit
12
14
  from git.exc import GitCommandError
@@ -15,11 +17,12 @@ from unidiff.constants import DEV_NULL
15
17
 
16
18
  from .context import Context
17
19
  from .project_config import ProjectConfig
18
- from .report_struct import Report
19
- from .constants import JSON_REPORT_FILE_NAME
20
+ from .report_struct import ProcessingWarning, Report, RawIssue
21
+ from .constants import JSON_REPORT_FILE_NAME, REFS_VALUE_ALL
20
22
  from .utils import make_streaming_function
21
23
  from .pipeline import Pipeline
22
24
  from .env import Env
25
+ from .gh_api import gh_api
23
26
 
24
27
 
25
28
  def review_subject_is_index(what):
@@ -94,7 +97,7 @@ def get_base_branch(repo: Repo, pr: int | str = None):
94
97
  except AttributeError:
95
98
  try:
96
99
  logging.info(
97
- "Checking if repo has 'main' or 'master' branchs to use as --against branch..."
100
+ "Checking if repo has 'main' or 'master' branches to use as --against branch..."
98
101
  )
99
102
  remote_refs = repo.remotes.origin.refs
100
103
  for branch_name in ['main', 'master']:
@@ -115,91 +118,99 @@ def get_diff(
115
118
  pr: str | int = None
116
119
  ) -> PatchSet | list[PatchedFile]:
117
120
  repo = repo or Repo(".")
121
+ if what == REFS_VALUE_ALL:
122
+ what = get_base_branch(repo, pr=pr)
123
+ # Git's canonical empty tree hash
124
+ against = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
125
+ use_merge_base = False
118
126
  if not against:
119
127
  # 'origin/main', 'origin/master', etc
120
128
  against = get_base_branch(repo, pr=pr)
121
129
  if review_subject_is_index(what):
122
130
  what = None # working copy
123
131
  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
- )
132
+ try:
133
+ if review_subject_is_index(what):
134
+ try:
135
+ current_ref = repo.active_branch.name
136
+ except TypeError:
137
+ # In detached HEAD state, use HEAD directly
138
+ current_ref = "HEAD"
184
139
  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])}"
140
+ "Detected detached HEAD state, using HEAD as current reference"
188
141
  )
189
- against = first_common_ancestor.hexsha
190
- else:
191
- logging.error(f"Can't find other merge parent for {merge_sha}")
192
142
  else:
193
- logging.warning(
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
143
+ current_ref = what
144
+ merge_base = repo.merge_base(current_ref or repo.active_branch.name, against)[0]
200
145
  logging.info(
201
- f"Using merge base: {ui.cyan(merge_base.hexsha[:8])} ({merge_base.summary})"
146
+ f"Merge base({ui.green(current_ref)},{ui.yellow(against)})"
147
+ f" --> {ui.cyan(merge_base.hexsha)}"
202
148
  )
149
+ # if branch is already an ancestor of "against", merge_base == branch ⇒ it’s been merged
150
+ if merge_base.hexsha == repo.commit(current_ref or repo.active_branch.name).hexsha:
151
+ # @todo: check case: reviewing working copy index in main branch #103
152
+ logging.info(
153
+ f"Branch is already merged. ({ui.green(current_ref)} vs {ui.yellow(against)})"
154
+ )
155
+ merge_sha = repo.git.log(
156
+ '--merges',
157
+ '--ancestry-path',
158
+ f'{current_ref}..{against}',
159
+ '-n',
160
+ '1',
161
+ '--pretty=format:%H'
162
+ ).strip()
163
+ if merge_sha:
164
+ logging.info(f"Merge commit is {ui.cyan(merge_sha)}")
165
+ merge_commit = repo.commit(merge_sha)
166
+
167
+ other_merge_parent = None
168
+ for parent in merge_commit.parents:
169
+ logging.info(f"Checking merge parent: {parent.hexsha[:8]}")
170
+ if parent.hexsha == merge_base.hexsha:
171
+ logging.info(f"merge parent is {ui.cyan(parent.hexsha[:8])}, skipping")
172
+ continue
173
+ if not commit_in_branch(repo, parent, against):
174
+ logging.warning(f"merge parent is not in {against}, skipping")
175
+ continue
176
+ logging.info(f"Found other merge parent: {ui.cyan(parent.hexsha[:8])}")
177
+ other_merge_parent = parent
178
+ break
179
+ if other_merge_parent:
180
+ first_common_ancestor = repo.merge_base(other_merge_parent, merge_base)[0]
181
+ # for gito remote (feature_branch vs origin/main)
182
+ # the same merge base appears in first_common_ancestor again
183
+ if first_common_ancestor.hexsha == merge_base.hexsha:
184
+ if merge_base.parents:
185
+ first_common_ancestor = repo.merge_base(
186
+ other_merge_parent, merge_base.parents[0]
187
+ )[0]
188
+ else:
189
+ logging.error(
190
+ "merge_base has no parents, "
191
+ "using merge_base as first_common_ancestor"
192
+ )
193
+ logging.info(
194
+ f"{what} will be compared to "
195
+ f"first common ancestor of {what} and {against}: "
196
+ f"{ui.cyan(first_common_ancestor.hexsha[:8])}"
197
+ )
198
+ against = first_common_ancestor.hexsha
199
+ else:
200
+ logging.error(f"Can't find other merge parent for {merge_sha}")
201
+ else:
202
+ logging.warning(
203
+ f"No merge‐commit found for {current_ref!r}→{against!r}; "
204
+ "falling back to merge‐base diff"
205
+ )
206
+ else:
207
+ # normal case: branch not yet merged
208
+ against = merge_base.hexsha
209
+ logging.info(
210
+ f"Using merge base: {ui.cyan(merge_base.hexsha[:8])} ({merge_base.summary})"
211
+ )
212
+ except Exception as e:
213
+ logging.error(f"Error finding merge base: {e}")
203
214
  logging.info(
204
215
  f"Making diff: {ui.green(what or 'INDEX')} vs {ui.yellow(against)}"
205
216
  )
@@ -225,12 +236,21 @@ def get_diff(
225
236
 
226
237
 
227
238
  def filter_diff(
228
- patch_set: PatchSet | Iterable[PatchedFile], filters: str | list[str]
239
+ patch_set: PatchSet | Iterable[PatchedFile],
240
+ filters: str | list[str],
241
+ exclude: bool = False,
229
242
  ) -> PatchSet | Iterable[PatchedFile]:
230
243
  """
231
244
  Filter the diff files by the given fnmatch filters.
245
+ Args:
246
+ patch_set (PatchSet | Iterable[PatchedFile]): The diff to filter.
247
+ filters (str | list[str]): The fnmatch patterns to filter by.
248
+ exclude (bool): If True, inverse logic (exclude files matching the filters).
249
+ Returns:
250
+ PatchSet | Iterable[PatchedFile]: The filtered diff.
232
251
  """
233
- assert isinstance(filters, (list, str))
252
+ if not isinstance(filters, (list, str)):
253
+ raise ValueError("Filters must be a string or a list of strings")
234
254
  if not isinstance(filters, list):
235
255
  filters = [f.strip() for f in filters.split(",") if f.strip()]
236
256
  if not filters:
@@ -238,7 +258,10 @@ def filter_diff(
238
258
  files = [
239
259
  file
240
260
  for file in patch_set
241
- if any(fnmatch.fnmatch(file.path, pattern) for pattern in filters)
261
+ if (
262
+ not any(fnmatch.fnmatch(file.path, pattern) for pattern in filters) if exclude
263
+ else any(fnmatch.fnmatch(file.path, pattern) for pattern in filters)
264
+ )
242
265
  ]
243
266
  return files
244
267
 
@@ -319,6 +342,8 @@ def _prepare(
319
342
  repo=repo, what=what, against=against, use_merge_base=use_merge_base, pr=pr,
320
343
  )
321
344
  diff = filter_diff(diff, filters)
345
+ if cfg.exclude_files:
346
+ diff = filter_diff(diff, cfg.exclude_files, exclude=True)
322
347
  if not diff:
323
348
  raise NoChangesInContextError()
324
349
  lines = {
@@ -328,9 +353,9 @@ def _prepare(
328
353
  file_diff.path,
329
354
  cfg.max_code_tokens
330
355
  - mc.tokenizing.num_tokens_from_string(str(file_diff)),
331
- use_local_files=review_subject_is_index(what)
356
+ use_local_files=review_subject_is_index(what) or what == REFS_VALUE_ALL
332
357
  )
333
- if file_diff.target_file != DEV_NULL and not file_diff.is_added_file
358
+ if file_diff.target_file != DEV_NULL or what == REFS_VALUE_ALL
334
359
  else ""
335
360
  )
336
361
  for file_diff in diff
@@ -373,6 +398,19 @@ def provide_affected_code_blocks(issues: dict, repo: Repo):
373
398
  i["affected_code"] = block
374
399
 
375
400
 
401
+ def _llm_response_validator(parsed_response: list[dict]):
402
+ """
403
+ Validate that the LLM response is a list of dicts that can be converted to RawIssue.
404
+ """
405
+ if not isinstance(parsed_response, list):
406
+ raise ValueError("Response is not a list")
407
+ for item in parsed_response:
408
+ if not isinstance(item, dict):
409
+ raise ValueError("Response item is not a dict")
410
+ RawIssue(**item)
411
+ return True
412
+
413
+
376
414
  async def review(
377
415
  repo: Repo = None,
378
416
  what: str = None,
@@ -382,6 +420,11 @@ async def review(
382
420
  out_folder: str | os.PathLike | None = None,
383
421
  pr: str | int = None
384
422
  ):
423
+ """
424
+ Conducts a code review.
425
+ Prints the review report to the console and saves it to a file.
426
+ """
427
+ reviewing_all = what == REFS_VALUE_ALL
385
428
  try:
386
429
  repo, cfg, diff, lines = _prepare(
387
430
  repo=repo,
@@ -394,25 +437,62 @@ async def review(
394
437
  except NoChangesInContextError:
395
438
  logging.error("No changes to review")
396
439
  return
440
+
441
+ def input_is_diff(file_diff: PatchedFile) -> bool:
442
+ """
443
+ In case of reviewing all changes, or added files,
444
+ we provide full file content as input.
445
+ Otherwise, we provide the diff and additional file lines separately.
446
+ """
447
+ return not reviewing_all and not file_diff.is_added_file
448
+
397
449
  responses = await mc.llm_parallel(
398
450
  [
399
451
  mc.prompt(
400
452
  cfg.prompt,
401
- input=file_diff,
402
- file_lines=lines[file_diff.path],
453
+ input=(
454
+ file_diff if input_is_diff(file_diff)
455
+ else str(file_diff.path) + ":\n" + lines[file_diff.path]
456
+ ),
457
+ file_lines=lines[file_diff.path] if input_is_diff(file_diff) else None,
403
458
  **cfg.prompt_vars,
404
459
  )
405
460
  for file_diff in diff
406
461
  ],
407
462
  retries=cfg.retries,
408
- parse_json=True,
463
+ parse_json={"validator": _llm_response_validator},
464
+ allow_failures=True,
409
465
  )
466
+ processing_warnings = []
467
+ for i, (res_or_error, file) in enumerate(zip(responses, diff)):
468
+ if isinstance(res_or_error, Exception):
469
+ if isinstance(res_or_error, mc.LLMContextLengthExceededError):
470
+ message = f'File "{file.path}" was skipped due to large size: {str(res_or_error)}.'
471
+ else:
472
+ message = (
473
+ f"File {file.path} was skipped due to error: "
474
+ f"[{type(res_or_error).__name__}] {res_or_error}"
475
+ )
476
+ if not message.endswith('.'):
477
+ message += '.'
478
+ processing_warnings.append(
479
+ ProcessingWarning(
480
+ message=message,
481
+ file=file.path,
482
+ )
483
+ )
484
+ responses[i] = []
485
+
410
486
  issues = {file.path: issues for file, issues in zip(diff, responses) if issues}
411
487
  provide_affected_code_blocks(issues, repo)
412
488
  exec(cfg.post_process, {"mc": mc, **locals()})
413
489
  out_folder = Path(out_folder or repo.working_tree_dir)
414
490
  out_folder.mkdir(parents=True, exist_ok=True)
415
- report = Report(issues=issues, number_of_processed_files=len(diff))
491
+ report = Report(
492
+ number_of_processed_files=len(diff),
493
+ processing_warnings=processing_warnings,
494
+ )
495
+ report.register_issues(issues)
416
496
  ctx = Context(
417
497
  report=report,
418
498
  config=cfg,
@@ -448,6 +528,10 @@ def answer(
448
528
  pr: str | int = None,
449
529
  aux_files: list[str] = None,
450
530
  ) -> str | None:
531
+ """
532
+ Answers a question about the code changes.
533
+ Returns the LLM response as a string.
534
+ """
451
535
  try:
452
536
  repo, config, diff, lines = _prepare(
453
537
  repo=repo,
@@ -477,11 +561,11 @@ def answer(
477
561
  if aux_files or config.aux_files:
478
562
  aux_files_dict = read_files(
479
563
  repo,
480
- (aux_files or list()) + config.aux_files,
564
+ (aux_files or []) + config.aux_files,
481
565
  config.max_code_tokens // 2
482
566
  )
483
567
  else:
484
- aux_files_dict = dict()
568
+ aux_files_dict = {}
485
569
 
486
570
  if not prompt_file and config.answer_prompt.startswith("tpl:"):
487
571
  prompt_file = str(config.answer_prompt)[4:]
@@ -5,3 +5,4 @@ class Env:
5
5
  logging_level: int = 1
6
6
  verbosity: int = 1
7
7
  gito_version: str = version("gito.bot")
8
+ working_folder = "."
@@ -67,9 +67,9 @@ def post_gh_comment(
67
67
  if 200 <= resp.status_code < 300:
68
68
  logging.info(f"Posted review comment to #{pr_or_issue_number} in {gh_repository}")
69
69
  return True
70
- else:
71
- logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
72
- return False
70
+
71
+ logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
72
+ return False
73
73
 
74
74
 
75
75
  def hide_gh_comment(
@@ -33,6 +33,10 @@ class ProjectConfig:
33
33
  when referenced in code review comments.
34
34
  """
35
35
  aux_files: list[str] = field(default_factory=list)
36
+ exclude_files: list[str] = field(default_factory=list)
37
+ """
38
+ List of file patterns to exclude from analysis.
39
+ """
36
40
  pipeline_steps: dict[str, dict | PipelineStep] = field(default_factory=dict)
37
41
  collapse_previous_code_review_comments: bool = field(default=True)
38
42
  """
@@ -1,48 +1,63 @@
1
1
  import json
2
2
  import logging
3
- from dataclasses import dataclass, field, asdict
3
+ from dataclasses import field, asdict, is_dataclass
4
4
  from datetime import datetime
5
5
  from enum import StrEnum
6
6
  from pathlib import Path
7
7
 
8
+ import textwrap
8
9
  import microcore as mc
9
- from colorama import Fore, Style, Back
10
10
  from microcore.utils import file_link
11
- import textwrap
11
+ from colorama import Fore, Style, Back
12
+ from pydantic.dataclasses import dataclass
12
13
 
13
14
  from .constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON, HTML_CR_COMMENT_MARKER
14
15
  from .project_config import ProjectConfig
15
- from .utils import syntax_hint, block_wrap_lr, max_line_len, remove_html_comments
16
+ from .utils import syntax_hint, block_wrap_lr, max_line_len, remove_html_comments, filter_kwargs
16
17
 
17
18
 
18
19
  @dataclass
19
- class Issue:
20
+ class RawIssue:
20
21
  @dataclass
21
22
  class AffectedCode:
22
23
  start_line: int = field()
23
24
  end_line: int | None = field(default=None)
25
+ proposal: str | None = field(default="")
26
+
27
+ title: str = field()
28
+ details: str | None = field(default="")
29
+ severity: int | None = field(default=None)
30
+ confidence: int | None = field(default=None)
31
+ tags: list[str] = field(default_factory=list)
32
+ affected_lines: list[AffectedCode] = field(default_factory=list)
33
+
34
+
35
+ @dataclass
36
+ class Issue(RawIssue):
37
+ @dataclass
38
+ class AffectedCode(RawIssue.AffectedCode):
24
39
  file: str = field(default="")
25
- proposal: str = field(default="")
26
40
  affected_code: str = field(default="")
27
41
 
28
42
  @property
29
43
  def syntax_hint(self) -> str:
30
44
  return syntax_hint(self.file)
31
45
 
32
- id: str = field()
33
- title: str = field()
34
- details: str = field(default="")
35
- severity: int | None = field(default=None)
36
- confidence: int | None = field(default=None)
37
- tags: list[str] = field(default_factory=list)
46
+ id: int | str = field(kw_only=True)
38
47
  file: str = field(default="")
39
48
  affected_lines: list[AffectedCode] = field(default_factory=list)
40
49
 
41
- def __post_init__(self):
42
- self.affected_lines = [
43
- Issue.AffectedCode(**dict(file=self.file) | i)
44
- for i in self.affected_lines
45
- ]
50
+ @staticmethod
51
+ def from_raw_issue(file: str, raw_issue: RawIssue | dict, issue_id: int | str) -> "Issue":
52
+ if is_dataclass(raw_issue):
53
+ raw_issue = asdict(raw_issue)
54
+ params = filter_kwargs(Issue, raw_issue | {"file": file, "id": issue_id})
55
+ for i, obj in enumerate(params.get("affected_lines") or []):
56
+ d = obj if isinstance(obj, dict) else asdict(obj)
57
+ params["affected_lines"][i] = Issue.AffectedCode(
58
+ **filter_kwargs(Issue.AffectedCode, {"file": file} | d)
59
+ )
60
+ return Issue(**params)
46
61
 
47
62
  def github_code_link(self, github_env: dict) -> str:
48
63
  url = (
@@ -57,6 +72,15 @@ class Issue:
57
72
  return url
58
73
 
59
74
 
75
+ @dataclass
76
+ class ProcessingWarning:
77
+ """
78
+ Warning generated during code review of files
79
+ """
80
+ message: str = field()
81
+ file: str | None = field(default=None)
82
+
83
+
60
84
  @dataclass
61
85
  class Report:
62
86
  class Format(StrEnum):
@@ -70,6 +94,7 @@ class Report:
70
94
  created_at: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
71
95
  model: str = field(default_factory=lambda: mc.config().MODEL)
72
96
  pipeline_out: dict = field(default_factory=dict)
97
+ processing_warnings: list[ProcessingWarning] = field(default_factory=list)
73
98
 
74
99
  @property
75
100
  def plain_issues(self):
@@ -79,19 +104,20 @@ class Report:
79
104
  for issue in issues
80
105
  ]
81
106
 
107
+ def register_issues(self, issues: dict[str, list[RawIssue | dict]]):
108
+ for file, file_issues in issues.items():
109
+ for issue in file_issues:
110
+ self.register_issue(file, issue)
111
+
112
+ def register_issue(self, file: str, issue: RawIssue | dict):
113
+ if file not in self.issues:
114
+ self.issues[file] = []
115
+ total = len(self.plain_issues)
116
+ self.issues[file].append(Issue.from_raw_issue(file, issue, issue_id=total + 1))
117
+ self.total_issues = total + 1
118
+
82
119
  def __post_init__(self):
83
- issue_id: int = 0
84
- for file in self.issues.keys():
85
- self.issues[file] = [
86
- Issue(
87
- **{
88
- "id": (issue_id := issue_id + 1),
89
- "file": file,
90
- } | issue
91
- )
92
- for issue in self.issues[file]
93
- ]
94
- self.total_issues = issue_id
120
+ self.total_issues = len(self.plain_issues)
95
121
 
96
122
  def save(self, file_name: str = ""):
97
123
  file_name = file_name or JSON_REPORT_FILE_NAME
@@ -1,6 +1,8 @@
1
+ import logging
1
2
  import re
2
3
  import sys
3
4
  import os
5
+ from dataclasses import fields, is_dataclass
4
6
  from pathlib import Path
5
7
  import importlib.metadata
6
8
  from typing import Optional
@@ -8,7 +10,7 @@ from typing import Optional
8
10
  import typer
9
11
  import git
10
12
  from git import Repo
11
-
13
+ from .env import Env
12
14
 
13
15
  _EXT_TO_HINT: dict[str, str] = {
14
16
  # scripting & languages
@@ -192,10 +194,10 @@ def detect_github_env() -> dict:
192
194
  "github_ref": ref,
193
195
  }
194
196
  # Fallback for local usage: try to get from git
195
- if not repo:
197
+ if not repo or repo == "octocat/Hello-World":
196
198
  git_repo = None
197
199
  try:
198
- git_repo = Repo(".", search_parent_directories=True)
200
+ git_repo = Repo(Env.working_folder, search_parent_directories=True)
199
201
  origin = git_repo.remotes.origin.url
200
202
  # e.g. git@github.com:Nayjest/ai-code-review.git -> Nayjest/ai-code-review
201
203
  match = re.search(r"[:/]([\w\-]+)/([\w\-\.]+?)(\.git)?$", origin)
@@ -240,3 +242,29 @@ def remove_html_comments(text):
240
242
  Removes all HTML comments (<!-- ... -->) from the input text.
241
243
  """
242
244
  return re.sub(r'<!--.*?-->\s*', '', text, flags=re.DOTALL)
245
+
246
+
247
+ def filter_kwargs(cls, kwargs, log_warnings=True):
248
+ """
249
+ Filters the keyword arguments to only include those that are fields of the given dataclass.
250
+ Args:
251
+ cls: The dataclass type to filter against.
252
+ kwargs: A dictionary of keyword arguments.
253
+ log_warnings: If True, logs warnings for fields not in the dataclass.
254
+ Returns:
255
+ A dictionary containing only the fields that are defined in the dataclass.
256
+ """
257
+ if not is_dataclass(cls):
258
+ raise TypeError(f"{cls.__name__} is not a dataclass or pydantic dataclass")
259
+
260
+ cls_fields = {f.name for f in fields(cls)}
261
+ filtered = {}
262
+ for k, v in kwargs.items():
263
+ if k in cls_fields:
264
+ filtered[k] = v
265
+ else:
266
+ if log_warnings:
267
+ logging.warning(
268
+ f"Warning: field '{k}' not in {cls.__name__}, dropping."
269
+ )
270
+ return filtered
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "github-code-review"
3
- version = "3.2.0"
3
+ version = "3.4.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,14 +21,15 @@ packages = [
21
21
 
22
22
  [tool.poetry.dependencies]
23
23
  python = "^3.11"
24
- ai-microcore = "4.3.0"
24
+ ai-microcore = "~4.5"
25
25
  GitPython = "^3.1.44"
26
26
  unidiff = "^0.7.5"
27
27
  google-generativeai = "^0.8.5"
28
- anthropic = "^0.57.1"
29
- typer = "^0.16.0"
28
+ anthropic = ">=0.57.1,<1"
29
+ typer = ">=0.16.0,<0.21"
30
30
  ghapi = "~=1.0.6"
31
31
  jira = "^3.8.0"
32
+ pydantic = "^2.12.3"
32
33
 
33
34
  [tool.poetry.group.dev.dependencies]
34
35
  flake8 = "*"
@@ -40,10 +41,10 @@ pyflakes = "*"
40
41
  poetry = "*"
41
42
 
42
43
  [tool.poetry.group.test.dependencies]
43
- pytest = "^7.4.3"
44
- pytest-asyncio = ">=0.21.0"
44
+ pytest = ">=7.4.3,<10"
45
+ pytest-asyncio = ">=0.21.0,<2"
45
46
  pytest-mock = "^3.12.0"
46
- pytest-cov = "^4.1.0"
47
+ pytest-cov = ">=4.1.0,<8"
47
48
 
48
49
  [build-system]
49
50
  requires = ["poetry-core"]