gac 3.10.3__py3-none-any.whl

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.

Potentially problematic release.


This version of gac might be problematic. Click here for more details.

Files changed (67) hide show
  1. gac/__init__.py +15 -0
  2. gac/__version__.py +3 -0
  3. gac/ai.py +109 -0
  4. gac/ai_utils.py +246 -0
  5. gac/auth_cli.py +214 -0
  6. gac/cli.py +218 -0
  7. gac/commit_executor.py +62 -0
  8. gac/config.py +125 -0
  9. gac/config_cli.py +95 -0
  10. gac/constants.py +328 -0
  11. gac/diff_cli.py +159 -0
  12. gac/errors.py +231 -0
  13. gac/git.py +372 -0
  14. gac/git_state_validator.py +184 -0
  15. gac/grouped_commit_workflow.py +423 -0
  16. gac/init_cli.py +70 -0
  17. gac/interactive_mode.py +182 -0
  18. gac/language_cli.py +377 -0
  19. gac/main.py +476 -0
  20. gac/model_cli.py +430 -0
  21. gac/oauth/__init__.py +27 -0
  22. gac/oauth/claude_code.py +464 -0
  23. gac/oauth/qwen_oauth.py +327 -0
  24. gac/oauth/token_store.py +81 -0
  25. gac/preprocess.py +511 -0
  26. gac/prompt.py +878 -0
  27. gac/prompt_builder.py +88 -0
  28. gac/providers/README.md +437 -0
  29. gac/providers/__init__.py +80 -0
  30. gac/providers/anthropic.py +17 -0
  31. gac/providers/azure_openai.py +57 -0
  32. gac/providers/base.py +329 -0
  33. gac/providers/cerebras.py +15 -0
  34. gac/providers/chutes.py +25 -0
  35. gac/providers/claude_code.py +79 -0
  36. gac/providers/custom_anthropic.py +103 -0
  37. gac/providers/custom_openai.py +44 -0
  38. gac/providers/deepseek.py +15 -0
  39. gac/providers/error_handler.py +139 -0
  40. gac/providers/fireworks.py +15 -0
  41. gac/providers/gemini.py +90 -0
  42. gac/providers/groq.py +15 -0
  43. gac/providers/kimi_coding.py +27 -0
  44. gac/providers/lmstudio.py +80 -0
  45. gac/providers/minimax.py +15 -0
  46. gac/providers/mistral.py +15 -0
  47. gac/providers/moonshot.py +15 -0
  48. gac/providers/ollama.py +73 -0
  49. gac/providers/openai.py +32 -0
  50. gac/providers/openrouter.py +21 -0
  51. gac/providers/protocol.py +71 -0
  52. gac/providers/qwen.py +64 -0
  53. gac/providers/registry.py +58 -0
  54. gac/providers/replicate.py +156 -0
  55. gac/providers/streamlake.py +31 -0
  56. gac/providers/synthetic.py +40 -0
  57. gac/providers/together.py +15 -0
  58. gac/providers/zai.py +31 -0
  59. gac/py.typed +0 -0
  60. gac/security.py +293 -0
  61. gac/utils.py +401 -0
  62. gac/workflow_utils.py +217 -0
  63. gac-3.10.3.dist-info/METADATA +283 -0
  64. gac-3.10.3.dist-info/RECORD +67 -0
  65. gac-3.10.3.dist-info/WHEEL +4 -0
  66. gac-3.10.3.dist-info/entry_points.txt +2 -0
  67. gac-3.10.3.dist-info/licenses/LICENSE +16 -0
