gac 1.13.0__py3-none-any.whl → 3.8.1__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.
- gac/__version__.py +1 -1
- gac/ai.py +33 -47
- gac/ai_utils.py +113 -41
- gac/auth_cli.py +214 -0
- gac/cli.py +72 -2
- gac/config.py +63 -6
- gac/config_cli.py +26 -5
- gac/constants.py +178 -2
- gac/git.py +158 -12
- gac/init_cli.py +40 -125
- gac/language_cli.py +378 -0
- gac/main.py +868 -158
- gac/model_cli.py +429 -0
- gac/oauth/__init__.py +27 -0
- gac/oauth/claude_code.py +464 -0
- gac/oauth/qwen_oauth.py +323 -0
- gac/oauth/token_store.py +81 -0
- gac/preprocess.py +3 -3
- gac/prompt.py +573 -226
- gac/providers/__init__.py +49 -0
- gac/providers/anthropic.py +11 -1
- gac/providers/azure_openai.py +101 -0
- gac/providers/cerebras.py +11 -1
- gac/providers/chutes.py +11 -1
- gac/providers/claude_code.py +112 -0
- gac/providers/custom_anthropic.py +6 -2
- gac/providers/custom_openai.py +6 -3
- gac/providers/deepseek.py +11 -1
- gac/providers/fireworks.py +11 -1
- gac/providers/gemini.py +11 -1
- gac/providers/groq.py +5 -1
- gac/providers/kimi_coding.py +67 -0
- gac/providers/lmstudio.py +12 -1
- gac/providers/minimax.py +11 -1
- gac/providers/mistral.py +48 -0
- gac/providers/moonshot.py +48 -0
- gac/providers/ollama.py +11 -1
- gac/providers/openai.py +11 -1
- gac/providers/openrouter.py +11 -1
- gac/providers/qwen.py +76 -0
- gac/providers/replicate.py +110 -0
- gac/providers/streamlake.py +11 -1
- gac/providers/synthetic.py +11 -1
- gac/providers/together.py +11 -1
- gac/providers/zai.py +11 -1
- gac/security.py +1 -1
- gac/utils.py +272 -4
- gac/workflow_utils.py +217 -0
- {gac-1.13.0.dist-info → gac-3.8.1.dist-info}/METADATA +90 -27
- gac-3.8.1.dist-info/RECORD +56 -0
- {gac-1.13.0.dist-info → gac-3.8.1.dist-info}/WHEEL +1 -1
- gac-1.13.0.dist-info/RECORD +0 -41
- {gac-1.13.0.dist-info → gac-3.8.1.dist-info}/entry_points.txt +0 -0
- {gac-1.13.0.dist-info → gac-3.8.1.dist-info}/licenses/LICENSE +0 -0
gac/prompt.py
CHANGED
|
@@ -8,10 +8,16 @@ formatting, and integration with diff preprocessing.
|
|
|
8
8
|
import logging
|
|
9
9
|
import re
|
|
10
10
|
|
|
11
|
+
from gac.constants import CommitMessageConstants
|
|
12
|
+
|
|
11
13
|
logger = logging.getLogger(__name__)
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
# ============================================================================
|
|
17
|
+
# Prompt Templates
|
|
18
|
+
# ============================================================================
|
|
19
|
+
|
|
20
|
+
DEFAULT_SYSTEM_TEMPLATE = """<role>
|
|
15
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.
|
|
16
22
|
</role>
|
|
17
23
|
|
|
@@ -33,12 +39,12 @@ When changes span multiple areas:
|
|
|
33
39
|
|
|
34
40
|
<format>
|
|
35
41
|
<one_liner>
|
|
36
|
-
Create a single-line commit message
|
|
42
|
+
Create a single-line commit message.
|
|
37
43
|
Your message should be clear, concise, and descriptive of the core change.
|
|
38
44
|
Use present tense ("Add feature" not "Added feature").
|
|
39
45
|
</one_liner><multi_line>
|
|
40
46
|
Create a commit message with:
|
|
41
|
-
- First line: A concise summary
|
|
47
|
+
- First line: A concise summary that could stand alone
|
|
42
48
|
- Blank line after the summary
|
|
43
49
|
- Detailed body with multiple bullet points explaining the key changes
|
|
44
50
|
- Focus on WHY changes were made, not just WHAT was changed
|
|
@@ -49,7 +55,7 @@ When changes span multiple areas:
|
|
|
49
55
|
|
|
50
56
|
Your commit message MUST follow this structure:
|
|
51
57
|
|
|
52
|
-
Line 1: A concise summary (
|
|
58
|
+
Line 1: A concise summary (that could stand alone) with conventional commit prefix
|
|
53
59
|
Line 2: BLANK LINE (required)
|
|
54
60
|
Lines 3+: Detailed multi-paragraph body with the following sections:
|
|
55
61
|
|
|
@@ -158,24 +164,6 @@ INCORRECT EXAMPLES (these formats are wrong and must NOT be used):
|
|
|
158
164
|
You MUST NOT prefix the type(scope) with another type. Use EXACTLY ONE type, which MUST include the scope in parentheses.
|
|
159
165
|
</conventions_with_scope>
|
|
160
166
|
|
|
161
|
-
<hint>
|
|
162
|
-
Additional context provided by the user: <hint_text></hint_text>
|
|
163
|
-
</hint>
|
|
164
|
-
|
|
165
|
-
<git_status>
|
|
166
|
-
<status></status>
|
|
167
|
-
</git_status>
|
|
168
|
-
|
|
169
|
-
<git_diff_stat>
|
|
170
|
-
<diff_stat></diff_stat>
|
|
171
|
-
</git_diff_stat>
|
|
172
|
-
|
|
173
|
-
<git_diff>
|
|
174
|
-
<diff></diff>
|
|
175
|
-
</git_diff>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
167
|
<examples_no_scope>
|
|
180
168
|
Good commit messages (no scope):
|
|
181
169
|
[OK] feat: add OAuth2 integration with Google and GitHub
|
|
@@ -252,297 +240,656 @@ Bad commit messages:
|
|
|
252
240
|
[ERROR] WIP: still working on this
|
|
253
241
|
[ERROR] Fixed bug
|
|
254
242
|
[ERROR] Changes
|
|
255
|
-
</examples_with_scope>
|
|
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
256
|
|
|
257
|
-
<
|
|
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>
|
|
258
268
|
IMMEDIATELY AFTER ANALYZING THE CHANGES, RESPOND WITH ONLY THE COMMIT MESSAGE.
|
|
259
269
|
DO NOT include any preamble, reasoning, explanations or anything other than the commit message itself.
|
|
260
270
|
DO NOT use markdown formatting, headers, or code blocks.
|
|
261
271
|
The entire response will be passed directly to 'git commit -m'.
|
|
262
|
-
</
|
|
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>
|
|
263
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.
|
|
264
343
|
|
|
265
|
-
|
|
266
|
-
|
|
344
|
+
Args:
|
|
345
|
+
custom_path: Optional path to a custom system template file
|
|
267
346
|
|
|
268
347
|
Returns:
|
|
269
|
-
|
|
348
|
+
System template content as string
|
|
270
349
|
"""
|
|
271
|
-
|
|
272
|
-
|
|
350
|
+
if custom_path:
|
|
351
|
+
return load_custom_system_template(custom_path)
|
|
273
352
|
|
|
353
|
+
logger.debug("Using default system template")
|
|
354
|
+
return DEFAULT_SYSTEM_TEMPLATE
|
|
274
355
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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.
|
|
285
369
|
|
|
286
370
|
Args:
|
|
287
|
-
|
|
288
|
-
processed_diff: Git diff output, already preprocessed and ready to use
|
|
289
|
-
diff_stat: Git diff stat output showing file changes summary
|
|
290
|
-
one_liner: Whether to request a one-line commit message
|
|
291
|
-
infer_scope: Whether to infer scope for the commit message
|
|
292
|
-
hint: Optional hint to guide the AI
|
|
293
|
-
verbose: Whether to generate detailed commit messages with motivation, architecture, and impact sections
|
|
371
|
+
path: Path to the custom system template file
|
|
294
372
|
|
|
295
373
|
Returns:
|
|
296
|
-
|
|
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
|
|
297
407
|
"""
|
|
298
|
-
|
|
408
|
+
pattern = f"<{section_name}>.*?</{section_name}>\\n?"
|
|
409
|
+
return re.sub(pattern, "", template, flags=re.DOTALL)
|
|
410
|
+
|
|
299
411
|
|
|
300
|
-
|
|
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
|
+
"""
|
|
301
422
|
try:
|
|
302
423
|
logger.debug(f"Processing infer_scope parameter: {infer_scope}")
|
|
303
424
|
if infer_scope:
|
|
304
|
-
# User wants to infer a scope from changes (any value other than None)
|
|
305
425
|
logger.debug("Using inferred-scope conventions")
|
|
306
|
-
template =
|
|
426
|
+
template = _remove_template_section(template, "conventions_no_scope")
|
|
307
427
|
template = template.replace("<conventions_with_scope>", "<conventions>")
|
|
308
428
|
template = template.replace("</conventions_with_scope>", "</conventions>")
|
|
309
429
|
else:
|
|
310
|
-
# No scope - use the plain conventions section
|
|
311
430
|
logger.debug("Using no-scope conventions")
|
|
312
|
-
template =
|
|
431
|
+
template = _remove_template_section(template, "conventions_with_scope")
|
|
313
432
|
template = template.replace("<conventions_no_scope>", "<conventions>")
|
|
314
433
|
template = template.replace("</conventions_no_scope>", "</conventions>")
|
|
315
434
|
except Exception as e:
|
|
316
435
|
logger.error(f"Error processing scope parameter: {e}")
|
|
317
|
-
|
|
318
|
-
template = re.sub(r"<conventions_with_scope>.*?</conventions_with_scope>\n", "", template, flags=re.DOTALL)
|
|
436
|
+
template = _remove_template_section(template, "conventions_with_scope")
|
|
319
437
|
template = template.replace("<conventions_no_scope>", "<conventions>")
|
|
320
438
|
template = template.replace("</conventions_no_scope>", "</conventions>")
|
|
439
|
+
return template
|
|
321
440
|
|
|
322
|
-
template = template.replace("<status></status>", status)
|
|
323
|
-
template = template.replace("<diff_stat></diff_stat>", diff_stat)
|
|
324
|
-
template = template.replace("<diff></diff>", processed_diff)
|
|
325
441
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
333
451
|
|
|
334
|
-
|
|
335
|
-
|
|
452
|
+
Returns:
|
|
453
|
+
Template with the appropriate format section selected
|
|
454
|
+
"""
|
|
336
455
|
if verbose:
|
|
337
|
-
|
|
338
|
-
template =
|
|
339
|
-
template = re.sub(r"<multi_line>.*?</multi_line>", "", template, flags=re.DOTALL)
|
|
456
|
+
template = _remove_template_section(template, "one_liner")
|
|
457
|
+
template = _remove_template_section(template, "multi_line")
|
|
340
458
|
elif one_liner:
|
|
341
|
-
|
|
342
|
-
template =
|
|
343
|
-
template = re.sub(r"<verbose>.*?</verbose>", "", template, flags=re.DOTALL)
|
|
459
|
+
template = _remove_template_section(template, "multi_line")
|
|
460
|
+
template = _remove_template_section(template, "verbose")
|
|
344
461
|
else:
|
|
345
|
-
|
|
346
|
-
template =
|
|
347
|
-
|
|
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.
|
|
348
469
|
|
|
349
|
-
|
|
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
|
+
"""
|
|
350
478
|
if verbose and infer_scope:
|
|
351
|
-
|
|
352
|
-
template =
|
|
353
|
-
template =
|
|
354
|
-
template = re.sub(
|
|
355
|
-
r"<examples_verbose_no_scope>.*?</examples_verbose_no_scope>\n?", "", template, flags=re.DOTALL
|
|
356
|
-
)
|
|
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")
|
|
357
482
|
template = template.replace("<examples_verbose_with_scope>", "<examples>")
|
|
358
483
|
template = template.replace("</examples_verbose_with_scope>", "</examples>")
|
|
359
484
|
elif verbose:
|
|
360
|
-
|
|
361
|
-
template =
|
|
362
|
-
template =
|
|
363
|
-
template = re.sub(
|
|
364
|
-
r"<examples_verbose_with_scope>.*?</examples_verbose_with_scope>\n?", "", template, flags=re.DOTALL
|
|
365
|
-
)
|
|
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")
|
|
366
488
|
template = template.replace("<examples_verbose_no_scope>", "<examples>")
|
|
367
489
|
template = template.replace("</examples_verbose_no_scope>", "</examples>")
|
|
368
490
|
elif infer_scope:
|
|
369
|
-
|
|
370
|
-
template =
|
|
371
|
-
template =
|
|
372
|
-
r"<examples_verbose_no_scope>.*?</examples_verbose_no_scope>\n?", "", template, flags=re.DOTALL
|
|
373
|
-
)
|
|
374
|
-
template = re.sub(
|
|
375
|
-
r"<examples_verbose_with_scope>.*?</examples_verbose_with_scope>\n?", "", template, flags=re.DOTALL
|
|
376
|
-
)
|
|
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")
|
|
377
494
|
template = template.replace("<examples_with_scope>", "<examples>")
|
|
378
495
|
template = template.replace("</examples_with_scope>", "</examples>")
|
|
379
496
|
else:
|
|
380
|
-
|
|
381
|
-
template =
|
|
382
|
-
template =
|
|
383
|
-
r"<examples_verbose_no_scope>.*?</examples_verbose_no_scope>\n?", "", template, flags=re.DOTALL
|
|
384
|
-
)
|
|
385
|
-
template = re.sub(
|
|
386
|
-
r"<examples_verbose_with_scope>.*?</examples_verbose_with_scope>\n?", "", template, flags=re.DOTALL
|
|
387
|
-
)
|
|
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")
|
|
388
500
|
template = template.replace("<examples_no_scope>", "<examples>")
|
|
389
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()
|
|
390
541
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
435
672
|
|
|
436
673
|
return system_prompt, user_prompt
|
|
437
674
|
|
|
438
675
|
|
|
439
|
-
def
|
|
440
|
-
|
|
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.
|
|
441
683
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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.
|
|
448
732
|
|
|
449
733
|
Args:
|
|
450
|
-
message:
|
|
734
|
+
message: The message to clean
|
|
451
735
|
|
|
452
736
|
Returns:
|
|
453
|
-
|
|
737
|
+
Message with <think> tags removed
|
|
454
738
|
"""
|
|
455
|
-
message =
|
|
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)
|
|
456
756
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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:
|
|
473
784
|
if indicator.lower() in message.lower():
|
|
474
|
-
# Extract everything after the indicator
|
|
475
785
|
message = message.split(indicator, 1)[1].strip()
|
|
476
786
|
break
|
|
477
787
|
|
|
478
|
-
# If message starts with any kind of explanation text, try to locate a conventional prefix
|
|
479
788
|
lines = message.split("\n")
|
|
480
789
|
for i, line in enumerate(lines):
|
|
481
|
-
if any(
|
|
482
|
-
line.strip().startswith(prefix)
|
|
483
|
-
for prefix in ["feat:", "fix:", "docs:", "style:", "refactor:", "perf:", "test:", "build:", "ci:", "chore:"]
|
|
484
|
-
):
|
|
790
|
+
if any(line.strip().startswith(f"{prefix}:") for prefix in CommitMessageConstants.CONVENTIONAL_PREFIXES):
|
|
485
791
|
message = "\n".join(lines[i:])
|
|
486
792
|
break
|
|
487
793
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
"</instructions>",
|
|
502
|
-
"<format>",
|
|
503
|
-
"</format>",
|
|
504
|
-
"<conventions>",
|
|
505
|
-
"</conventions>",
|
|
506
|
-
]:
|
|
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:
|
|
507
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
|
|
508
816
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
"fix",
|
|
513
|
-
"docs",
|
|
514
|
-
"style",
|
|
515
|
-
"refactor",
|
|
516
|
-
"perf",
|
|
517
|
-
"test",
|
|
518
|
-
"build",
|
|
519
|
-
"ci",
|
|
520
|
-
"chore",
|
|
521
|
-
]
|
|
522
|
-
|
|
523
|
-
# Look for double prefix pattern like "chore: feat(scope):" and fix it
|
|
524
|
-
# This regex looks for a conventional prefix followed by another conventional prefix with a scope
|
|
817
|
+
Returns:
|
|
818
|
+
Message with double prefix corrected
|
|
819
|
+
"""
|
|
525
820
|
double_prefix_pattern = re.compile(
|
|
526
|
-
r"^("
|
|
821
|
+
r"^("
|
|
822
|
+
+ r"|\s*".join(CommitMessageConstants.CONVENTIONAL_PREFIXES)
|
|
823
|
+
+ r"):\s*("
|
|
824
|
+
+ r"|\s*".join(CommitMessageConstants.CONVENTIONAL_PREFIXES)
|
|
825
|
+
+ r")\(([^)]+)\):"
|
|
527
826
|
)
|
|
528
827
|
match = double_prefix_pattern.match(message)
|
|
529
828
|
|
|
530
829
|
if match:
|
|
531
|
-
# Extract the second type and scope, which is what we want to keep
|
|
532
830
|
second_type = match.group(2)
|
|
533
831
|
scope = match.group(3)
|
|
534
832
|
description = message[match.end() :].strip()
|
|
535
833
|
message = f"{second_type}({scope}): {description}"
|
|
536
834
|
|
|
537
|
-
|
|
835
|
+
return message
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _ensure_conventional_prefix(message: str) -> str:
|
|
839
|
+
"""Ensure the message starts with a conventional commit prefix.
|
|
840
|
+
|
|
841
|
+
Args:
|
|
842
|
+
message: The message to check
|
|
843
|
+
|
|
844
|
+
Returns:
|
|
845
|
+
Message with conventional prefix ensured
|
|
846
|
+
"""
|
|
538
847
|
if not any(
|
|
539
848
|
message.strip().startswith(prefix + ":") or message.strip().startswith(prefix + "(")
|
|
540
|
-
for prefix in
|
|
849
|
+
for prefix in CommitMessageConstants.CONVENTIONAL_PREFIXES
|
|
541
850
|
):
|
|
542
851
|
message = f"chore: {message.strip()}"
|
|
852
|
+
return message
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def _normalize_whitespace(message: str) -> str:
|
|
856
|
+
"""Normalize whitespace, ensuring no more than one blank line between paragraphs.
|
|
857
|
+
|
|
858
|
+
Args:
|
|
859
|
+
message: The message to normalize
|
|
860
|
+
|
|
861
|
+
Returns:
|
|
862
|
+
Message with normalized whitespace
|
|
863
|
+
"""
|
|
864
|
+
return re.sub(r"\n(?:[ \t]*\n){2,}", "\n\n", message).strip()
|
|
865
|
+
|
|
543
866
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
867
|
+
# ============================================================================
|
|
868
|
+
# Message Cleaning
|
|
869
|
+
# ============================================================================
|
|
547
870
|
|
|
871
|
+
|
|
872
|
+
def clean_commit_message(message: str) -> str:
|
|
873
|
+
"""Clean up a commit message generated by an AI model.
|
|
874
|
+
|
|
875
|
+
This function:
|
|
876
|
+
1. Removes any preamble or reasoning text
|
|
877
|
+
2. Removes code block markers and formatting
|
|
878
|
+
3. Removes XML tags that might have leaked into the response
|
|
879
|
+
4. Fixes double type prefix issues (e.g., "chore: feat(scope):")
|
|
880
|
+
5. Normalizes whitespace
|
|
881
|
+
|
|
882
|
+
Args:
|
|
883
|
+
message: Raw commit message from AI
|
|
884
|
+
|
|
885
|
+
Returns:
|
|
886
|
+
Cleaned commit message ready for use
|
|
887
|
+
"""
|
|
888
|
+
message = message.strip()
|
|
889
|
+
message = _remove_think_tags(message)
|
|
890
|
+
message = _remove_code_blocks(message)
|
|
891
|
+
message = _extract_commit_from_reasoning(message)
|
|
892
|
+
message = _remove_xml_tags(message)
|
|
893
|
+
message = _fix_double_prefix(message)
|
|
894
|
+
message = _normalize_whitespace(message)
|
|
548
895
|
return message
|