ace-git-commit 0.23.0
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.
- checksums.yaml +7 -0
- data/.ace-defaults/git/commit.yml +22 -0
- data/.ace-defaults/nav/protocols/guide-sources/ace-git-commit.yml +10 -0
- data/.ace-defaults/nav/protocols/prompt-sources/ace-git-commit.yml +19 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-git-commit.yml +19 -0
- data/CHANGELOG.md +404 -0
- data/COMPARISON.md +176 -0
- data/LICENSE +21 -0
- data/README.md +44 -0
- data/Rakefile +14 -0
- data/exe/ace-git-commit +13 -0
- data/handbook/guides/version-control-system-message.g.md +507 -0
- data/handbook/prompts/git-commit.md +22 -0
- data/handbook/prompts/git-commit.system.md +150 -0
- data/handbook/skills/as-git-commit/SKILL.md +57 -0
- data/handbook/workflow-instructions/git/commit.wf.md +75 -0
- data/lib/ace/git_commit/atoms/git_executor.rb +62 -0
- data/lib/ace/git_commit/atoms/gitignore_checker.rb +118 -0
- data/lib/ace/git_commit/cli/commands/commit.rb +147 -0
- data/lib/ace/git_commit/cli.rb +23 -0
- data/lib/ace/git_commit/models/commit_group.rb +53 -0
- data/lib/ace/git_commit/models/commit_options.rb +75 -0
- data/lib/ace/git_commit/models/split_commit_result.rb +60 -0
- data/lib/ace/git_commit/models/stage_result.rb +71 -0
- data/lib/ace/git_commit/molecules/commit_grouper.rb +123 -0
- data/lib/ace/git_commit/molecules/commit_summarizer.rb +43 -0
- data/lib/ace/git_commit/molecules/diff_analyzer.rb +111 -0
- data/lib/ace/git_commit/molecules/file_stager.rb +153 -0
- data/lib/ace/git_commit/molecules/message_generator.rb +438 -0
- data/lib/ace/git_commit/molecules/path_resolver.rb +365 -0
- data/lib/ace/git_commit/molecules/split_commit_executor.rb +272 -0
- data/lib/ace/git_commit/organisms/commit_orchestrator.rb +330 -0
- data/lib/ace/git_commit/version.rb +7 -0
- data/lib/ace/git_commit.rb +41 -0
- metadata +149 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module GitCommit
|
|
8
|
+
module Molecules
|
|
9
|
+
# MessageGenerator generates commit messages using LLM
|
|
10
|
+
class MessageGenerator
|
|
11
|
+
DEFAULT_MODEL = "glite"
|
|
12
|
+
MAX_TOKENS = 8192
|
|
13
|
+
SYSTEM_PROMPT_PATH = "ace-git-commit/handbook/prompts/git-commit.system.md"
|
|
14
|
+
COMMIT_HEADER_PATTERN = /\A(feat|fix|docs|style|refactor|test|chore|spec|perf|build|ci|revert)(\([^)]+\))?:\s+\S+/
|
|
15
|
+
|
|
16
|
+
class BatchParseError < StandardError; end
|
|
17
|
+
|
|
18
|
+
def initialize(config = nil)
|
|
19
|
+
@config = config || {}
|
|
20
|
+
@model = @config.fetch("model", DEFAULT_MODEL)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Generate a commit message from diff
|
|
24
|
+
# @param diff [String] The git diff
|
|
25
|
+
# @param intention [String, nil] Optional intention/context
|
|
26
|
+
# @param files [Array<String>] List of changed files
|
|
27
|
+
# @param config [Hash, nil] Optional per-invocation config override
|
|
28
|
+
# @return [String] Generated commit message
|
|
29
|
+
def generate(diff, intention: nil, files: [], config: nil)
|
|
30
|
+
system_prompt = load_system_prompt
|
|
31
|
+
user_prompt = build_user_prompt(diff, intention, files)
|
|
32
|
+
|
|
33
|
+
# Use QueryInterface with named parameters matching CLI
|
|
34
|
+
response = Ace::LLM::QueryInterface.query(
|
|
35
|
+
resolve_model(config),
|
|
36
|
+
user_prompt,
|
|
37
|
+
system: system_prompt,
|
|
38
|
+
temperature: 0.7,
|
|
39
|
+
timeout: 60,
|
|
40
|
+
max_tokens: MAX_TOKENS
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
clean_commit_message(response[:text])
|
|
44
|
+
rescue Ace::LLM::Error => e
|
|
45
|
+
raise Error, "Failed to generate commit message: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Generate commit messages for multiple groups in one LLM call
|
|
49
|
+
# @param groups_context [Array<Hash>] Array of {scope_name:, diff:, files:, type_hint:, description:}
|
|
50
|
+
# @param intention [String, nil] Optional intention/context
|
|
51
|
+
# @param config [Hash, nil] Optional config override
|
|
52
|
+
# @return [Hash] { messages: Array<String>, order: Array<String> } with LLM-recommended order
|
|
53
|
+
def generate_batch(groups_context, intention: nil, config: nil)
|
|
54
|
+
return {messages: [], order: []} if groups_context.empty?
|
|
55
|
+
|
|
56
|
+
if groups_context.length == 1
|
|
57
|
+
msg = generate(groups_context.first[:diff], intention: intention, files: groups_context.first[:files], config: config)
|
|
58
|
+
return {messages: [msg], order: [groups_context.first[:scope_name]]}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
system_prompt = load_batch_system_prompt
|
|
62
|
+
user_prompt = build_batch_user_prompt(groups_context, intention)
|
|
63
|
+
|
|
64
|
+
response = Ace::LLM::QueryInterface.query(
|
|
65
|
+
resolve_model(config),
|
|
66
|
+
user_prompt,
|
|
67
|
+
system: system_prompt,
|
|
68
|
+
temperature: 0.7,
|
|
69
|
+
timeout: 120,
|
|
70
|
+
max_tokens: MAX_TOKENS
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
parse_batch_response(response[:text], groups_context)
|
|
74
|
+
rescue BatchParseError => e
|
|
75
|
+
repaired = retry_batch_parse(groups_context, intention, response[:text], e.message, config)
|
|
76
|
+
return repaired if repaired
|
|
77
|
+
|
|
78
|
+
raise Error, "Failed to generate batch commit messages: #{e.message}"
|
|
79
|
+
rescue Ace::LLM::Error => e
|
|
80
|
+
raise Error, "Failed to generate batch commit messages: #{e.message}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def load_batch_system_prompt
|
|
86
|
+
<<~PROMPT
|
|
87
|
+
You are a git commit message generator. Generate clear, concise commit messages following conventional commit format.
|
|
88
|
+
|
|
89
|
+
You will receive MULTIPLE groups of changes that need SEPARATE commit messages.
|
|
90
|
+
Each group represents a different scope/area of the codebase.
|
|
91
|
+
|
|
92
|
+
CRITICAL: Generate DISTINCT messages for each group. Each message must:
|
|
93
|
+
- Accurately describe what changed in THAT specific group
|
|
94
|
+
- Use different wording than other groups
|
|
95
|
+
- Focus on the specific scope/area of that group
|
|
96
|
+
|
|
97
|
+
Format for EACH message:
|
|
98
|
+
<type>(<scope>): <subject>
|
|
99
|
+
|
|
100
|
+
<body>
|
|
101
|
+
|
|
102
|
+
Types: feat, fix, docs, style, refactor, test, chore, spec
|
|
103
|
+
|
|
104
|
+
TYPE SELECTION - VERY IMPORTANT:
|
|
105
|
+
- Each group MAY have a "PREFERRED TYPE" hint - this hint applies ONLY to that specific group
|
|
106
|
+
- Groups WITHOUT a type hint: analyze the actual code changes and select appropriately:
|
|
107
|
+
* feat = new functionality, new features, new capabilities
|
|
108
|
+
* fix = bug fixes
|
|
109
|
+
* refactor = code restructuring without behavior change
|
|
110
|
+
* test = test additions/changes
|
|
111
|
+
* docs = documentation OF the software (user guides, API docs, README)
|
|
112
|
+
* chore = build/config changes only
|
|
113
|
+
* spec = specifications and artifacts from making software (task specs, planning docs, retros, ideas)
|
|
114
|
+
- DO NOT let hints from one group influence your type selection for other groups
|
|
115
|
+
- For code packages (lib/, src/, actual implementation): prefer feat/fix/refactor based on changes
|
|
116
|
+
|
|
117
|
+
COMMIT ORDER - Output groups in logical commit order:
|
|
118
|
+
- Implementation/feature code FIRST (the actual functionality)
|
|
119
|
+
- Supporting libraries/dependencies that the feature uses
|
|
120
|
+
- Configuration that enables/configures the feature
|
|
121
|
+
- Documentation, specs, retros LAST (they document what was done)
|
|
122
|
+
|
|
123
|
+
Rules of thumb:
|
|
124
|
+
- feat/fix commits usually come before chore/docs
|
|
125
|
+
- Core packages before config packages
|
|
126
|
+
- But use judgment - sometimes config must come first if it enables the feature
|
|
127
|
+
|
|
128
|
+
OUTPUT MUST BE STRICT JSON ONLY (no markdown, no prose, no code fences):
|
|
129
|
+
{
|
|
130
|
+
"order": ["scope-a", "scope-b"],
|
|
131
|
+
"messages": [
|
|
132
|
+
{"scope": "scope-a", "message": "feat(scope-a): ..."},
|
|
133
|
+
{"scope": "scope-b", "message": "fix(scope-b): ..."}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
HARD RULES:
|
|
138
|
+
- "order" must include every scope exactly once
|
|
139
|
+
- "messages" must include every scope exactly once
|
|
140
|
+
- each "message" must start with a valid conventional commit header
|
|
141
|
+
- do not use "chore" unless the diff is actually build/config/maintenance only
|
|
142
|
+
PROMPT
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def build_batch_user_prompt(groups_context, intention)
|
|
146
|
+
prompt = []
|
|
147
|
+
|
|
148
|
+
if intention && !intention.empty?
|
|
149
|
+
prompt << "Overall intention/context: #{intention}"
|
|
150
|
+
prompt << ""
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
prompt << "Generate #{groups_context.length} DISTINCT commit messages for these groups."
|
|
154
|
+
prompt << "OUTPUT THEM IN YOUR RECOMMENDED COMMIT ORDER (implementation first, docs last)."
|
|
155
|
+
prompt << "Return STRICT JSON only with keys: order, messages."
|
|
156
|
+
prompt << "Messages format: <type>(<scope>): <subject>"
|
|
157
|
+
prompt << ""
|
|
158
|
+
|
|
159
|
+
groups_context.each_with_index do |ctx, _index|
|
|
160
|
+
prompt << "=" * 60
|
|
161
|
+
prompt << "SCOPE: #{ctx[:scope_name]}"
|
|
162
|
+
prompt << "=" * 60
|
|
163
|
+
|
|
164
|
+
# Include type hint OR explicit instruction to analyze
|
|
165
|
+
prompt << if ctx[:type_hint] && !ctx[:type_hint].to_s.empty?
|
|
166
|
+
"PREFERRED TYPE FOR THIS GROUP ONLY: #{ctx[:type_hint]}"
|
|
167
|
+
else
|
|
168
|
+
"TYPE: Analyze changes and select appropriate type (feat/fix/refactor/test/docs/chore)"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Include description if provided
|
|
172
|
+
if ctx[:description] && !ctx[:description].to_s.empty?
|
|
173
|
+
prompt << "Scope context: #{ctx[:description]}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
prompt << ""
|
|
177
|
+
|
|
178
|
+
if ctx[:files] && !ctx[:files].empty?
|
|
179
|
+
prompt << "Files in this group:"
|
|
180
|
+
ctx[:files].each { |f| prompt << " - #{f}" }
|
|
181
|
+
prompt << ""
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
prompt << "Diff for this group:"
|
|
185
|
+
prompt << ctx[:diff]
|
|
186
|
+
prompt << ""
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
prompt.join("\n")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def parse_batch_response(response, groups_context)
|
|
193
|
+
return {messages: [clean_commit_message(response)], order: [groups_context.first[:scope_name]]} if groups_context.length == 1
|
|
194
|
+
|
|
195
|
+
scope_names = groups_context.map { |g| g[:scope_name] }
|
|
196
|
+
parsed = parse_batch_json(response)
|
|
197
|
+
order = Array(parsed["order"])
|
|
198
|
+
messages_array = Array(parsed["messages"])
|
|
199
|
+
|
|
200
|
+
message_by_scope = {}
|
|
201
|
+
messages_array.each do |item|
|
|
202
|
+
next unless item.is_a?(Hash)
|
|
203
|
+
|
|
204
|
+
scope = item["scope"] || item[:scope]
|
|
205
|
+
message = item["message"] || item[:message]
|
|
206
|
+
next if scope.nil? || message.nil?
|
|
207
|
+
|
|
208
|
+
message_by_scope[scope.to_s] = clean_commit_message(message.to_s)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
validate_scope_list!("order", order, scope_names)
|
|
212
|
+
validate_scope_list!("messages", message_by_scope.keys, scope_names)
|
|
213
|
+
|
|
214
|
+
ordered_messages = order.map do |scope|
|
|
215
|
+
msg = message_by_scope[scope]
|
|
216
|
+
validate_commit_header!(scope, msg)
|
|
217
|
+
msg
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
{messages: ordered_messages, order: order}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def parse_batch_json(response)
|
|
224
|
+
raw = response.to_s.strip
|
|
225
|
+
raw = raw.gsub(/\A```(?:json)?\s*/i, "").gsub(/\s*```\z/, "").strip
|
|
226
|
+
raw = extract_json_block(raw)
|
|
227
|
+
JSON.parse(raw)
|
|
228
|
+
rescue JSON::ParserError => e
|
|
229
|
+
raise BatchParseError, "Invalid batch JSON: #{e.message}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def extract_json_block(text)
|
|
233
|
+
start_idx = text.index("{")
|
|
234
|
+
end_idx = text.rindex("}")
|
|
235
|
+
raise BatchParseError, "Batch response does not include a JSON object." unless start_idx && end_idx
|
|
236
|
+
|
|
237
|
+
text[start_idx..end_idx]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def validate_scope_list!(label, actual, expected)
|
|
241
|
+
actual = actual.map(&:to_s)
|
|
242
|
+
missing = expected - actual
|
|
243
|
+
extra = actual - expected
|
|
244
|
+
duplicates = actual.group_by(&:itself).select { |_k, v| v.length > 1 }.keys
|
|
245
|
+
|
|
246
|
+
return if missing.empty? && extra.empty? && duplicates.empty?
|
|
247
|
+
|
|
248
|
+
parts = []
|
|
249
|
+
parts << "missing=#{missing.join(",")}" unless missing.empty?
|
|
250
|
+
parts << "extra=#{extra.join(",")}" unless extra.empty?
|
|
251
|
+
parts << "duplicates=#{duplicates.join(",")}" unless duplicates.empty?
|
|
252
|
+
raise BatchParseError, "#{label} scope validation failed (#{parts.join(" | ")})"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def validate_commit_header!(scope, message)
|
|
256
|
+
return if message && message.match?(COMMIT_HEADER_PATTERN)
|
|
257
|
+
|
|
258
|
+
raise BatchParseError, "Invalid commit header for scope '#{scope}': #{message.inspect}"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def retry_batch_parse(groups_context, intention, previous_response, reason, config)
|
|
262
|
+
warn "[ace-git-commit] Batch parse failed, retrying with strict JSON repair: #{reason}"
|
|
263
|
+
|
|
264
|
+
repair_prompt = build_batch_repair_user_prompt(groups_context, intention, previous_response, reason)
|
|
265
|
+
repair_response = Ace::LLM::QueryInterface.query(
|
|
266
|
+
resolve_model(config),
|
|
267
|
+
repair_prompt,
|
|
268
|
+
system: load_batch_system_prompt,
|
|
269
|
+
temperature: 0.2,
|
|
270
|
+
timeout: 120,
|
|
271
|
+
max_tokens: MAX_TOKENS
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
parse_batch_response(repair_response[:text], groups_context)
|
|
275
|
+
rescue BatchParseError => e
|
|
276
|
+
warn "[ace-git-commit] Batch parse retry failed: #{e.message}"
|
|
277
|
+
nil
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def build_batch_repair_user_prompt(groups_context, intention, bad_response, reason)
|
|
281
|
+
scope_names = groups_context.map { |g| g[:scope_name] }
|
|
282
|
+
prompt = []
|
|
283
|
+
prompt << "Your previous response was invalid for strict JSON batch commit output."
|
|
284
|
+
prompt << "Reason: #{reason}"
|
|
285
|
+
prompt << "Allowed scopes: #{scope_names.join(", ")}"
|
|
286
|
+
prompt << "Intention/context: #{intention}" if intention && !intention.empty?
|
|
287
|
+
prompt << ""
|
|
288
|
+
prompt << "Previous response:"
|
|
289
|
+
prompt << bad_response.to_s
|
|
290
|
+
prompt << ""
|
|
291
|
+
prompt << "Return ONLY valid JSON in this exact shape:"
|
|
292
|
+
prompt << '{"order":["scope-a","scope-b"],"messages":[{"scope":"scope-a","message":"feat(scope-a): ..."},{"scope":"scope-b","message":"fix(scope-b): ..."}]}'
|
|
293
|
+
prompt.join("\n")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def resolve_model(config_override)
|
|
297
|
+
return @model unless config_override.is_a?(Hash)
|
|
298
|
+
|
|
299
|
+
config_override.fetch("model", @model)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Load system prompt from template
|
|
303
|
+
# @return [String] System prompt content
|
|
304
|
+
def load_system_prompt
|
|
305
|
+
# Try to find the prompt in the project structure
|
|
306
|
+
prompt_path = find_system_prompt_path
|
|
307
|
+
|
|
308
|
+
if prompt_path && File.exist?(prompt_path)
|
|
309
|
+
File.read(prompt_path)
|
|
310
|
+
else
|
|
311
|
+
# Fallback to embedded prompt
|
|
312
|
+
default_system_prompt
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Find the system prompt file path
|
|
317
|
+
# @return [String, nil] Path to system prompt or nil
|
|
318
|
+
def find_system_prompt_path
|
|
319
|
+
# Look for ace-git-commit/handbook in current directory or parent directories
|
|
320
|
+
current = Pathname.pwd
|
|
321
|
+
|
|
322
|
+
while current.parent != current
|
|
323
|
+
prompt_file = current.join(SYSTEM_PROMPT_PATH)
|
|
324
|
+
return prompt_file.to_s if prompt_file.exist?
|
|
325
|
+
|
|
326
|
+
# Also check if we're already in ace
|
|
327
|
+
if current.basename.to_s == "ace-git-commit"
|
|
328
|
+
parent_prompt = current.parent.join(SYSTEM_PROMPT_PATH)
|
|
329
|
+
return parent_prompt.to_s if parent_prompt.exist?
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
current = current.parent
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
nil
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Build user prompt from diff and context
|
|
339
|
+
# @param diff [String] The git diff
|
|
340
|
+
# @param intention [String, nil] Optional intention
|
|
341
|
+
# @param files [Array<String>] Changed files
|
|
342
|
+
# @return [String] User prompt
|
|
343
|
+
def build_user_prompt(diff, intention, files)
|
|
344
|
+
prompt = []
|
|
345
|
+
|
|
346
|
+
if intention && !intention.empty?
|
|
347
|
+
prompt << "Intention/Context: #{intention}"
|
|
348
|
+
prompt << ""
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
if files && !files.empty?
|
|
352
|
+
prompt << "Changed files:"
|
|
353
|
+
files.each { |f| prompt << " - #{f}" }
|
|
354
|
+
prompt << ""
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
prompt << "Git diff:"
|
|
358
|
+
prompt << diff
|
|
359
|
+
|
|
360
|
+
prompt.join("\n")
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Clean and format the generated commit message
|
|
364
|
+
# @param message [String] Raw generated message
|
|
365
|
+
# @return [String] Cleaned message
|
|
366
|
+
def clean_commit_message(message)
|
|
367
|
+
return "" if message.nil?
|
|
368
|
+
|
|
369
|
+
# Remove any markdown code blocks
|
|
370
|
+
message = message.gsub(/```[a-z]*\n?/, "")
|
|
371
|
+
message = message.gsub(/```\n?/, "")
|
|
372
|
+
|
|
373
|
+
# Remove leading/trailing whitespace
|
|
374
|
+
message = message.strip
|
|
375
|
+
|
|
376
|
+
# Ensure proper formatting
|
|
377
|
+
lines = message.lines.map(&:rstrip)
|
|
378
|
+
|
|
379
|
+
# Remove empty lines at the beginning
|
|
380
|
+
while lines.first && lines.first.strip.empty?
|
|
381
|
+
lines.shift
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Ensure single blank line between title and body
|
|
385
|
+
if lines.length > 1
|
|
386
|
+
# Find the first non-empty line after the title
|
|
387
|
+
title_index = 0
|
|
388
|
+
body_start = 1
|
|
389
|
+
|
|
390
|
+
while body_start < lines.length && lines[body_start].strip.empty?
|
|
391
|
+
body_start += 1
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
if body_start < lines.length
|
|
395
|
+
# Reconstruct with single blank line
|
|
396
|
+
result = [lines[title_index]]
|
|
397
|
+
result << ""
|
|
398
|
+
result.concat(lines[body_start..-1])
|
|
399
|
+
lines = result
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
lines.join("\n")
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Default system prompt if template not found
|
|
407
|
+
# @return [String] Default prompt
|
|
408
|
+
def default_system_prompt
|
|
409
|
+
<<~PROMPT
|
|
410
|
+
You are a git commit message generator. Generate clear, concise commit messages following conventional commit format.
|
|
411
|
+
|
|
412
|
+
Format:
|
|
413
|
+
<type>(<scope>): <subject>
|
|
414
|
+
|
|
415
|
+
<body>
|
|
416
|
+
|
|
417
|
+
Types:
|
|
418
|
+
- feat: New feature
|
|
419
|
+
- fix: Bug fix
|
|
420
|
+
- docs: Documentation changes
|
|
421
|
+
- style: Code style changes (formatting, etc.)
|
|
422
|
+
- refactor: Code refactoring
|
|
423
|
+
- test: Test changes
|
|
424
|
+
- chore: Build process or auxiliary tool changes
|
|
425
|
+
|
|
426
|
+
Rules:
|
|
427
|
+
- Subject line: max 72 characters, imperative mood
|
|
428
|
+
- Scope: optional, component or area affected
|
|
429
|
+
- Body: explain what and why, not how
|
|
430
|
+
- Keep messages clear and professional
|
|
431
|
+
|
|
432
|
+
Generate only the commit message, no additional commentary.
|
|
433
|
+
PROMPT
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|