git-cai-cli 0.16.0__tar.gz → 0.16.2__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 (116) hide show
  1. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.github/workflows/python-tests.yml +1 -1
  2. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.github/workflows/release_aur.yml +5 -2
  3. git_cai_cli-0.16.2/.linters/.checkov.yml +7 -0
  4. git_cai_cli-0.16.2/.linters/.proselintrc.json +32 -0
  5. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.mega-linter.yml +14 -6
  6. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/Makefile +2 -2
  7. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/PKG-INFO +1 -1
  8. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/cai_config.yml +2 -1
  9. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/docs/man/git-cai.1 +2 -2
  10. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/_version.py +3 -3
  11. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/config.py +1 -1
  12. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/gitutils.py +9 -2
  13. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/llm.py +36 -2
  14. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/squash.py +11 -8
  15. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli.egg-info/PKG-INFO +1 -1
  16. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli.egg-info/SOURCES.txt +1 -2
  17. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli.egg-info/scm_file_list.json +82 -83
  18. git_cai_cli-0.16.2/src/git_cai_cli.egg-info/scm_version.json +8 -0
  19. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_llm.py +61 -0
  20. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_signoff.py +14 -0
  21. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_squash.py +48 -0
  22. git_cai_cli-0.16.2/uv.lock +898 -0
  23. git_cai_cli-0.16.0/.linters/.checkov.yml +0 -2
  24. git_cai_cli-0.16.0/.linters/.markdown-link-check.json +0 -10
  25. git_cai_cli-0.16.0/.linters/.proselintrc +0 -78
  26. git_cai_cli-0.16.0/src/git_cai_cli.egg-info/scm_version.json +0 -8
  27. git_cai_cli-0.16.0/uv.lock +0 -826
  28. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.caiignore +0 -0
  29. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.gitattributes +0 -0
  30. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.github/cd/.SRCINFO +0 -0
  31. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.github/cd/PKGBUILD +0 -0
  32. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.github/ci/_version.py +0 -0
  33. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.github/ci/cai_config.ci.yml +0 -0
  34. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.github/ci/tokens.ci.yml +0 -0
  35. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.github/workflows/release.yml +0 -0
  36. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.gitignore +0 -0
  37. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.linters/.bandit.yml +0 -0
  38. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.linters/.flake8 +0 -0
  39. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.linters/.ls-lint.yml +0 -0
  40. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.linters/.markdownlint.json +0 -0
  41. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.linters/.pylintrc +0 -0
  42. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.linters/.yamllint.yml +0 -0
  43. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.linters/check_git_branch_name.sh +0 -0
  44. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.linters/lychee.toml +0 -0
  45. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.linters/pyrightconfig.json +0 -0
  46. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.markdownlintignore +0 -0
  47. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.semgrepignore +0 -0
  48. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/.trivyignore +0 -0
  49. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/CLAUDE.md +0 -0
  50. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/LICENSE +0 -0
  51. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/README.md +0 -0
  52. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/docs/git-cai.txt +0 -0
  53. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/pyproject.toml +0 -0
  54. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/setup.cfg +0 -0
  55. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/__init__.py +0 -0
  56. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/cli/__init__.py +0 -0
  57. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/cli/cli.py +0 -0
  58. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/cli/helptext.py +0 -0
  59. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/cli/modes.py +0 -0
  60. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/__init__.py +0 -0
  61. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/changelog.py +0 -0
  62. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/completion.py +0 -0
  63. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/doctor.py +0 -0
  64. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/explain.py +0 -0
  65. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/generation.py +0 -0
  66. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/init.py +0 -0
  67. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/languages.py +0 -0
  68. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/options.py +0 -0
  69. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/pr.py +0 -0
  70. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/prompts_fallback.py +0 -0
  71. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/release.py +0 -0
  72. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/secrets.py +0 -0
  73. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/spinner.py +0 -0
  74. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/split.py +0 -0
  75. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/stats.py +0 -0
  76. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/core/validate.py +0 -0
  77. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli/main.py +0 -0
  78. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli.egg-info/dependency_links.txt +0 -0
  79. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli.egg-info/entry_points.txt +0 -0
  80. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli.egg-info/requires.txt +0 -0
  81. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/src/git_cai_cli.egg-info/top_level.txt +0 -0
  82. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/conftest.py +0 -0
  83. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/integration/test_cli_integration.py +0 -0
  84. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/integration/test_config_integration.py +0 -0
  85. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/integration/test_gitutils_integration.py +0 -0
  86. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/integration/test_modes_integration.py +0 -0
  87. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/integration/test_options_integration.py +0 -0
  88. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/integration/test_pr_integration.py +0 -0
  89. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/integration/test_squash_integration.py +0 -0
  90. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_amend.py +0 -0
  91. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_branch_context.py +0 -0
  92. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_changelog.py +0 -0
  93. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_classification.py +0 -0
  94. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_cli.py +0 -0
  95. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_completion.py +0 -0
  96. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_config.py +0 -0
  97. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_conventional.py +0 -0
  98. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_doctor.py +0 -0
  99. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_explain.py +0 -0
  100. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_generation.py +0 -0
  101. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_gitutils.py +0 -0
  102. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_helptext.py +0 -0
  103. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_init.py +0 -0
  104. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_main.py +0 -0
  105. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_modes.py +0 -0
  106. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_options.py +0 -0
  107. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_pr.py +0 -0
  108. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_print.py +0 -0
  109. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_prompt_loading.py +0 -0
  110. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_release.py +0 -0
  111. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_secrets.py +0 -0
  112. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_set_config.py +0 -0
  113. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_spinner.py +0 -0
  114. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_split.py +0 -0
  115. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_stats.py +0 -0
  116. {git_cai_cli-0.16.0 → git_cai_cli-0.16.2}/tests/unit/test_validate.py +0 -0