gac/prompt.py ADDED
@@ -0,0 +1,878 @@
1
+ # flake8: noqa: E501
2
+ """Prompt creation for gac.
3
+
4
+ This module handles the creation of prompts for AI models, including template loading,
5
+ formatting, and integration with diff preprocessing.
6
+ """
7
+
8
+ import logging
9
+ import re
10
+
11
+ from gac.constants import CommitMessageConstants
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ # ============================================================================
17
+ # Prompt Templates
18
+ # ============================================================================
19
+
20
+ DEFAULT_SYSTEM_TEMPLATE = """<role>
21
+ You are an expert git commit message generator. Your task is to analyze code changes and create a concise, meaningful git commit message. You will receive git status and diff information. Your entire response will be used directly as a git commit message.
22
+ </role>
23
+
24
+ <focus>
25
+ Your commit message must reflect the core purpose and impact of these changes.
26
+ Prioritize the primary intent over implementation details.
27
+ Consider what future developers need to understand about this change.
28
+ Identify if this introduces new capabilities, fixes problems, or improves existing code.
29
+ </focus>
30
+
31
+ <mixed_changes>
32
+ When changes span multiple areas:
33
+ - Choose the commit type based on the PRIMARY purpose, not the largest file count
34
+ - Feature additions with supporting tests/docs should use 'feat'
35
+ - Bug fixes with added tests should use 'fix'
36
+ - Refactoring that improves multiple components should use 'refactor'
37
+ - Documentation updates are 'docs' only when that's the sole purpose
38
+ </mixed_changes>
39
+
40
+ <format>
41
+ <one_liner>
42
+ Create a single-line commit message.
43
+ Your message should be clear, concise, and descriptive of the core change.
44
+ Use present tense ("Add feature" not "Added feature").
45
+ </one_liner><multi_line>
46
+ Create a commit message with:
47
+ - First line: A concise summary that could stand alone
48
+ - Blank line after the summary
49
+ - Detailed body with multiple bullet points explaining the key changes
50
+ - Focus on WHY changes were made, not just WHAT was changed
51
+ - Order points from most important to least important
52
+ </multi_line><verbose>
53
+ IMPORTANT: You MUST create a MULTI-PARAGRAPH commit message with detailed sections.
54
+ DO NOT create a single-line commit message.
55
+
56
+ Your commit message MUST follow this structure:
57
+
58
+ Line 1: A concise summary (that could stand alone) with conventional commit prefix
59
+ Line 2: BLANK LINE (required)
60
+ Lines 3+: Detailed multi-paragraph body with the following sections:
61
+
62
+ ## Motivation
63
+ Explain why this commit exists in 2-3 sentences. What problem does it solve? What need does it address?
64
+
65
+ ## Architecture / Approach
66
+ Describe how it was implemented in 2-4 sentences. Include key design decisions and any rejected alternatives.
67
+ Reference specific modules, functions, or classes when relevant.
68
+
69
+ ## Affected Components
70
+ List the main modules, subsystems, or directories impacted by this change.
71
+
72
+ OPTIONAL sections (include only if relevant):
73
+
74
+ ## Performance / Security Impact
75
+ Describe any performance improvements, trade-offs, or security considerations.
76
+ Include concrete data such as benchmark results if available.
77
+
78
+ ## Compatibility / Testing
79
+ Mention any compatibility considerations, known limitations, testing performed, or next steps for validation.
80
+
81
+ REQUIREMENTS:
82
+ - Your response MUST be at least 10 lines long with multiple paragraphs
83
+ - Use active voice and present tense ("Implements", "Adds", "Refactors")
84
+ - Provide concrete, specific information rather than vague descriptions
85
+ - Keep the tone professional and technical
86
+ - Focus on intent and reasoning, not just code changes
87
+ - Use markdown headers (##) for section organization
88
+ </verbose>
89
+ </format>
90
+
91
+ <conventions_no_scope>
92
+ You MUST start your commit message with the most appropriate conventional commit prefix.
93
+
94
+ IMPORTANT: Check file types FIRST when determining the commit type:
95
+ - If changes are ONLY to documentation files (*.md, *.rst, *.txt in docs/, README*, CHANGELOG*, etc.), ALWAYS use 'docs:'
96
+ - Use 'docs:' ONLY when ALL changes are documentation files - INCLUDING README updates, regardless of how significant the changes are
97
+ - If changes include both documentation and code, use the prefix for the code changes, unless it is a documentation-only change
98
+
99
+ Commit type prefixes:
100
+ - feat: A new feature or functionality addition
101
+ - fix: A bug fix or error correction
102
+ - docs: Documentation changes only (INCLUDING README updates, regardless of how significant)
103
+ - style: Changes to code style/formatting without logic changes
104
+ - refactor: Code restructuring without behavior changes
105
+ - perf: Performance improvements
106
+ - test: Adding/modifying tests
107
+ - build: Changes to build system/dependencies
108
+ - ci: Changes to CI configuration
109
+ - chore: Miscellaneous changes not affecting src/test files
110
+
111
+ Select ONE prefix that best matches the primary purpose of the changes.
112
+ If multiple prefixes apply, choose the one that represents the most significant change.
113
+ If you cannot confidently determine a type, use 'chore'.
114
+
115
+ Do NOT include a scope in your commit prefix.
116
+ </conventions_no_scope>
117
+
118
+ <conventions_with_scope>
119
+ You MUST write a conventional commit message with EXACTLY ONE type and an inferred scope.
120
+
121
+ FORMAT: type(scope): description
122
+
123
+ IMPORTANT: Check file types FIRST when determining the commit type:
124
+ - If changes are ONLY to documentation files (*.md, *.rst, *.txt in docs/, README*, CHANGELOG*, etc.), ALWAYS use 'docs'
125
+ - If changes include both documentation and code, use the prefix for the code changes, unless it is a documentation-only change
126
+
127
+ Select ONE type from this list that best matches the primary purpose of the changes:
128
+ - feat: A new feature or functionality addition
129
+ - fix: A bug fix or error correction
130
+ - docs: Documentation changes only (INCLUDING README and CHANGELOG updates, regardless of how significant)
131
+ - style: Changes to code style/formatting without logic changes
132
+ - refactor: Code restructuring without behavior changes
133
+ - perf: Performance improvements
134
+ - test: Adding/modifying tests
135
+ - build: Changes to build system/dependencies
136
+ - ci: Changes to CI configuration
137
+ - chore: Miscellaneous changes not affecting src/test files
138
+
139
+ You MUST infer an appropriate scope from the changes. A good scope is concise (usually one word) and indicates the component or area that was changed.
140
+
141
+ <scope_rules>
142
+ For scope inference, select the most specific component affected:
143
+ - Use module/component names from the codebase (auth, api, cli, core)
144
+ - Use functional areas for cross-cutting changes (config, build, test)
145
+ - Keep scopes consistent with existing commit history when possible
146
+ - Prefer established patterns over creating new scope names
147
+ - Use singular form (auth, not auths; test, not tests)
148
+ </scope_rules>
149
+
150
+ Examples of good scopes: api, auth, ui, core, docs, build, prompt, config
151
+
152
+ CORRECT EXAMPLES (these formats are correct):
153
+ ✅ feat(auth): add login functionality
154
+ ✅ fix(api): resolve null response issue
155
+ ✅ refactor(core): improve data processing
156
+ ✅ docs(readme): update installation instructions
157
+
158
+ INCORRECT EXAMPLES (these formats are wrong and must NOT be used):
159
+ ❌ chore: feat(component): description
160
+ ❌ fix: refactor(component): description
161
+ ❌ feat: feat(component): description
162
+ ❌ chore: chore(component): description
163
+
164
+ You MUST NOT prefix the type(scope) with another type. Use EXACTLY ONE type, which MUST include the scope in parentheses.
165
+ </conventions_with_scope>
166
+
167
+ <examples_no_scope>
168
+ Good commit messages (no scope):
169
+ [OK] feat: add OAuth2 integration with Google and GitHub
170
+ [OK] fix: resolve race condition in user session management
171
+ [OK] docs: add troubleshooting section for common installation issues
172
+ [OK] refactor: extract validation logic into reusable utilities
173
+ [OK] test: add comprehensive unit tests for token validation
174
+ [OK] build: upgrade to latest security patches
175
+
176
+ Bad commit messages:
177
+ [ERROR] fix stuff
178
+ [ERROR] update code
179
+ [ERROR] feat(auth): add login (scope included when not requested)
180
+ [ERROR] WIP: still working on this
181
+ [ERROR] Fixed bug
182
+ [ERROR] Changes
183
+ </examples_no_scope>
184
+
185
+ <examples_verbose_no_scope>
186
+ Example of a good VERBOSE commit message (without scope):
187
+
188
+ feat: add verbose mode for detailed commit message generation
189
+
190
+ ## Motivation
191
+ Users need the ability to generate comprehensive commit messages that follow best practices for code review and documentation. The existing one-liner and multi-line modes don't provide sufficient structure for complex changes that require detailed explanations of motivation, architecture decisions, and impact.
192
+
193
+ ## Architecture / Approach
194
+ Adds a new --verbose/-v flag to the CLI that modifies the prompt generation in build_prompt(). When enabled, the prompt instructs the AI to generate commit messages with structured sections including Motivation, Architecture/Approach, Affected Components, and optional Performance/Testing sections. The implementation uses the existing format selection logic with verbose taking priority over one_liner and multi_line modes.
195
+
196
+ ## Affected Components
197
+ - src/gac/cli.py: Added --verbose flag and parameter passing
198
+ - src/gac/main.py: Extended main() to accept and pass verbose parameter
199
+ - src/gac/prompt.py: Added <verbose> template section with detailed instructions
200
+ - tests/test_prompt.py: Added test coverage for verbose mode
201
+
202
+ ## Compatibility / Testing
203
+ Added new test test_build_prompt_verbose_mode to verify the verbose template generation. All existing tests pass. The verbose mode is opt-in via the -v flag, maintaining backward compatibility.
204
+ </examples_verbose_no_scope>
205
+
206
+ <examples_verbose_with_scope>
207
+ Example of a good VERBOSE commit message (with scope):
208
+
209
+ feat(cli): add verbose mode for detailed commit message generation
210
+
211
+ ## Motivation
212
+ Users need the ability to generate comprehensive commit messages that follow best practices for code review and documentation. The existing one-liner and multi-line modes don't provide sufficient structure for complex changes that require detailed explanations of motivation, architecture decisions, and impact.
213
+
214
+ ## Architecture / Approach
215
+ Adds a new --verbose/-v flag to the CLI that modifies the prompt generation in build_prompt(). When enabled, the prompt instructs the AI to generate commit messages with structured sections including Motivation, Architecture/Approach, Affected Components, and optional Performance/Testing sections. The implementation uses the existing format selection logic with verbose taking priority over one_liner and multi_line modes.
216
+
217
+ ## Affected Components
218
+ - src/gac/cli.py: Added --verbose flag and parameter passing
219
+ - src/gac/main.py: Extended main() to accept and pass verbose parameter
220
+ - src/gac/prompt.py: Added <verbose> template section with detailed instructions
221
+ - tests/test_prompt.py: Added test coverage for verbose mode
222
+
223
+ ## Compatibility / Testing
224
+ Added new test test_build_prompt_verbose_mode to verify the verbose template generation. All existing tests pass. The verbose mode is opt-in via the -v flag, maintaining backward compatibility.
225
+ </examples_verbose_with_scope>
226
+
227
+ <examples_with_scope>
228
+ Good commit message top lines (with scope):
229
+ [OK] feat(auth): add OAuth2 integration with Google and GitHub
230
+ [OK] fix(api): resolve race condition in user session management
231
+ [OK] docs(readme): add troubleshooting section for common installation issues
232
+ [OK] refactor(core): extract validation logic into reusable utilities
233
+ [OK] test(auth): add comprehensive unit tests for token validation
234
+ [OK] build(deps): upgrade to latest security patches
235
+
236
+ Bad commit messages:
237
+ [ERROR] fix stuff
238
+ [ERROR] update code
239
+ [ERROR] feat: fix(auth): add login (double prefix)
240
+ [ERROR] WIP: still working on this
241
+ [ERROR] Fixed bug
242
+ [ERROR] Changes
243
+ </examples_with_scope>"""
244
+
245
+ DEFAULT_USER_TEMPLATE = """<hint>
246
+ Additional context provided by the user: <hint_text></hint_text>
247
+ </hint>
248
+
249
+ <git_diff>
250
+ <diff></diff>
251
+ </git_diff>
252
+
253
+ <git_diff_stat>
254
+ <diff_stat></diff_stat>
255
+ </git_diff_stat>
256
+
257
+ <git_status>
258
+ <status></status>
259
+ </git_status>
260
+
261
+ <language_instructions>
262
+ IMPORTANT: You MUST write the entire commit message in <language_name></language_name>.
263
+ All text in the commit message, including the summary line and body, must be in <language_name></language_name>.
264
+ <prefix_instruction></prefix_instruction>
265
+ </language_instructions>
266
+
267
+ <format_instructions>
268
+ IMMEDIATELY AFTER ANALYZING THE CHANGES, RESPOND WITH ONLY THE COMMIT MESSAGE.
269
+ DO NOT include any preamble, reasoning, explanations or anything other than the commit message itself.
270
+ DO NOT use markdown formatting, headers, or code blocks.
271
+ The entire response will be passed directly to 'git commit -m'.
272
+ </format_instructions>"""
273
+
274
+ QUESTION_GENERATION_TEMPLATE = """<role>
275
+ You are an expert code reviewer specializing in identifying missing context and intent in code changes. Your task is to analyze git diffs and generate focused questions that clarify the "why" behind the changes.
276
+ </role>
277
+
278
+ <focus>
279
+ Analyze the git diff and determine the appropriate number of questions based on change complexity. Generate 1-5 focused questions to clarify intent, motivation, and impact. Your questions should help the developer provide the essential context needed for a meaningful commit message.
280
+ </focus>
281
+
282
+ <adaptive_guidelines>
283
+ - For very small changes (single file, <10 lines): Ask 1-2 essential questions about core purpose
284
+ - For small changes (few files, <50 lines): Ask 1-3 questions covering intent and impact
285
+ - For medium changes (multiple files, <200 lines): Ask 2-4 questions covering scope, intent, and impact
286
+ - For large changes (many files or substantial modifications): Ask 3-5 questions covering all aspects
287
+ - Always prioritize questions that would most help generate an informative commit message
288
+ - Lean toward fewer questions for straightforward changes
289
+ </adaptive_guidelines>
290
+
291
+ <guidelines>
292
+ - Focus on WHY the changes were made, not just WHAT was changed
293
+ - Ask about the intent, motivation, or business purpose behind the changes
294
+ - Consider what future developers need to understand about this change
295
+ - Ask about the broader impact or consequences of the changes
296
+ - Target areas where technical implementation doesn't reveal the underlying purpose
297
+ - Keep questions concise and specific
298
+ - Format as a clean list for easy parsing
299
+ </guidelines>
300
+
301
+ <rules>
302
+ NEVER write or rewrite the commit message; only ask questions.
303
+ DO NOT suggest specific commit message formats or wording.
304
+ DO NOT ask about implementation details that are already clear from the diff.
305
+ DO NOT include any explanations or preamble with your response.
306
+ </rules>
307
+
308
+ <output_format>
309
+ Respond with ONLY a numbered list of questions, one per line:
310
+ 1. First focused question?
311
+ 2. Second focused question?
312
+ 3. Third focused question?
313
+ 4. [etc...]
314
+ </output_format>
315
+
316
+ <examples>
317
+ Good example questions for small changes:
318
+ 1. What problem does this fix?
319
+ 2. Why was this approach chosen?
320
+
321
+ Good example questions for larger changes:
322
+ 1. What problem or user need does this change address?
323
+ 2. Why was this particular approach chosen over alternatives?
324
+ 3. What impact will this have on existing functionality?
325
+ 4. What motivated the addition of these new error cases?
326
+ 5. Why are these validation rules being added now?
327
+
328
+ Bad examples (violates rules):
329
+ ❌ feat: add user authentication - This is a commit message, not a question
330
+ ❌ Should I use "feat" or "fix" for this change? - This asks about formatting, not context
331
+ ❌ Why did you rename the variable from x to y? - Too implementation-specific
332
+ ❌ You should reformat this as "fix: resolve authentication issue" - This rewrites the message
333
+ </examples>"""
334
+
335
+
336
+ # ============================================================================
337
+ # Template Loading
338
+ # ============================================================================
339
+
340
+
341
+ def load_system_template(custom_path: str | None = None) -> str:
342
+ """Load the system prompt template.
343
+
344
+ Args:
345
+ custom_path: Optional path to a custom system template file
346
+
347
+ Returns:
348
+ System template content as string
349
+ """
350
+ if custom_path:
351
+ return load_custom_system_template(custom_path)
352
+
353
+ logger.debug("Using default system template")
354
+ return DEFAULT_SYSTEM_TEMPLATE
355
+
356
+
357
+ def load_user_template() -> str:
358
+ """Load the user prompt template (contains git data sections and instructions).
359
+
360
+ Returns:
361
+ User template content as string
362
+ """
363
+ logger.debug("Using default user template")
364
+ return DEFAULT_USER_TEMPLATE
365
+
366
+
367
+ def load_custom_system_template(path: str) -> str:
368
+ """Load a custom system template from a file.
369
+
370
+ Args:
371
+ path: Path to the custom system template file
372
+
373
+ Returns:
374
+ Custom system template content
375
+
376
+ Raises:
377
+ FileNotFoundError: If the template file doesn't exist
378
+ IOError: If there's an error reading the file
379
+ """
380
+ try:
381
+ with open(path, encoding="utf-8") as f:
382
+ content = f.read()
383
+ logger.info(f"Loaded custom system template from {path}")
384
+ return content
385
+ except FileNotFoundError:
386
+ logger.error(f"Custom system template not found: {path}")
387
+ raise
388
+ except OSError as e:
389
+ logger.error(f"Error reading custom system template from {path}: {e}")
390
+ raise
391
+
392
+
393
+ # ============================================================================
394
+ # Template Processing Helpers
395
+ # ============================================================================
396
+
397
+
398
+ def _remove_template_section(template: str, section_name: str) -> str:
399
+ """Remove a tagged section from the template.
400
+
401
+ Args:
402
+ template: The template string
403
+ section_name: Name of the section to remove (without < > brackets)
404
+
405
+ Returns:
406
+ Template with the section removed
407
+ """
408
+ pattern = f"<{section_name}>.*?</{section_name}>\\n?"
409
+ return re.sub(pattern, "", template, flags=re.DOTALL)
410
+
411
+
412
+ def _select_conventions_section(template: str, infer_scope: bool) -> str:
413
+ """Select and normalize the appropriate conventions section.
414
+
415
+ Args:
416
+ template: The template string
417
+ infer_scope: Whether to infer scope for commits
418
+
419
+ Returns:
420
+ Template with the appropriate conventions section selected
421
+ """
422
+ try:
423
+ logger.debug(f"Processing infer_scope parameter: {infer_scope}")
424
+ if infer_scope:
425
+ logger.debug("Using inferred-scope conventions")
426
+ template = _remove_template_section(template, "conventions_no_scope")
427
+ template = template.replace("<conventions_with_scope>", "<conventions>")
428
+ template = template.replace("</conventions_with_scope>", "</conventions>")
429
+ else:
430
+ logger.debug("Using no-scope conventions")
431
+ template = _remove_template_section(template, "conventions_with_scope")
432
+ template = template.replace("<conventions_no_scope>", "<conventions>")
433
+ template = template.replace("</conventions_no_scope>", "</conventions>")
434
+ except Exception as e:
435
+ logger.error(f"Error processing scope parameter: {e}")
436
+ template = _remove_template_section(template, "conventions_with_scope")
437
+ template = template.replace("<conventions_no_scope>", "<conventions>")
438
+ template = template.replace("</conventions_no_scope>", "</conventions>")
439
+ return template
440
+
441
+
442
+ def _select_format_section(template: str, verbose: bool, one_liner: bool) -> str:
443
+ """Select the appropriate format section based on verbosity and one-liner settings.
444
+
445
+ Priority: verbose > one_liner > multi_line
446
+
447
+ Args:
448
+ template: The template string
449
+ verbose: Whether to use verbose format
450
+ one_liner: Whether to use one-liner format
451
+
452
+ Returns:
453
+ Template with the appropriate format section selected
454
+ """
455
+ if verbose:
456
+ template = _remove_template_section(template, "one_liner")
457
+ template = _remove_template_section(template, "multi_line")
458
+ elif one_liner:
459
+ template = _remove_template_section(template, "multi_line")
460
+ template = _remove_template_section(template, "verbose")
461
+ else:
462
+ template = _remove_template_section(template, "one_liner")
463
+ template = _remove_template_section(template, "verbose")
464
+ return template
465
+
466
+
467
+ def _select_examples_section(template: str, verbose: bool, infer_scope: bool) -> str:
468
+ """Select the appropriate examples section based on verbosity and scope settings.
469
+
470
+ Args:
471
+ template: The template string
472
+ verbose: Whether verbose mode is enabled
473
+ infer_scope: Whether scope inference is enabled
474
+
475
+ Returns:
476
+ Template with the appropriate examples section selected
477
+ """
478
+ if verbose and infer_scope:
479
+ template = _remove_template_section(template, "examples_no_scope")
480
+ template = _remove_template_section(template, "examples_with_scope")
481
+ template = _remove_template_section(template, "examples_verbose_no_scope")
482
+ template = template.replace("<examples_verbose_with_scope>", "<examples>")
483
+ template = template.replace("</examples_verbose_with_scope>", "</examples>")
484
+ elif verbose:
485
+ template = _remove_template_section(template, "examples_no_scope")
486
+ template = _remove_template_section(template, "examples_with_scope")
487
+ template = _remove_template_section(template, "examples_verbose_with_scope")
488
+ template = template.replace("<examples_verbose_no_scope>", "<examples>")
489
+ template = template.replace("</examples_verbose_no_scope>", "</examples>")
490
+ elif infer_scope:
491
+ template = _remove_template_section(template, "examples_no_scope")
492
+ template = _remove_template_section(template, "examples_verbose_no_scope")
493
+ template = _remove_template_section(template, "examples_verbose_with_scope")
494
+ template = template.replace("<examples_with_scope>", "<examples>")
495
+ template = template.replace("</examples_with_scope>", "</examples>")
496
+ else:
497
+ template = _remove_template_section(template, "examples_with_scope")
498
+ template = _remove_template_section(template, "examples_verbose_no_scope")
499
+ template = _remove_template_section(template, "examples_verbose_with_scope")
500
+ template = template.replace("<examples_no_scope>", "<examples>")
501
+ template = template.replace("</examples_no_scope>", "</examples>")
502
+ return template
503
+
504
+
505
+ # ============================================================================
506
+ # Prompt Building
507
+ # ============================================================================
508
+
509
+
510
+ def build_prompt(
511
+ status: str,
512
+ processed_diff: str,
513
+ diff_stat: str = "",
514
+ one_liner: bool = False,
515
+ infer_scope: bool = False,
516
+ hint: str = "",
517
+ verbose: bool = False,
518
+ system_template_path: str | None = None,
519
+ language: str | None = None,
520
+ translate_prefixes: bool = False,
521
+ ) -> tuple[str, str]:
522
+ """Build system and user prompts for the AI model using the provided templates and git information.
523
+
524
+ Args:
525
+ status: Git status output
526
+ processed_diff: Git diff output, already preprocessed and ready to use
527
+ diff_stat: Git diff stat output showing file changes summary
528
+ one_liner: Whether to request a one-line commit message
529
+ infer_scope: Whether to infer scope for the commit message
530
+ hint: Optional hint to guide the AI
531
+ verbose: Whether to generate detailed commit messages with motivation, architecture, and impact sections
532
+ system_template_path: Optional path to custom system template
533
+ language: Optional language for commit messages (e.g., "Spanish", "French", "Japanese")
534
+ translate_prefixes: Whether to translate conventional commit prefixes (default: False keeps them in English)
535
+
536
+ Returns:
537
+ Tuple of (system_prompt, user_prompt) ready to be sent to an AI model
538
+ """
539
+ system_template = load_system_template(system_template_path)
540
+ user_template = load_user_template()
541
+
542
+ system_template = _select_conventions_section(system_template, infer_scope)
543
+ system_template = _select_format_section(system_template, verbose, one_liner)
544
+ system_template = _select_examples_section(system_template, verbose, infer_scope)
545
+ system_template = re.sub(r"\n(?:[ \t]*\n){2,}", "\n\n", system_template)
546
+
547
+ user_template = user_template.replace("<status></status>", status)
548
+ user_template = user_template.replace("<diff_stat></diff_stat>", diff_stat)
549
+ user_template = user_template.replace("<diff></diff>", processed_diff)
550
+
551
+ if hint:
552
+ user_template = user_template.replace("<hint_text></hint_text>", hint)
553
+ logger.debug(f"Added hint ({len(hint)} characters)")
554
+ else:
555
+ user_template = _remove_template_section(user_template, "hint")
556
+ logger.debug("No hint provided")
557
+
558
+ if language:
559
+ user_template = user_template.replace("<language_name></language_name>", language)
560
+
561
+ # Set prefix instruction based on translate_prefixes setting
562
+ if translate_prefixes:
563
+ prefix_instruction = f"""CRITICAL: You MUST translate the conventional commit prefix into {language}.
564
+ DO NOT use English prefixes like 'feat:', 'fix:', 'docs:', etc.
565
+ Instead, translate them into {language} equivalents.
566
+ Examples:
567
+ - 'feat:' → translate to {language} word for 'feature' or 'add'
568
+ - 'fix:' → translate to {language} word for 'fix' or 'correct'
569
+ - 'docs:' → translate to {language} word for 'documentation'
570
+ The ENTIRE commit message, including the prefix, must be in {language}."""
571
+ logger.debug(f"Set commit message language to: {language} (with prefix translation)")
572
+ else:
573
+ prefix_instruction = (
574
+ "The conventional commit prefix (feat:, fix:, etc.) should remain in English, but everything after the prefix must be in "
575
+ + language
576
+ + "."
577
+ )
578
+ logger.debug(f"Set commit message language to: {language} (English prefixes)")
579
+
580
+ user_template = user_template.replace("<prefix_instruction></prefix_instruction>", prefix_instruction)
581
+ else:
582
+ user_template = _remove_template_section(user_template, "language_instructions")
583
+ logger.debug("Using default language (English)")
584
+
585
+ user_template = re.sub(r"\n(?:[ \t]*\n){2,}", "\n\n", user_template)
586
+
587
+ return system_template.strip(), user_template.strip()
588
+
589
+
590
+ def build_group_prompt(
591
+ status: str,
592
+ processed_diff: str,
593
+ diff_stat: str,
594
+ one_liner: bool,
595
+ hint: str,
596
+ infer_scope: bool,
597
+ verbose: bool,
598
+ system_template_path: str | None,
599
+ language: str | None,
600
+ translate_prefixes: bool,
601
+ ) -> tuple[str, str]:
602
+ """Build prompt for grouped commit generation (JSON output with multiple commits)."""
603
+ system_prompt, user_prompt = build_prompt(
604
+ status=status,
605
+ processed_diff=processed_diff,
606
+ diff_stat=diff_stat,
607
+ one_liner=one_liner,
608
+ hint=hint,
609
+ infer_scope=infer_scope,
610
+ verbose=verbose,
611
+ system_template_path=system_template_path,
612
+ language=language,
613
+ translate_prefixes=translate_prefixes,
614
+ )
615
+
616
+ user_prompt = _remove_template_section(user_prompt, "format_instructions")
617
+
618
+ grouping_instructions = """
619
+ <format_instructions>
620
+ Your task is to split the changed files into separate, logical commits. Think of this like sorting files into different folders where each file belongs in exactly one folder.
621
+
622
+ CRITICAL REQUIREMENT - Every File Used Exactly Once:
623
+ You must assign EVERY file from the diff to exactly ONE commit.
624
+ - NO file should be left out
625
+ - NO file should appear in multiple commits
626
+ - EVERY file must be used once and ONLY once
627
+
628
+ Think of it like dealing cards: Once you've dealt a card to a player, that card cannot be dealt to another player.
629
+
630
+ HOW TO SPLIT THE FILES:
631
+ 1. Review all changed files in the diff
632
+ 2. Group files by logical relationship (e.g., related features, bug fixes, documentation)
633
+ 3. Assign each file to exactly one commit based on what makes the most sense
634
+ 4. If a file could fit in multiple commits, pick the best fit and move on - do NOT duplicate it
635
+ 5. Continue until every single file has been assigned to a commit
636
+
637
+ ORDERING:
638
+ Order the commits in a logical sequence considering dependencies, natural progression, and overall workflow.
639
+
640
+ YOUR RESPONSE FORMAT:
641
+ Respond with valid JSON following this structure:
642
+ ```json
643
+ {
644
+ "commits": [
645
+ {
646
+ "files": ["src/auth/login.ts", "src/auth/logout.ts"],
647
+ "message": "<commit_message_conforming_to_prescribed_structure_and_format>"
648
+ },
649
+ {
650
+ "files": ["src/db/schema.sql", "src/db/migrations/001.sql"],
651
+ "message": "<commit_message_conforming_to_prescribed_structure_and_format>"
652
+ },
653
+ {
654
+ "files": ["tests/auth.test.ts", "tests/db.test.ts", "README.md"],
655
+ "message": "<commit_message_conforming_to_prescribed_structure_and_format>"
656
+ }
657
+ ]
658
+ }
659
+ ```
660
+
661
+ ☝️ Notice how EVERY file path in the example above appears exactly ONCE across all commits. "src/auth/login.ts" appears once. "tests/auth.test.ts" appears once. No file is repeated.
662
+
663
+ VALIDATION CHECKLIST - Before responding, verify:
664
+ □ Total files across all commits = Total files in the diff
665
+ □ Each file appears in exactly 1 commit (no duplicates, no omissions)
666
+ □ Every commit has at least one file
667
+ □ If you list all files from all commits and count them, you get the same count as unique files in the diff
668
+ </format_instructions>
669
+ """
670
+
671
+ user_prompt = user_prompt + grouping_instructions
672
+
673
+ return system_prompt, user_prompt
674
+
675
+
676
+ def build_question_generation_prompt(
677
+ status: str,
678
+ processed_diff: str,
679
+ diff_stat: str = "",
680
+ hint: str = "",
681
+ ) -> tuple[str, str]:
682
+ """Build system and user prompts for question generation about staged changes.
683
+
684
+ Args:
685
+ status: Git status output
686
+ processed_diff: Git diff output, already preprocessed and ready to use
687
+ diff_stat: Git diff stat output showing file changes summary
688
+ hint: Optional hint to guide the question generation
689
+
690
+ Returns:
691
+ Tuple of (system_prompt, user_prompt) ready to be sent to an AI model
692
+ """
693
+ system_prompt = QUESTION_GENERATION_TEMPLATE
694
+
695
+ # Build user prompt with git context
696
+ user_prompt = f"""<git_diff>
697
+ {processed_diff}
698
+ </git_diff>
699
+
700
+ <git_diff_stat>
701
+ {diff_stat}
702
+ </git_diff_stat>
703
+
704
+ <git_status>
705
+ {status}
706
+ </git_status>"""
707
+
708
+ if hint:
709
+ user_prompt = f"""<hint>
710
+ Additional context provided by the user: {hint}
711
+ </hint>
712
+
713
+ {user_prompt}"""
714
+
715
+ # Add instruction to ask questions in the appropriate language if specified
716
+ user_prompt += """
717
+
718
+ <format_instructions>
719
+ Analyze the changes above and determine the appropriate number of questions based on the change complexity. Generate 1-5 focused questions that clarify the intent, motivation, and impact of these changes. For very small changes, ask only 1-2 essential questions. Respond with ONLY a numbered list of questions as specified in the system prompt.
720
+ </format_instructions>"""
721
+
722
+ return system_prompt.strip(), user_prompt.strip()
723
+
724
+
725
+ # ============================================================================
726
+ # Message Cleaning Helpers
727
+ # ============================================================================
728
+
729
+
730
+ def _remove_think_tags(message: str) -> str:
731
+ """Remove AI reasoning <think> tags and their content from the message.
732
+
733
+ Args:
734
+ message: The message to clean
735
+
736
+ Returns:
737
+ Message with <think> tags removed
738
+ """
739
+ while re.search(r"<think>(?:(?!</think>)[^\n])*\n.*?</think>", message, flags=re.DOTALL | re.IGNORECASE):
740
+ message = re.sub(
741
+ r"<think>(?:(?!</think>)[^\n])*\n.*?</think>\s*", "", message, flags=re.DOTALL | re.IGNORECASE, count=1
742
+ )
743
+
744
+ message = re.sub(r"\n\n+\s*<think>.*?</think>\s*", "", message, flags=re.DOTALL | re.IGNORECASE)
745
+ message = re.sub(r"<think>.*?</think>\s*\n\n+", "", message, flags=re.DOTALL | re.IGNORECASE)
746
+
747
+ message = re.sub(r"<think>\s*\n.*$", "", message, flags=re.DOTALL | re.IGNORECASE)
748
+
749
+ conventional_prefixes_pattern = r"(" + "|".join(CommitMessageConstants.CONVENTIONAL_PREFIXES) + r")[\(:)]"
750
+ if re.search(r"^.*?</think>", message, flags=re.DOTALL | re.IGNORECASE):
751
+ prefix_match = re.search(conventional_prefixes_pattern, message, flags=re.IGNORECASE)
752
+ think_match = re.search(r"</think>", message, flags=re.IGNORECASE)
753
+
754
+ if not prefix_match or (think_match and think_match.start() < prefix_match.start()):
755
+ message = re.sub(r"^.*?</think>\s*", "", message, flags=re.DOTALL | re.IGNORECASE)
756
+
757
+ message = re.sub(r"</think>\s*$", "", message, flags=re.IGNORECASE)
758
+
759
+ return message
760
+
761
+
762
+ def _remove_code_blocks(message: str) -> str:
763
+ """Remove markdown code blocks from the message.
764
+
765
+ Args:
766
+ message: The message to clean
767
+
768
+ Returns:
769
+ Message with code blocks removed
770
+ """
771
+ return re.sub(r"```[\w]*\n|```", "", message)
772
+
773
+
774
+ def _extract_commit_from_reasoning(message: str) -> str:
775
+ """Extract the actual commit message from reasoning/preamble text.
776
+
777
+ Args:
778
+ message: The message potentially containing reasoning
779
+
780
+ Returns:
781
+ Extracted commit message
782
+ """
783
+ for indicator in CommitMessageConstants.COMMIT_INDICATORS:
784
+ if indicator.lower() in message.lower():
785
+ message = message.split(indicator, 1)[1].strip()
786
+ break
787
+
788
+ lines = message.split("\n")
789
+ for i, line in enumerate(lines):
790
+ if any(line.strip().startswith(f"{prefix}:") for prefix in CommitMessageConstants.CONVENTIONAL_PREFIXES):
791
+ message = "\n".join(lines[i:])
792
+ break
793
+
794
+ return message
795
+
796
+
797
+ def _remove_xml_tags(message: str) -> str:
798
+ """Remove XML tags that might have leaked into the message.
799
+
800
+ Args:
801
+ message: The message to clean
802
+
803
+ Returns:
804
+ Message with XML tags removed
805
+ """
806
+ for tag in CommitMessageConstants.XML_TAGS_TO_REMOVE:
807
+ message = message.replace(tag, "")
808
+ return message
809
+
810
+
811
+ def _fix_double_prefix(message: str) -> str:
812
+ """Fix double type prefix issues like 'chore: feat(scope):' to 'feat(scope):'.
813
+
814
+ Args:
815
+ message: The message to fix
816
+
817
+ Returns:
818
+ Message with double prefix corrected
819
+ """
820
+ double_prefix_pattern = re.compile(
821
+ r"^("
822
+ + r"|\s*".join(CommitMessageConstants.CONVENTIONAL_PREFIXES)
823
+ + r"):\s*("
824
+ + r"|\s*".join(CommitMessageConstants.CONVENTIONAL_PREFIXES)
825
+ + r")\(([^)]+)\):"
826
+ )
827
+ match = double_prefix_pattern.match(message)
828
+
829
+ if match:
830
+ second_type = match.group(2)
831
+ scope = match.group(3)
832
+ description = message[match.end() :].strip()
833
+ message = f"{second_type}({scope}): {description}"
834
+
835
+ return message
836
+
837
+
838
+ def _normalize_whitespace(message: str) -> str:
839
+ """Normalize whitespace, ensuring no more than one blank line between paragraphs.
840
+
841
+ Args:
842
+ message: The message to normalize
843
+
844
+ Returns:
845
+ Message with normalized whitespace
846
+ """
847
+ return re.sub(r"\n(?:[ \t]*\n){2,}", "\n\n", message).strip()
848
+
849
+
850
+ # ============================================================================
851
+ # Message Cleaning
852
+ # ============================================================================
853
+
854
+
855
+ def clean_commit_message(message: str) -> str:
856
+ """Clean up a commit message generated by an AI model.
857
+
858
+ This function:
859
+ 1. Removes any preamble or reasoning text
860
+ 2. Removes code block markers and formatting
861
+ 3. Removes XML tags that might have leaked into the response
862
+ 4. Fixes double type prefix issues (e.g., "chore: feat(scope):")
863
+ 5. Normalizes whitespace
864
+
865
+ Args:
866
+ message: Raw commit message from AI
867
+
868
+ Returns:
869
+ Cleaned commit message ready for use
870
+ """
871
+ message = message.strip()
872
+ message = _remove_think_tags(message)
873
+ message = _remove_code_blocks(message)
874
+ message = _extract_commit_from_reasoning(message)
875
+ message = _remove_xml_tags(message)
876
+ message = _fix_double_prefix(message)
877
+ message = _normalize_whitespace(message)
878
+ return message