@@ -99,7 +99,7 @@ jobs:
99
99
 
100
100
  # MegaLinter
101
101
  - name: MegaLinter
102
- uses: oxsecurity/megalinter@42bb470545e359597e7f12156947c436e4e3fb9a
102
+ uses: oxsecurity/megalinter@15e5b45552097e318c93de385779ce3b1084052c # v10.0.0
103
103
  id: ml
104
104
  env:
105
105
  VALIDATE_ALL_CODEBASE: false
@@ -9,6 +9,9 @@ on:
9
9
  description: "Release version (e.g. 0.6.5)"
10
10
  required: true
11
11
 
12
+ permissions:
13
+ contents: read
14
+
12
15
  jobs:
13
16
  aur:
14
17
  runs-on: ubuntu-slim
@@ -48,8 +51,8 @@ jobs:
48
51
  echo "Downloading tag: ${TAG}"
49
52
 
50
53
  curl -fL \
51
- -o cai-${VERSION}.tar.gz \
52
- https://github.com/thorstenfoltz/cai/archive/refs/tags/${TAG}.tar.gz
54
+ -o "cai-${VERSION}.tar.gz" \
55
+ "https://github.com/thorstenfoltz/cai/archive/refs/tags/${TAG}.tar.gz"
53
56
 
54
57
  - name: Compute sha256sum
55
58
  id: sha
@@ -0,0 +1,7 @@
1
+ # skip-path only filters directory scans. On pull requests MegaLinter passes
2
+ # the changed files to checkov with --file, which ignores skip-path, so
3
+ # workflow findings must be suppressed per check instead.
4
+ # CKV_GHA_7: workflow_dispatch inputs must be empty; release_aur.yml needs a
5
+ # manual version input to republish a release.
6
+ skip-check:
7
+ - CKV_GHA_7
@@ -0,0 +1,32 @@
1
+ {
2
+ "checks": {
3
+ "annotations": false,
4
+ "archaism": false,
5
+ "cliches": true,
6
+ "dates_times": true,
7
+ "hedging": true,
8
+ "industrial_language": false,
9
+ "lexical_illusions": false,
10
+ "malapropisms": true,
11
+ "misc": true,
12
+ "misc.phrasal_adjectives": false,
13
+ "mixed_metaphors": true,
14
+ "mondegreens": true,
15
+ "needless_variants": false,
16
+ "nonwords": true,
17
+ "oxymorons": true,
18
+ "psychology": true,
19
+ "redundancy": true,
20
+ "restricted": false,
21
+ "skunked_terms": true,
22
+ "social_awareness": true,
23
+ "spelling": true,
24
+ "spelling.athletes": false,
25
+ "terms": true,
26
+ "typography": true,
27
+ "typography.symbols": false,
28
+ "uncomparables": true,
29
+ "weasel_words": true,
30
+ "weasel_words.very": false
31
+ }
32
+ }
@@ -2,20 +2,26 @@
2
2
  # See all available variables at https://megalinter.io/configuration/
3
3
  # and in linters documentation
4
4
 
5
+ ADDITIONAL_EXCLUDED_DIRECTORIES:
6
+ # Build/test caches: gitignored, but project-mode linters scan them anyway.
7
+ - __pycache__
8
+ - .mypy_cache
9
+ - .pytest_cache
10
+ - .ruff_cache
11
+ - .benchmarks
5
12
  APPLY_FIXES: none # all, none, or list of linter keys
6
13
  BASH_SHELLCHECK_ARGUMENTS: -e "SC2162"
7
14
  CLEAR_REPORT_FOLDER: true
8
15
  DISABLE_ERRORS: false
9
16
  ENABLE_LINTERS:
17
+ - ACTION_ACTIONLINT
10
18
  - BASH_EXEC
11
19
  - BASH_SHELLCHECK
12
20
  - BASH_SHFMT
13
21
  - JSON_JSONLINT
14
22
  - JSON_PRETTIER
15
23
  - JSON_V8R
16
- - MAKEFILE_CHECKMAKE
17
24
  - MARKDOWN_MARKDOWNLINT
18
- - MARKDOWN_MARKDOWN_LINK_CHECK
19
25
  - MARKDOWN_MARKDOWN_TABLE_FORMATTER
20
26
  - PYTHON_BLACK
21
27
  - PYTHON_BANDIT
@@ -26,9 +32,8 @@ ENABLE_LINTERS:
26
32
  - PYTHON_PYRIGHT
27
33
  - PYTHON_RUFF
28
34
  - PYTHON_RUFF_FORMAT
35
+ - REPOSITORY_BETTERLEAKS
29
36
  - REPOSITORY_CHECKOV
30
- - REPOSITORY_GITLEAKS
31
- - REPOSITORY_KICS
32
37
  - REPOSITORY_LS_LINT
33
38
  - REPOSITORY_SECRETLINT
34
39
  - REPOSITORY_SEMGREP
@@ -48,7 +53,8 @@ IGNORE_GITIGNORED_FILES: true
48
53
  LINTER_RULES_PATH: .linters
49
54
  LOG_LEVEL: INFO
50
55
  MARKDOWN_DEFAULT_STYLE: markdownlint
51
- MARKDOWN_MARKDOWN_LINK_CHECK_RULES_PATH: .linters
56
+ # Pins the Docker image for local runs to the version the CI workflow pins.
57
+ MEGALINTER_VERSION: v10.0.0
52
58
  PARALLEL: true
53
59
  PRINT_ALPACA: false
54
60
  PYTHON_BANDIT_RULES_PATH: .linters
@@ -68,8 +74,10 @@ REPOSITORY_SEMGREP_RULESETS:
68
74
  "p/secure-defaults",
69
75
  ]
70
76
  SECURITY_SUGGESTIONS: false
77
+ # Lob API keys look like `test_<alnum>`, so every pytest function name is
78
+ # reported as a verified secret. No other detector is affected.
79
+ REPOSITORY_TRUFFLEHOG_ARGUMENTS: --exclude-detectors=Lob
71
80
  SHOW_ELAPSED_TIME: true
72
81
  SHOW_SKIPPED_LINTERS: true
73
82
  SPELL_PROSELINT_RULES_PATH: .linters
74
- SPELL_VALE_RULES_PATH: .linters
75
83
  VALIDATE_ALL_CODEBASE: false
@@ -42,10 +42,10 @@ clean: ## Clean cache of uv and delete virtual environment
42
42
 
43
43
  lint:
44
44
  @sh ./.linters/check_git_branch_name.sh
45
- @npx mega-linter-runner
45
+ @npx mega-linter-runner@v10
46
46
 
47
47
  lint-fix: ## Lints the code using sqlfluff and fixes the issues
48
- @npx mega-linter-runner --fix
48
+ @npx mega-linter-runner@v10 --fix
49
49
 
50
50
  test: ## Runs tests
51
51
  @$(UV) run pytest
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-cai-cli
3
- Version: 0.16.0
3
+ Version: 0.16.2
4
4
  Summary: Use LLM to create git commit messages
5
5
  Author-email: Thorsten Foltz <thorsten.foltz@live.com>
6
6
  License-Expression: MIT
@@ -16,6 +16,7 @@ pr_to_file: false
16
16
  pr_file_name: PR_DESCRIPTION.md
17
17
  pr_prompt_file: "/home/thorsten/.config/cai/pr_prompt.md"
18
18
  stats: true
19
+ signoff: true
19
20
  anthropic:
20
21
  model: claude-haiku-4-5
21
22
  temperature: 0
@@ -37,7 +38,7 @@ ollama:
37
38
  temperature: 0
38
39
  timeout: 300
39
40
  openai:
40
- model: gpt-5.4-mini
41
+ model: gpt-5.6-luna
41
42
  xai:
42
43
  model: grok-4.3
43
44
  temperature: 0
@@ -2,12 +2,12 @@
2
2
  .\" Title: git-cai
3
3
  .\" Author: Thorsten Foltz
4
4
  .\" Generator: Asciidoctor 2.0.26
5
- .\" Date: 2026-07-28
5
+ .\" Date: 2026-08-06
6
6
  .\" Manual: \ \&
7
7
  .\" Source: \ \&
8
8
  .\" Language: English
9
9
  .\"
10
- .TH "GIT\-CAI" "1" "2026-07-28" "\ \&" "\ \&"
10
+ .TH "GIT\-CAI" "1" "2026-08-06" "\ \&" "\ \&"
11
11
  .ie \n(.g .ds Aq \(aq
12
12
  .el .ds Aq '
13
13
  .ss \n[.ss] 0
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.16.0'
22
- __version_tuple__ = version_tuple = (0, 16, 0)
21
+ __version__ = version = '0.16.2'
22
+ __version_tuple__ = version_tuple = (0, 16, 2)
23
23
 
24
- __commit_id__ = commit_id = 'g7f8a0581c'
24
+ __commit_id__ = commit_id = 'g2674674a6'
@@ -40,7 +40,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
40
40
  # ``max_tokens`` is still read as a legacy alias (see llm.py)
41
41
  "max_output_tokens": 32768,
42
42
  },
43
- "openai": {"model": "gpt-5.4-mini"},
43
+ "openai": {"model": "gpt-5.6-luna"},
44
44
  "deepseek": {"model": "deepseek-chat", "temperature": 0},
45
45
  "gemini": {"model": "gemini-3.1-flash-lite", "temperature": 0},
46
46
  "groq": {"model": "openai/gpt-oss-20b", "temperature": 0},
@@ -259,8 +259,15 @@ def append_signoff(message: str, identity: tuple[str, str] | None = None) -> str
259
259
  if not stripped:
260
260
  return trailer
261
261
 
262
- last_line = stripped.rsplit("\n", 1)[-1]
263
- if _TRAILER_LINE_RE.match(last_line):
262
+ # Only join an existing trailer block directly: a block is the final
263
+ # paragraph and must consist solely of ``Key: value`` lines. A message
264
+ # with a single paragraph never qualifies — a lone conventional subject
265
+ # ("feat: add x") looks like a trailer line, and gluing the sign-off to
266
+ # it makes git stop recognizing the trailer at all.
267
+ paragraphs = stripped.split("\n\n")
268
+ if len(paragraphs) > 1 and all(
269
+ _TRAILER_LINE_RE.match(line) for line in paragraphs[-1].splitlines()
270
+ ):
264
271
  return f"{stripped}\n{trailer}"
265
272
 
266
273
  return f"{stripped}\n\n{trailer}"
@@ -37,7 +37,9 @@ log = logging.getLogger(__name__)
37
37
 
38
38
 
39
39
  # Models that reject any non-default ``temperature`` (return HTTP 400).
40
- # Extend this list as new restricted models appear.
40
+ # Pure fast path: it saves the rejected request. Models missing here are
41
+ # still handled — ``_http_post`` retries once without the field — so add
42
+ # an entry only when a warning shows up in practice.
41
43
  _TEMPERATURE_UNSUPPORTED_PATTERNS = [
42
44
  re.compile(r"^gpt-5", re.IGNORECASE), # OpenAI GPT-5 family (incl. mini/nano)
43
45
  re.compile(r"^o[134](-|$)", re.IGNORECASE), # OpenAI o1/o3/o4 reasoning series
@@ -123,14 +125,46 @@ def _get_http_session() -> requests.Session:
123
125
  return _build_retrying_session()
124
126
 
125
127
 
128
+ def _strip_temperature(payload: Any) -> bool:
129
+ """Remove every ``temperature`` key from a request body, in place.
130
+
131
+ Providers nest it differently — top level for the OpenAI-compatible
132
+ APIs and Anthropic, ``generationConfig`` for Gemini, ``options`` for
133
+ Ollama — so recurse into nested dicts. Returns True if anything was
134
+ removed.
135
+ """
136
+ if not isinstance(payload, dict):
137
+ return False
138
+ removed = payload.pop("temperature", None) is not None
139
+ for value in payload.values():
140
+ if _strip_temperature(value):
141
+ removed = True
142
+ return removed
143
+
144
+
126
145
  def _http_post(*args, **kwargs):
127
146
  """POST via the module-level retrying session.
128
147
 
129
148
  Single patch-point for tests; never uses ``requests.post`` directly so
130
149
  every provider call benefits from urllib3 retry/backoff on transient
131
150
  failures (429 / 5xx).
151
+
152
+ Models that reject ``temperature`` answer 400 instead of ignoring the
153
+ field, and new ones keep appearing, so a hardcoded model list can only
154
+ ever be a fast path (see ``_TEMPERATURE_UNSUPPORTED_PATTERNS``). When
155
+ the provider's own error names the field, drop it and retry once
156
+ rather than failing the run.
132
157
  """
133
- return _get_http_session().post(*args, **kwargs)
158
+ response = _get_http_session().post(*args, **kwargs)
159
+ if response.status_code == 400 and "temperature" in response.text.lower():
160
+ if _strip_temperature(kwargs.get("json")):
161
+ log.warning(
162
+ "Provider rejected the configured temperature (HTTP 400); "
163
+ "retrying without it. Remove 'temperature' from this "
164
+ "model's config block to skip the extra request."
165
+ )
166
+ response = _get_http_session().post(*args, **kwargs)
167
+ return response
134
168
 
135
169
 
136
170
  def load_prompt_file(
@@ -409,7 +409,17 @@ def squash_branch(
409
409
  log.info("Squash summary generated in %.2fs", elapsed)
410
410
  generator.record_elapsed(int(elapsed * 1000))
411
411
 
412
- # 4) Let user edit the summary without making a commit yet
412
+ # 4) Let user edit the summary without making a commit yet.
413
+ # The trailer is added before the editor opens — same as commit mode,
414
+ # so the user sees (and can drop) it instead of it appearing only
415
+ # after the squash commit was already written.
416
+ if apply_signoff:
417
+ try:
418
+ summary_message = append_signoff(summary_message)
419
+ except RuntimeError as e:
420
+ log.error("%s", e)
421
+ sys.exit(1)
422
+
413
423
  log.info(
414
424
  "Opening editor for final squash commit message. Save = continue, exit w/o save = cancel."
415
425
  )
@@ -449,13 +459,6 @@ def squash_branch(
449
459
  finally:
450
460
  tf_name.unlink(missing_ok=True)
451
461
 
452
- if apply_signoff:
453
- try:
454
- final_message = append_signoff(final_message)
455
- except RuntimeError as e:
456
- log.error("%s", e)
457
- sys.exit(1)
458
-
459
462
  # 5) Perform squash
460
463
  subprocess.run(["git", "reset", "--soft", merge_base], check=True)
461
464
  subprocess.run(["git", "commit", "-m", final_message], check=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-cai-cli
3
- Version: 0.16.0
3
+ Version: 0.16.2
4
4
  Summary: Use LLM to create git commit messages
5
5
  Author-email: Thorsten Foltz <thorsten.foltz@live.com>
6
6
  License-Expression: MIT
@@ -24,9 +24,8 @@ uv.lock
24
24
  .linters/.checkov.yml
25
25
  .linters/.flake8
26
26
  .linters/.ls-lint.yml
27
- .linters/.markdown-link-check.json
28
27
  .linters/.markdownlint.json
29
- .linters/.proselintrc
28
+ .linters/.proselintrc.json
30
29
  .linters/.pylintrc
31
30
  .linters/.yamllint.yml
32
31
  .linters/check_git_branch_name.sh
@@ -1,105 +1,104 @@
1
1
  {
2
2
  "files": [
3
- ".gitattributes",
4
- ".semgrepignore",
5
- ".mega-linter.yml",
6
- "Makefile",
7
- ".trivyignore",
8
- "pyproject.toml",
9
- "cai_config.yml",
10
- ".markdownlintignore",
11
- "LICENSE",
12
- "CLAUDE.md",
13
- "README.md",
14
- "uv.lock",
15
- ".gitignore",
16
3
  ".caiignore",
17
- ".github/workflows/python-tests.yml",
18
- ".github/workflows/release_aur.yml",
19
- ".github/workflows/release.yml",
20
- ".github/cd/PKGBUILD",
4
+ ".gitattributes",
21
5
  ".github/cd/.SRCINFO",
6
+ ".github/cd/PKGBUILD",
22
7
  ".github/ci/_version.py",
23
8
  ".github/ci/cai_config.ci.yml",
24
9
  ".github/ci/tokens.ci.yml",
10
+ ".github/workflows/python-tests.yml",
11
+ ".github/workflows/release.yml",
12
+ ".github/workflows/release_aur.yml",
13
+ ".gitignore",
14
+ ".linters/.bandit.yml",
15
+ ".linters/.checkov.yml",
16
+ ".linters/.flake8",
17
+ ".linters/.ls-lint.yml",
18
+ ".linters/.markdownlint.json",
19
+ ".linters/.proselintrc.json",
20
+ ".linters/.pylintrc",
21
+ ".linters/.yamllint.yml",
22
+ ".linters/check_git_branch_name.sh",
23
+ ".linters/lychee.toml",
24
+ ".linters/pyrightconfig.json",
25
+ ".markdownlintignore",
26
+ ".mega-linter.yml",
27
+ ".semgrepignore",
28
+ ".trivyignore",
29
+ "CLAUDE.md",
30
+ "LICENSE",
31
+ "Makefile",
32
+ "README.md",
33
+ "cai_config.yml",
34
+ "docs/git-cai.txt",
35
+ "docs/man/git-cai.1",
36
+ "pyproject.toml",
37
+ "src/git_cai_cli/__init__.py",
38
+ "src/git_cai_cli/cli/__init__.py",
39
+ "src/git_cai_cli/cli/cli.py",
40
+ "src/git_cai_cli/cli/helptext.py",
41
+ "src/git_cai_cli/cli/modes.py",
42
+ "src/git_cai_cli/core/__init__.py",
43
+ "src/git_cai_cli/core/changelog.py",
44
+ "src/git_cai_cli/core/completion.py",
45
+ "src/git_cai_cli/core/config.py",
46
+ "src/git_cai_cli/core/doctor.py",
47
+ "src/git_cai_cli/core/explain.py",
48
+ "src/git_cai_cli/core/generation.py",
49
+ "src/git_cai_cli/core/gitutils.py",
50
+ "src/git_cai_cli/core/init.py",
51
+ "src/git_cai_cli/core/languages.py",
52
+ "src/git_cai_cli/core/llm.py",
53
+ "src/git_cai_cli/core/options.py",
54
+ "src/git_cai_cli/core/pr.py",
55
+ "src/git_cai_cli/core/prompts_fallback.py",
56
+ "src/git_cai_cli/core/release.py",
57
+ "src/git_cai_cli/core/secrets.py",
58
+ "src/git_cai_cli/core/spinner.py",
59
+ "src/git_cai_cli/core/split.py",
60
+ "src/git_cai_cli/core/squash.py",
61
+ "src/git_cai_cli/core/stats.py",
62
+ "src/git_cai_cli/core/validate.py",
63
+ "src/git_cai_cli/main.py",
25
64
  "tests/conftest.py",
26
- "tests/integration/test_options_integration.py",
27
- "tests/integration/test_modes_integration.py",
65
+ "tests/integration/test_cli_integration.py",
66
+ "tests/integration/test_config_integration.py",
28
67
  "tests/integration/test_gitutils_integration.py",
68
+ "tests/integration/test_modes_integration.py",
69
+ "tests/integration/test_options_integration.py",
29
70
  "tests/integration/test_pr_integration.py",
30
- "tests/integration/test_config_integration.py",
31
71
  "tests/integration/test_squash_integration.py",
32
- "tests/integration/test_cli_integration.py",
72
+ "tests/unit/test_amend.py",
73
+ "tests/unit/test_branch_context.py",
74
+ "tests/unit/test_changelog.py",
33
75
  "tests/unit/test_classification.py",
34
- "tests/unit/test_prompt_loading.py",
35
- "tests/unit/test_stats.py",
76
+ "tests/unit/test_cli.py",
77
+ "tests/unit/test_completion.py",
78
+ "tests/unit/test_config.py",
36
79
  "tests/unit/test_conventional.py",
37
- "tests/unit/test_branch_context.py",
38
- "tests/unit/test_signoff.py",
39
80
  "tests/unit/test_doctor.py",
81
+ "tests/unit/test_explain.py",
40
82
  "tests/unit/test_generation.py",
41
- "tests/unit/test_squash.py",
42
- "tests/unit/test_print.py",
43
- "tests/unit/test_options.py",
44
- "tests/unit/test_llm.py",
45
- "tests/unit/test_changelog.py",
83
+ "tests/unit/test_gitutils.py",
46
84
  "tests/unit/test_helptext.py",
85
+ "tests/unit/test_init.py",
86
+ "tests/unit/test_llm.py",
87
+ "tests/unit/test_main.py",
88
+ "tests/unit/test_modes.py",
89
+ "tests/unit/test_options.py",
47
90
  "tests/unit/test_pr.py",
48
- "tests/unit/test_secrets.py",
49
- "tests/unit/test_split.py",
50
- "tests/unit/test_explain.py",
91
+ "tests/unit/test_print.py",
92
+ "tests/unit/test_prompt_loading.py",
51
93
  "tests/unit/test_release.py",
52
- "tests/unit/test_amend.py",
53
- "tests/unit/test_gitutils.py",
94
+ "tests/unit/test_secrets.py",
54
95
  "tests/unit/test_set_config.py",
55
- "tests/unit/test_completion.py",
56
- "tests/unit/test_main.py",
57
- "tests/unit/test_modes.py",
58
- "tests/unit/test_validate.py",
59
- "tests/unit/test_cli.py",
96
+ "tests/unit/test_signoff.py",
60
97
  "tests/unit/test_spinner.py",
61
- "tests/unit/test_init.py",
62
- "tests/unit/test_config.py",
63
- "src/git_cai_cli/main.py",
64
- "src/git_cai_cli/__init__.py",
65
- "src/git_cai_cli/cli/modes.py",
66
- "src/git_cai_cli/cli/__init__.py",
67
- "src/git_cai_cli/cli/cli.py",
68
- "src/git_cai_cli/cli/helptext.py",
69
- "src/git_cai_cli/core/languages.py",
70
- "src/git_cai_cli/core/prompts_fallback.py",
71
- "src/git_cai_cli/core/split.py",
72
- "src/git_cai_cli/core/pr.py",
73
- "src/git_cai_cli/core/completion.py",
74
- "src/git_cai_cli/core/llm.py",
75
- "src/git_cai_cli/core/stats.py",
76
- "src/git_cai_cli/core/config.py",
77
- "src/git_cai_cli/core/release.py",
78
- "src/git_cai_cli/core/explain.py",
79
- "src/git_cai_cli/core/__init__.py",
80
- "src/git_cai_cli/core/spinner.py",
81
- "src/git_cai_cli/core/options.py",
82
- "src/git_cai_cli/core/changelog.py",
83
- "src/git_cai_cli/core/validate.py",
84
- "src/git_cai_cli/core/secrets.py",
85
- "src/git_cai_cli/core/init.py",
86
- "src/git_cai_cli/core/generation.py",
87
- "src/git_cai_cli/core/squash.py",
88
- "src/git_cai_cli/core/doctor.py",
89
- "src/git_cai_cli/core/gitutils.py",
90
- "docs/git-cai.txt",
91
- "docs/man/git-cai.1",
92
- ".linters/.checkov.yml",
93
- ".linters/.pylintrc",
94
- ".linters/.ls-lint.yml",
95
- ".linters/pyrightconfig.json",
96
- ".linters/.markdown-link-check.json",
97
- ".linters/check_git_branch_name.sh",
98
- ".linters/lychee.toml",
99
- ".linters/.markdownlint.json",
100
- ".linters/.yamllint.yml",
101
- ".linters/.proselintrc",
102
- ".linters/.flake8",
103
- ".linters/.bandit.yml"
98
+ "tests/unit/test_split.py",
99
+ "tests/unit/test_squash.py",
100
+ "tests/unit/test_stats.py",
101
+ "tests/unit/test_validate.py",
102
+ "uv.lock"
104
103
  ]
105
104
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.16.2",
3
+ "distance": 0,
4
+ "node": "g2674674a6ae699ea810dde77263bb4e195d1d625",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-15"
8
+ }
@@ -8,8 +8,10 @@ from unittest.mock import MagicMock, patch
8
8
  import pytest
9
9
  from git_cai_cli.core.llm import (
10
10
  CommitMessageGenerator,
11
+ _http_post,
11
12
  _model_rejects_temperature,
12
13
  _resolve_temperature,
14
+ _strip_temperature,
13
15
  )
14
16
 
15
17
 
@@ -1383,6 +1385,65 @@ def test_ollama_start_skips_start_new_session_on_windows(monkeypatch, generator)
1383
1385
  # ---------------------------------------------------------------------------
1384
1386
 
1385
1387
 
1388
+ def test_strip_temperature_removes_nested_occurrences():
1389
+ payload = {
1390
+ "model": "m",
1391
+ "temperature": 0,
1392
+ "generationConfig": {"temperature": 0.7, "topK": 1},
1393
+ "options": {"temperature": 0},
1394
+ }
1395
+ assert _strip_temperature(payload) is True
1396
+ assert payload == {"model": "m", "generationConfig": {"topK": 1}, "options": {}}
1397
+ # nothing left to remove -> no pointless retry
1398
+ assert _strip_temperature(payload) is False
1399
+
1400
+
1401
+ def test_http_post_retries_without_temperature_on_400(caplog):
1402
+ """A model not covered by the known-list rejects the field with 400;
1403
+ the request is replayed once without it instead of failing the run."""
1404
+ rejected = MagicMock(
1405
+ status_code=400,
1406
+ text=(
1407
+ '{"error":{"message":"Unsupported value: \'temperature\'",'
1408
+ '"param":"temperature"}}'
1409
+ ),
1410
+ )
1411
+ accepted = MagicMock(status_code=200, text="{}")
1412
+ session = MagicMock()
1413
+ session.post.side_effect = [rejected, accepted]
1414
+ payload = {"model": "future-model", "temperature": 0}
1415
+
1416
+ with (
1417
+ patch(
1418
+ f"{CommitMessageGenerator.__module__}._get_http_session",
1419
+ return_value=session,
1420
+ ),
1421
+ caplog.at_level(logging.WARNING),
1422
+ ):
1423
+ result = _http_post("https://api.example.com/v1", json=payload)
1424
+
1425
+ assert result is accepted
1426
+ assert session.post.call_count == 2
1427
+ assert session.post.call_args_list[1].kwargs["json"] == {"model": "future-model"}
1428
+ assert "retrying without it" in caplog.text
1429
+
1430
+
1431
+ def test_http_post_does_not_retry_on_unrelated_400():
1432
+ rejected = MagicMock(
1433
+ status_code=400, text='{"error":{"message":"model not found"}}'
1434
+ )
1435
+ session = MagicMock()
1436
+ session.post.return_value = rejected
1437
+
1438
+ with patch(
1439
+ f"{CommitMessageGenerator.__module__}._get_http_session", return_value=session
1440
+ ):
1441
+ result = _http_post("https://api.example.com/v1", json={"temperature": 0})
1442
+
1443
+ assert result is rejected
1444
+ assert session.post.call_count == 1
1445
+
1446
+
1386
1447
  def test_model_rejects_temperature_openai_gpt5():
1387
1448
  assert _model_rejects_temperature("gpt-5.4-mini") is True
1388
1449
  assert _model_rejects_temperature("gpt-5.2") is True
@@ -116,6 +116,20 @@ def test_append_signoff_when_other_signoff_already_present():
116
116
  )
117
117
 
118
118
 
119
+ def test_append_signoff_separates_single_line_conventional_subject():
120
+ """A lone subject like ``feat: x`` matches the trailer pattern but is
121
+ not a trailer block — gluing the sign-off to it makes git drop the
122
+ trailer entirely (``git interpret-trailers --parse`` returns nothing)."""
123
+ out = append_signoff("feat: add signoff", identity=("Alice", "alice@example.com"))
124
+ assert out == "feat: add signoff\n\nSigned-off-by: Alice <alice@example.com>"
125
+
126
+
127
+ def test_append_signoff_ignores_trailer_lookalike_at_end_of_body():
128
+ msg = "Add thing\n\nBody line\nNote: this is prose, not a trailer block"
129
+ out = append_signoff(msg, identity=("Alice", "alice@example.com"))
130
+ assert out == f"{msg}\n\nSigned-off-by: Alice <alice@example.com>"
131
+
132
+
119
133
  def test_append_signoff_on_empty_message_just_returns_trailer():
120
134
  out = append_signoff("", identity=("Alice", "alice@example.com"))
121
135
  assert out == "Signed-off-by: Alice <alice@example.com>"