@kb-labs/commit-core 0.6.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.
@@ -0,0 +1,2694 @@
1
+ import { useLogger, useAnalytics, useLLM } from '@kb-labs/sdk';
2
+ import { simpleGit } from 'simple-git';
3
+ import { dirname, basename, extname } from 'path';
4
+ import { minimatch } from 'minimatch';
5
+ import * as readline from 'readline';
6
+
7
+ // src/generator/commit-plan.ts
8
+
9
+ // src/generator/commit-tools.ts
10
+ var COMMIT_PLAN_TOOL_PHASE3 = {
11
+ name: "generate_commit_plan",
12
+ description: "Generate commits for missing files - either extend existing commits or create new ones",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {
16
+ commits: {
17
+ type: "array",
18
+ description: "List of commit actions. Can mix extend_existing and create_new actions.",
19
+ items: {
20
+ type: "object",
21
+ required: ["action", "files"],
22
+ properties: {
23
+ action: {
24
+ type: "string",
25
+ enum: ["create_new", "extend_existing"],
26
+ description: 'Whether to create a new commit or add files to an existing commit. Use "extend_existing" to avoid creating unnecessary commits.'
27
+ },
28
+ existingCommitId: {
29
+ type: "string",
30
+ description: 'ID of existing commit to extend (REQUIRED when action is "extend_existing"). Example: "c1", "c2".',
31
+ pattern: "^c[0-9]+$"
32
+ },
33
+ id: {
34
+ type: "string",
35
+ description: "Unique commit identifier (REQUIRED for create_new action, e.g., c1, c2, c3).",
36
+ pattern: "^c[0-9]+$"
37
+ },
38
+ type: {
39
+ type: "string",
40
+ enum: ["feat", "fix", "refactor", "chore", "docs", "test", "build", "ci", "perf"],
41
+ description: "Conventional commit type (REQUIRED for create_new action)."
42
+ },
43
+ scope: {
44
+ type: "string",
45
+ description: "Scope of the commit (optional)."
46
+ },
47
+ message: {
48
+ type: "string",
49
+ description: "Commit message in imperative mood (REQUIRED for create_new action).",
50
+ minLength: 5,
51
+ maxLength: 100
52
+ },
53
+ body: {
54
+ type: "string",
55
+ description: "Detailed description (optional)."
56
+ },
57
+ files: {
58
+ type: "array",
59
+ items: { type: "string" },
60
+ description: "List of file paths to add to this commit.",
61
+ minItems: 1
62
+ },
63
+ releaseHint: {
64
+ type: "string",
65
+ enum: ["none", "patch", "minor", "major"],
66
+ description: "Semantic versioning impact (REQUIRED for create_new action)."
67
+ },
68
+ breaking: {
69
+ type: "boolean",
70
+ description: "Whether this is a breaking change.",
71
+ default: false
72
+ },
73
+ reasoning: {
74
+ type: "object",
75
+ description: "Reasoning for classification (REQUIRED for create_new action).",
76
+ properties: {
77
+ newBehavior: { type: "boolean" },
78
+ fixesBug: { type: "boolean" },
79
+ internalOnly: { type: "boolean" },
80
+ explanation: { type: "string", minLength: 10, maxLength: 300 },
81
+ confidence: { type: "number", minimum: 0, maximum: 1 }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ },
88
+ required: ["commits"]
89
+ }
90
+ };
91
+ var COMMIT_PLAN_TOOL = {
92
+ name: "generate_commit_plan",
93
+ description: "Generate a structured commit plan with conventional commits following best practices",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ needsMoreContext: {
98
+ type: "boolean",
99
+ description: "Whether you need to see diff content to make accurate commit type decisions. Set to true if file paths and stats alone are insufficient.",
100
+ default: false
101
+ },
102
+ requestedFiles: {
103
+ type: "array",
104
+ items: { type: "string" },
105
+ description: "List of SPECIFIC files you need diffs for (MAXIMUM 15 files). Only request files where file path and stats are insufficient. DO NOT request all files - be selective and choose only the most critical/ambiguous ones. Prioritize files with unclear intent or complex changes.",
106
+ default: []
107
+ },
108
+ commits: {
109
+ type: "array",
110
+ description: "List of commit groups. Each commit groups related files by logical change (not by file type or directory).",
111
+ items: {
112
+ type: "object",
113
+ required: ["id", "type", "message", "files", "releaseHint", "breaking", "reasoning"],
114
+ properties: {
115
+ id: {
116
+ type: "string",
117
+ description: "Unique commit identifier (e.g., c1, c2, c3).",
118
+ pattern: "^c[0-9]+$"
119
+ },
120
+ type: {
121
+ type: "string",
122
+ enum: ["feat", "fix", "refactor", "chore", "docs", "test", "build", "ci", "perf"],
123
+ description: "Conventional commit type. Use refactor for internal changes, feat only for new user-facing features."
124
+ },
125
+ scope: {
126
+ type: "string",
127
+ description: 'Scope of the commit (e.g., "cli", "api", "core"). Should reflect affected area, not individual files.'
128
+ },
129
+ message: {
130
+ type: "string",
131
+ description: 'Commit message in imperative mood, lowercase, no period at end (e.g., "add authentication middleware")',
132
+ minLength: 5,
133
+ maxLength: 100
134
+ },
135
+ body: {
136
+ type: "string",
137
+ description: "Detailed description with bullet points listing affected files/changes. Use for commits with 2+ files."
138
+ },
139
+ files: {
140
+ type: "array",
141
+ items: { type: "string" },
142
+ description: "List of file paths in this commit. Each file must appear in exactly ONE commit (no duplicates across commits).",
143
+ minItems: 1
144
+ },
145
+ releaseHint: {
146
+ type: "string",
147
+ enum: ["none", "patch", "minor", "major"],
148
+ description: 'Semantic versioning impact. Use "minor" for feat, "patch" for fix/refactor, "none" for chore/docs/test.'
149
+ },
150
+ breaking: {
151
+ type: "boolean",
152
+ description: "Whether this is a breaking change (breaks public API compatibility).",
153
+ default: false
154
+ },
155
+ reasoning: {
156
+ type: "object",
157
+ required: ["newBehavior", "fixesBug", "internalOnly", "explanation", "confidence"],
158
+ description: "Reasoning for commit type classification. Used for validation and debugging.",
159
+ properties: {
160
+ newBehavior: {
161
+ type: "boolean",
162
+ description: "Does this change add NEW USER-VISIBLE BEHAVIOR? (new API, feature, capability that users can access)"
163
+ },
164
+ fixesBug: {
165
+ type: "boolean",
166
+ description: "Does this change fix BROKEN functionality? (corrects bug or error)"
167
+ },
168
+ internalOnly: {
169
+ type: "boolean",
170
+ description: "Is this ONLY INTERNAL restructuring? (code reorganization, renaming, extracting functions without changing behavior)"
171
+ },
172
+ explanation: {
173
+ type: "string",
174
+ description: "Explain your classification decision. Why did you choose this commit type?",
175
+ minLength: 10,
176
+ maxLength: 300
177
+ },
178
+ confidence: {
179
+ type: "number",
180
+ minimum: 0,
181
+ maximum: 1,
182
+ description: "Confidence in this classification (0.0 to 1.0). Use <0.7 if you need more context (will trigger Phase 2)."
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+ },
190
+ required: ["commits"]
191
+ }
192
+ };
193
+ async function getGitStatus(cwd) {
194
+ const git = simpleGit(cwd);
195
+ const status = await git.status(["--ignore-submodules=all"]);
196
+ return {
197
+ staged: status.staged.filter((f) => !shouldIgnoreFile(f)),
198
+ unstaged: [...status.modified, ...status.deleted].filter((f) => !status.staged.includes(f)).filter((f) => !shouldIgnoreFile(f)),
199
+ untracked: status.not_added.filter((f) => !shouldIgnoreFile(f))
200
+ };
201
+ }
202
+ function shouldIgnoreFile(file) {
203
+ const ignoredPaths = [
204
+ "node_modules/",
205
+ ".git/",
206
+ "dist/",
207
+ "build/",
208
+ ".next/",
209
+ ".turbo/",
210
+ "coverage/",
211
+ ".cache/",
212
+ ".temp/",
213
+ "tmp/"
214
+ ];
215
+ return ignoredPaths.some((path) => file.includes(path));
216
+ }
217
+ function getAllChangedFiles(status) {
218
+ const allFiles = [
219
+ .../* @__PURE__ */ new Set([...status.staged, ...status.unstaged, ...status.untracked])
220
+ ];
221
+ return allFiles.filter((file) => !shouldIgnoreFile(file));
222
+ }
223
+ function isTextFile(file) {
224
+ return !file.binary;
225
+ }
226
+ async function isNewFile(cwd, filePath) {
227
+ try {
228
+ const repo = await findGitRepo(cwd, filePath);
229
+ if (!repo) {
230
+ return true;
231
+ }
232
+ const git = simpleGit(repo.repoPath);
233
+ const result = await git.raw(["ls-tree", "HEAD", "--", repo.relativePath]);
234
+ return result.trim().length === 0;
235
+ } catch {
236
+ return true;
237
+ }
238
+ }
239
+ async function getFileSummaries(cwd, files) {
240
+ if (files.length === 0) {
241
+ return [];
242
+ }
243
+ const summaries = [];
244
+ const filesByRepo = /* @__PURE__ */ new Map();
245
+ for (const file of files) {
246
+ const repo = await findGitRepo(cwd, file);
247
+ if (!repo) {
248
+ const group2 = filesByRepo.get(cwd) ?? [];
249
+ group2.push({ repoPath: cwd, relativePath: file, originalPath: file });
250
+ filesByRepo.set(cwd, group2);
251
+ continue;
252
+ }
253
+ const group = filesByRepo.get(repo.repoPath) ?? [];
254
+ group.push({
255
+ repoPath: repo.repoPath,
256
+ relativePath: repo.relativePath,
257
+ originalPath: file
258
+ });
259
+ filesByRepo.set(repo.repoPath, group);
260
+ }
261
+ for (const [repoPath, fileInfos] of filesByRepo) {
262
+ const git = simpleGit(repoPath);
263
+ const relativePaths = fileInfos.map((f) => f.relativePath);
264
+ try {
265
+ const stagedDiff = await git.diffSummary([
266
+ "--cached",
267
+ "--",
268
+ ...relativePaths
269
+ ]);
270
+ const unstagedDiff = await git.diffSummary(["--", ...relativePaths]);
271
+ const allDiffFiles = /* @__PURE__ */ new Map();
272
+ for (const file of unstagedDiff.files) {
273
+ allDiffFiles.set(file.file, file);
274
+ }
275
+ for (const file of stagedDiff.files) {
276
+ allDiffFiles.set(file.file, file);
277
+ }
278
+ for (const file of allDiffFiles.values()) {
279
+ const fileInfo = fileInfos.find((f) => f.relativePath === file.file);
280
+ if (!fileInfo) {
281
+ continue;
282
+ }
283
+ const isNew = await isNewFile(repoPath, file.file);
284
+ if (isTextFile(file)) {
285
+ summaries.push({
286
+ path: fileInfo.originalPath,
287
+ status: mapDiffStatus(file.insertions, file.deletions),
288
+ additions: file.insertions,
289
+ deletions: file.deletions,
290
+ binary: false,
291
+ isNewFile: isNew
292
+ });
293
+ } else {
294
+ summaries.push({
295
+ path: fileInfo.originalPath,
296
+ status: "modified",
297
+ additions: 0,
298
+ deletions: 0,
299
+ binary: true,
300
+ isNewFile: isNew
301
+ });
302
+ }
303
+ }
304
+ const processedPaths = new Set(
305
+ Array.from(allDiffFiles.values()).map((f) => f.file)
306
+ );
307
+ const missingInRepo = fileInfos.filter(
308
+ (f) => !processedPaths.has(f.relativePath)
309
+ );
310
+ for (const fileInfo of missingInRepo) {
311
+ summaries.push({
312
+ path: fileInfo.originalPath,
313
+ status: "added",
314
+ additions: 0,
315
+ deletions: 0,
316
+ binary: false,
317
+ isNewFile: true
318
+ });
319
+ }
320
+ } catch {
321
+ for (const fileInfo of fileInfos) {
322
+ summaries.push({
323
+ path: fileInfo.originalPath,
324
+ status: "modified",
325
+ additions: 0,
326
+ deletions: 0,
327
+ binary: false,
328
+ isNewFile: false
329
+ // Conservative assumption
330
+ });
331
+ }
332
+ }
333
+ }
334
+ return summaries;
335
+ }
336
+ function mapDiffStatus(insertions, deletions) {
337
+ if (deletions === 0 && insertions > 0) {
338
+ return "added";
339
+ }
340
+ if (insertions === 0 && deletions > 0) {
341
+ return "deleted";
342
+ }
343
+ return "modified";
344
+ }
345
+ async function getFileDiffs(cwd, files) {
346
+ if (files.length === 0) {
347
+ return /* @__PURE__ */ new Map();
348
+ }
349
+ const diffs = /* @__PURE__ */ new Map();
350
+ const filesByRepo = /* @__PURE__ */ new Map();
351
+ for (const file of files) {
352
+ const repo = await findGitRepo(cwd, file);
353
+ if (!repo) {
354
+ const group2 = filesByRepo.get(cwd) ?? [];
355
+ group2.push({ repoPath: cwd, relativePath: file, originalPath: file });
356
+ filesByRepo.set(cwd, group2);
357
+ continue;
358
+ }
359
+ const group = filesByRepo.get(repo.repoPath) ?? [];
360
+ group.push({
361
+ repoPath: repo.repoPath,
362
+ relativePath: repo.relativePath,
363
+ originalPath: file
364
+ });
365
+ filesByRepo.set(repo.repoPath, group);
366
+ }
367
+ for (const [repoPath, fileInfos] of filesByRepo) {
368
+ const git = simpleGit(repoPath);
369
+ for (const { relativePath, originalPath } of fileInfos) {
370
+ try {
371
+ let diff = await git.diff(["--cached", "--", relativePath]);
372
+ if (!diff) {
373
+ diff = await git.diff(["--", relativePath]);
374
+ }
375
+ if (!diff) {
376
+ diff = await git.show([`:${relativePath}`]).catch(() => "");
377
+ }
378
+ if (diff) {
379
+ diffs.set(originalPath, diff);
380
+ }
381
+ } catch {
382
+ }
383
+ }
384
+ }
385
+ return diffs;
386
+ }
387
+ async function existsAsync(path) {
388
+ try {
389
+ const { existsSync } = await import('fs');
390
+ return existsSync(path);
391
+ } catch {
392
+ return false;
393
+ }
394
+ }
395
+ async function findGitRepo(basePath, filePath) {
396
+ const segments = filePath.split("/");
397
+ for (let i = segments.length - 1; i > 0; i--) {
398
+ const potentialRepoSegments = segments.slice(0, i);
399
+ const potentialRepoPath = `${basePath}/${potentialRepoSegments.join("/")}`;
400
+ const gitDir = `${potentialRepoPath}/.git`;
401
+ if (await existsAsync(gitDir)) {
402
+ const relativePath = segments.slice(i).join("/");
403
+ return { repoPath: potentialRepoPath, relativePath };
404
+ }
405
+ }
406
+ if (await existsAsync(`${basePath}/.git`)) {
407
+ return { repoPath: basePath, relativePath: filePath };
408
+ }
409
+ return null;
410
+ }
411
+ async function getRecentCommits(cwd, count = 10) {
412
+ const git = simpleGit(cwd);
413
+ try {
414
+ const log = await git.log({
415
+ maxCount: count,
416
+ format: {
417
+ message: "%s"
418
+ // Subject line only
419
+ }
420
+ });
421
+ return log.all.map((commit) => commit.message);
422
+ } catch {
423
+ return [];
424
+ }
425
+ }
426
+
427
+ // src/generator/llm-prompt.ts
428
+ var SYSTEM_PROMPT = `You are a git commit message generator. Analyze the changed files and generate a commit plan.
429
+
430
+ CRITICAL OUTPUT FORMAT:
431
+ - Return ONLY a valid JSON object
432
+ - Do NOT wrap in markdown code blocks (no \`\`\`json, no \`\`\`)
433
+ - Do NOT add any text before or after the JSON
434
+ - Ensure all strings are properly escaped (use double quotes, escape backslashes and quotes)
435
+
436
+ IMPORTANT: You must assess your confidence level. If file paths and stats alone are not enough to determine the correct commit type and message, set needsMoreContext to true and list the files you need to see the diff for.
437
+
438
+ CRITICAL GROUPING RULES:
439
+ - Group files by LOGICAL CHANGE, not by file type or directory
440
+ - If multiple files implement the same feature/fix/refactor, they belong in ONE commit
441
+ - For initial project setup (many new files): group by package or functional area (contracts, core, cli, docs, tests)
442
+ - Target: 3-8 commits for <50 files, 5-12 commits for 50-150 files, 10-20 commits for 150+ files
443
+ - Ask yourself: "Would a developer make these changes in separate commits?" If no, group them!
444
+ - CRITICAL: Each file must appear in EXACTLY ONE commit - no duplicates across commits!
445
+
446
+ CRITICAL: FILE TYPE SHORTCUTS (check FIRST, before other rules):
447
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
448
+
449
+ \u{1F6AB} STOP! Check file extensions BEFORE asking questions below:
450
+
451
+ \u{1F4DD} ALL files are *.md or *.mdx? \u2192 Type: docs (SKIP all questions below!)
452
+ \u{1F9EA} ALL files in test/, __tests__/, *.test.ts, *.spec.ts? \u2192 Type: test
453
+ \u274C If ANY file has code (.ts, .js, .tsx, .jsx, .py, etc.) \u2192 Continue to questions below
454
+
455
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
456
+
457
+ CRITICAL COMMIT TYPE CLASSIFICATION:
458
+ For EACH commit, you MUST answer these questions to determine the correct type:
459
+
460
+ 1. Does this change add NEW USER-VISIBLE BEHAVIOR? (yes/no)
461
+ - Can users/developers do something they couldn't before?
462
+ - Is there a new API, feature, or capability?
463
+ \u2192 YES = likely feat
464
+
465
+ 2. Does this change fix BROKEN functionality? (yes/no)
466
+ - Was something not working correctly?
467
+ - Is this correcting a bug or error?
468
+ \u2192 YES = fix
469
+
470
+ 3. Is this ONLY INTERNAL restructuring? (yes/no)
471
+ - Code reorganization, renaming, extracting functions?
472
+ - Improving structure WITHOUT changing behavior?
473
+ - Modified files with balanced additions/deletions?
474
+ \u2192 YES = refactor
475
+
476
+ 4. Is this configuration, build, or maintenance work? (yes/no)
477
+ - Dependencies, build configs, tooling?
478
+ - No code logic changes?
479
+ \u2192 YES = chore
480
+
481
+ DEFAULT BIAS: When uncertain between feat and refactor, choose refactor!
482
+
483
+ Rules:
484
+ 1. Use conventional commits: feat, fix, refactor, chore, docs, test, build, ci, perf
485
+ 2. Group related files - number of commits should scale with file count (see grouping rules above)
486
+ 3. Each commit MUST include "reasoning" field explaining your classification
487
+ 4. Message should be lowercase, imperative mood, no period at end
488
+ 5. breaking: true only for breaking API changes
489
+ 6. For commits with 2+ files, add "body" with bullet points listing affected files/changes
490
+ 7. Scope should reflect the affected area (e.g., "cli", "api"), not individual files
491
+ 8. CRITICAL: If ALL files in a commit have status "deleted", use type "chore" or "refactor", NOT "feat"
492
+ 9. CRITICAL: If a commit is mostly deletions (>80% deletions), use "refactor" or "chore", NOT "feat"
493
+
494
+ 10. CRITICAL: WRITE INFORMATIVE COMMIT MESSAGES (not generic):
495
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
496
+
497
+ \u274C BAD (too generic):
498
+ - "add feature"
499
+ - "update files"
500
+ - "refactor code"
501
+ - "fix bug"
502
+ - "improve types"
503
+
504
+ \u2705 GOOD (specific and descriptive):
505
+ - "add JWT authentication with refresh token support"
506
+ - "update TypeScript configuration for strict mode"
507
+ - "refactor plugin execution to use factory pattern"
508
+ - "fix null pointer exception in authentication middleware"
509
+ - "improve type safety in workflow execution context"
510
+
511
+ Guidelines:
512
+ - Include WHAT was changed (specific feature/component)
513
+ - Include HOW if relevant (method, pattern, technology)
514
+ - Use concrete nouns (not "files", "code", "feature")
515
+ - Add context that helps reviewers understand the change
516
+ - For body: list specific changes, not just file names
517
+
518
+ 11. CRITICAL: SPECIFIC TYPE DETECTION (check BEFORE defaulting to refactor):
519
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
520
+
521
+ \u{1F4DD} docs: Documentation ONLY (no code changes)
522
+ \u2705 ALL files are markdown (*.md, *.mdx) \u2192 docs (even if IsNewFile: true)
523
+ \u2705 README.md, CONTRIBUTING.md, API docs, ADRs
524
+ \u2705 Files in docs/ or doc/ directory with only markdown
525
+ \u2705 JSDoc comments only (no logic changes)
526
+ \u274C NOT docs if ANY file has code logic changes
527
+
528
+ **IMPORTANT**: If ALL files end with .md or .mdx \u2192 ALWAYS use docs, NEVER feat!
529
+
530
+ \u{1F41B} fix: Corrects BROKEN functionality
531
+ \u2705 Bug fixes, error handling corrections
532
+ \u2705 Fixes crashes, incorrect behavior
533
+ \u2705 Corrects typos in USER-FACING text (not code comments)
534
+ \u274C NOT fix if adding new behavior (that's feat)
535
+
536
+ \u{1F9EA} test: Test files ONLY
537
+ \u2705 Files in test/, tests/, __tests__/, *.test.ts, *.spec.ts
538
+ \u2705 Adding/updating test cases
539
+ \u274C NOT test if also changing source code
540
+
541
+ 12. CRITICAL: IsNewFile flag determines STRONG BIAS against feat:
542
+
543
+ IsNewFile: FALSE (modified existing file):
544
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
545
+ \u2192 DEFAULT to refactor/fix/chore (NOT feat!)
546
+ \u2192 Only use feat if adds MAJOR new user-facing capability
547
+
548
+ Common cases where IsNewFile: false = NOT feat:
549
+ \u2705 Changed dependencies/imports \u2192 chore
550
+ \u2705 Added method to existing class \u2192 refactor
551
+ \u2705 Updated implementation logic \u2192 refactor
552
+ \u2705 Fixed bug in existing code \u2192 fix
553
+ \u2705 Renamed/moved code \u2192 refactor
554
+ \u2705 Modified config files \u2192 chore
555
+
556
+ \u274C WRONG: IsNewFile: false, minor additions \u2192 feat
557
+ \u2705 RIGHT: IsNewFile: false, minor additions \u2192 refactor
558
+
559
+
560
+ IsNewFile: TRUE (brand new file):
561
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
562
+ \u2192 Might be feat IF adds new user capability
563
+ \u2192 If only config/tooling/internal \u2192 chore
564
+
565
+ Examples:
566
+ \u2705 New API endpoint file \u2192 feat
567
+ \u2705 New CLI command file \u2192 feat
568
+ \u2705 New config file \u2192 chore
569
+ \u2705 New test file \u2192 test
570
+
571
+ EXACT JSON SCHEMA (copy this structure):
572
+ {
573
+ "needsMoreContext": false,
574
+ "requestedFiles": ["file1.ts"],
575
+ "commits": [
576
+ {
577
+ "id": "c1",
578
+ "type": "feat",
579
+ "scope": "auth",
580
+ "message": "add JWT authentication with refresh token support",
581
+ "body": "- implement JWT token generation and validation\\n- add refresh token rotation mechanism\\n- integrate with existing user authentication flow",
582
+ "files": ["src/auth/jwt-strategy.ts", "src/auth/refresh-token.ts", "src/middleware/auth.ts"],
583
+ "releaseHint": "minor",
584
+ "breaking": false,
585
+ "reasoning": {
586
+ "newBehavior": true,
587
+ "fixesBug": false,
588
+ "internalOnly": false,
589
+ "explanation": "Adds new JWT authentication capability with refresh tokens - enables secure stateless authentication for API users",
590
+ "confidence": 0.85
591
+ }
592
+ }
593
+ ]
594
+ }
595
+
596
+ VALID TYPE VALUES: feat, fix, refactor, chore, docs, test, build, ci, perf
597
+ VALID RELEASEHINT VALUES: none, patch, minor, major
598
+
599
+ EXAMPLE OUTPUT (use this as template):
600
+ {
601
+ "needsMoreContext": false,
602
+ "requestedFiles": [],
603
+ "commits": [
604
+ {
605
+ "id": "c1",
606
+ "type": "feat",
607
+ "scope": "cli",
608
+ "message": "add commit generation command",
609
+ "body": "- implement generate command\\n- add LLM integration",
610
+ "files": ["src/commands/generate.ts", "src/llm.ts"],
611
+ "releaseHint": "minor",
612
+ "breaking": false,
613
+ "reasoning": {
614
+ "newBehavior": true,
615
+ "fixesBug": false,
616
+ "internalOnly": false,
617
+ "explanation": "New command allows users to generate commits with LLM - new capability",
618
+ "confidence": 0.9
619
+ }
620
+ },
621
+ {
622
+ "id": "c2",
623
+ "type": "test",
624
+ "message": "add tests for commit generator",
625
+ "files": ["tests/generate.test.ts"],
626
+ "releaseHint": "none",
627
+ "breaking": false,
628
+ "reasoning": {
629
+ "newBehavior": false,
630
+ "fixesBug": false,
631
+ "internalOnly": true,
632
+ "explanation": "Test coverage for new feature - internal quality improvement",
633
+ "confidence": 0.85
634
+ }
635
+ }
636
+ ]
637
+ }
638
+
639
+ REAL-WORLD EXAMPLES (learn from these patterns):
640
+
641
+ Example 1: Modified files with low addition ratio \u2192 refactor, NOT feat
642
+ Files:
643
+ - commit-plan.ts (modified, +150/-120)
644
+ - llm-prompt.ts (modified, +80/-60)
645
+ Addition ratio: 230/350 = 65% (low, mostly structural changes)
646
+ \u274C WRONG: feat(core): add commit plan and llm prompt generators
647
+ \u2705 CORRECT: refactor(core): update commit plan and llm prompt logic
648
+ Reason: Modified files + low addition ratio = refactoring existing code
649
+
650
+ Example 2: New package with many files \u2192 feat, NOT chore
651
+ Files: 21 new files in packages/core-resource-broker/
652
+ - package.json (new)
653
+ - src/broker/resource-broker.ts (new)
654
+ - src/queue/priority-queue.ts (new)
655
+ - ... (18 more new files)
656
+ \u274C WRONG: chore(core-resource-broker): initialize core resource broker package
657
+ \u2705 CORRECT: feat(core-resource-broker): add resource broker for rate limiting and queueing
658
+ Reason: New package = new functionality = feat (even if many files)
659
+
660
+ Example 3: Bulk move (many added files but not new) \u2192 refactor, NOT feat
661
+ Files: 100 files with status "added" but isNewFile: false
662
+ - packages/analytics/core/file1.ts (added, isNewFile: false)
663
+ - packages/analytics/core/file2.ts (added, isNewFile: false)
664
+ - ... (98 more files, all moved from elsewhere)
665
+ \u274C WRONG: feat(analytics): add analytics packages
666
+ \u2705 CORRECT: refactor(analytics): reorganize analytics package structure
667
+ Reason: isNewFile: false means files existed before, just moved/reorganized
668
+
669
+ Example 4: All deleted files \u2192 chore, NOT feat
670
+ Files: 22 files, all with status "deleted"
671
+ - packages/analytics/test1.spec.ts (deleted, +0/-1579)
672
+ - packages/analytics/test2.spec.ts (deleted, +0/-856)
673
+ - ... (20 more deleted files)
674
+ \u274C WRONG: feat(analytics): add analytics functionality
675
+ \u2705 CORRECT: chore(analytics): remove unused test files
676
+ Reason: Deleting files is cleanup (chore), not new feature
677
+
678
+ Example 5: True new feature (genuinely new files) \u2192 feat
679
+ Files: 5 new files with isNewFile: true
680
+ - src/auth/jwt-strategy.ts (added, +200/-0, isNewFile: true)
681
+ - src/auth/auth-middleware.ts (added, +150/-0, isNewFile: true)
682
+ - ... (3 more new files)
683
+ \u2705 CORRECT: feat(auth): add JWT authentication
684
+ Reason: New functionality, truly new files, implements new capability
685
+
686
+ Example 6: Documentation ONLY \u2192 docs, NOT chore or feat
687
+ Files:
688
+ - README.md (modified, +50/-20, isNewFile: false)
689
+ - CONTRIBUTING.md (modified, +30/-10, isNewFile: false)
690
+ - docs/api.md (modified, +100/-50, isNewFile: false)
691
+ \u274C WRONG: chore(docs): update documentation files
692
+ \u274C WRONG: feat(docs): add documentation
693
+ \u2705 CORRECT: docs: improve README and API documentation
694
+ Reason: ALL files are markdown = docs (regardless of IsNewFile)
695
+
696
+ Example 6b: NEW documentation files \u2192 docs, NOT feat
697
+ Files:
698
+ - docs/benchmarks/README.md (added, +200/-0, isNewFile: true)
699
+ - docs/benchmarks/RESULTS.md (added, +100/-0, isNewFile: true)
700
+ \u274C WRONG: feat(docs): add benchmarks documentation
701
+ \u2705 CORRECT: docs(benchmarks): add benchmarks documentation
702
+ Reason: ALL files are .md = docs type (even if IsNewFile: true)
703
+
704
+ Example 7: Bug fix with error handling \u2192 fix, NOT refactor
705
+ Files:
706
+ - src/api/auth.ts (modified, +15/-5, isNewFile: false)
707
+ Diff shows: Added try-catch, null check for token validation
708
+ \u274C WRONG: refactor(api): update auth token validation
709
+ \u2705 CORRECT: fix(api): handle null token in authentication
710
+ Reason: Adds error handling to prevent crash = bug fix
711
+
712
+ Example 8: Test files ONLY \u2192 test, NOT chore
713
+ Files:
714
+ - tests/auth.test.ts (added, +200/-0, isNewFile: true)
715
+ - tests/fixtures/users.json (added, +50/-0, isNewFile: true)
716
+ \u274C WRONG: chore(tests): add test files
717
+ \u2705 CORRECT: test(auth): add authentication test suite
718
+ Reason: Test files only = test type
719
+ `;
720
+ var SYSTEM_PROMPT_WITH_DIFF = `You are a git commit message generator. You now have the actual diff content for better context.
721
+
722
+ CRITICAL OUTPUT FORMAT:
723
+ - Return ONLY a valid JSON object
724
+ - Do NOT wrap in markdown code blocks (no \`\`\`json, no \`\`\`)
725
+ - Do NOT add any text before or after the JSON
726
+ - Ensure all strings are properly escaped (use double quotes, escape backslashes and quotes)
727
+
728
+ CRITICAL: USE IsNewFile METADATA TO DISTINGUISH NEW vs MODIFIED FILES:
729
+ - Each file includes "IsNewFile: true" or "IsNewFile: false"
730
+ - IsNewFile: false \u2192 File EXISTED BEFORE in git history \u2192 Use "refactor", "fix", or "chore"
731
+ - IsNewFile: true \u2192 File is TRULY NEW (never existed) \u2192 Use "feat" for new functionality
732
+ - NEVER use "feat: add initial" for files with "IsNewFile: false" - these are modifications!
733
+ - For files marked "[EXISTING FILE - was modified]" in diff section \u2192 Use refactor/fix/chore, NOT feat
734
+
735
+ CRITICAL GROUPING RULES:
736
+ - Group files by LOGICAL CHANGE based on diff content
737
+ - If files are changed for the same reason, they belong in ONE commit
738
+ - IMPORTANT: Most files with status "modified" are REFACTORING, not new features
739
+ - For refactoring: analyze the diff to understand what changed (renamed variables, restructured code, etc.)
740
+ - Only use "add initial" or "setup" messages if you see truly NEW functionality being created from scratch
741
+ - Target: 3-8 commits for <50 files, 5-12 commits for 50-150 files, 10-20 commits for 150+ files
742
+ - Only separate genuinely DIFFERENT changes
743
+ - CRITICAL: Each file must appear in EXACTLY ONE commit - no duplicates across commits!
744
+
745
+ CRITICAL: FILE TYPE SHORTCUTS (check FIRST, before other rules):
746
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
747
+
748
+ \u{1F6AB} STOP! Check file extensions in DIFF BEFORE asking questions below:
749
+
750
+ \u{1F4DD} ALL files in diff are *.md or *.mdx? \u2192 Type: docs (SKIP all questions below!)
751
+ \u{1F9EA} ALL files in diff are test/, __tests__/, *.test.ts, *.spec.ts? \u2192 Type: test
752
+ \u274C If ANY file has code (.ts, .js, .tsx, .jsx, .py, etc.) \u2192 Continue to questions below
753
+
754
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
755
+
756
+ CRITICAL COMMIT TYPE CLASSIFICATION (same as Phase 1):
757
+ For EACH commit, answer these questions using the DIFF content:
758
+
759
+ 1. Does this change add NEW USER-VISIBLE BEHAVIOR?
760
+ - Look at the diff: is there a new API, feature, or capability?
761
+ - Check IsNewFile: false = likely refactor, true = might be feat
762
+ \u2192 YES = feat
763
+
764
+ 2. Does this change fix BROKEN functionality?
765
+ - Look for bug fixes, error handling corrections
766
+ \u2192 YES = fix
767
+
768
+ 3. Is this ONLY INTERNAL restructuring?
769
+ - Renaming, extracting functions, reorganizing code?
770
+ - IsNewFile: false with balanced +/- = refactor
771
+ \u2192 YES = refactor
772
+
773
+ DEFAULT BIAS: When uncertain between feat and refactor, choose refactor!
774
+
775
+ Rules:
776
+
777
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
778
+ !!! RULE 0 (HIGHEST PRIORITY - OVERRIDES ALL OTHER RULES): !!!
779
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
780
+ !!!
781
+ !!! IF a file has "IsNewFile: false" in its metadata, IT IS A MODIFIED FILE.
782
+ !!! Modified files CANNOT be feat UNLESS they add ENTIRELY NEW user-facing APIs.
783
+ !!!
784
+ !!! BEFORE classifying ANY commit as "feat", CHECK ALL files' IsNewFile flags:
785
+ !!! - If ANY file has IsNewFile: false \u2192 START with refactor/fix/chore
786
+ !!! - If ALL files have IsNewFile: true \u2192 MIGHT be feat (check diff content)
787
+ !!!
788
+ !!! EXAMPLES OF MODIFIED FILES (IsNewFile: false) \u2192 NOT FEAT:
789
+ !!! \u274C WRONG: Modified file adds new function \u2192 feat
790
+ !!! \u2705 RIGHT: Modified file adds new function \u2192 refactor
791
+ !!!
792
+ !!! \u274C WRONG: Modified file adds new class \u2192 feat
793
+ !!! \u2705 RIGHT: Modified file adds new class \u2192 refactor
794
+ !!!
795
+ !!! \u274C WRONG: Modified file adds new CLI command \u2192 feat
796
+ !!! \u2705 RIGHT: Modified file adds new CLI command \u2192 refactor
797
+ !!!
798
+ !!! ONLY USE FEAT for modified files if:
799
+ !!! - Diff shows COMPLETELY NEW public API endpoint (e.g., POST /api/new-resource)
800
+ !!! - Diff shows COMPLETELY NEW product feature visible to end users
801
+ !!!
802
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
803
+
804
+ 1. Use conventional commits: feat, fix, refactor, chore, docs, test, build, ci, perf
805
+ 2. Group related files - number of commits should scale with file count (see grouping rules above)
806
+ 3. Each commit MUST include "reasoning" field based on actual diff content
807
+ 4. Message should be lowercase, imperative mood, no period at end
808
+ 5. breaking: true only for breaking API changes
809
+ 6. Add "body" with bullet points explaining the actual changes you see in the diff
810
+ 7. Scope should reflect the affected area (e.g., "cli", "api"), not individual files
811
+ 8. CRITICAL: If ALL files in a commit are being DELETED (only deletions in diff), use type "chore" or "refactor", NOT "feat"
812
+ 9. CRITICAL: If a commit is mostly deletions (>80% of lines are deletions), use "refactor" or "chore", NOT "feat"
813
+
814
+ 10. CRITICAL: WRITE INFORMATIVE COMMIT MESSAGES (look at diff content):
815
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
816
+
817
+ \u274C BAD (too generic):
818
+ - "add feature"
819
+ - "update files"
820
+ - "refactor code"
821
+ - "fix bug"
822
+ - "improve implementation"
823
+
824
+ \u2705 GOOD (specific, based on diff):
825
+ - "add rate limiting middleware with Redis backend"
826
+ - "update API client to support pagination parameters"
827
+ - "refactor workflow executor to use async/await pattern"
828
+ - "fix memory leak in event listener cleanup"
829
+ - "improve error handling in authentication flow"
830
+
831
+ Guidelines:
832
+ - Read the DIFF to understand WHAT changed
833
+ - Include specific component/module names from diff
834
+ - Mention the technology/pattern if relevant (Redis, JWT, factory pattern)
835
+ - For body: describe concrete changes, not just "update X file"
836
+ - Use technical terms that developers will understand
837
+
838
+ 12. CRITICAL: SPECIFIC TYPE DETECTION (check diff content BEFORE defaulting to refactor):
839
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
840
+
841
+ \u{1F4DD} docs: Documentation ONLY (no code logic changes)
842
+ \u2705 ALL files in diff are markdown (*.md, *.mdx) \u2192 docs (even if IsNewFile: true)
843
+ \u2705 Diff shows ONLY markdown content changes
844
+ \u2705 README.md, ADR files, API docs, benchmarks docs
845
+ \u2705 Files in docs/ or doc/ directory with only markdown
846
+ \u274C NOT docs if diff includes ANY code logic changes
847
+
848
+ **IMPORTANT**: If ALL files end with .md or .mdx \u2192 ALWAYS use docs, NEVER feat!
849
+
850
+ \u{1F41B} fix: Corrects BROKEN functionality (look for bug-fix patterns in diff)
851
+ \u2705 Diff adds try-catch, null checks, validation
852
+ \u2705 Diff fixes incorrect calculations or logic errors
853
+ \u2705 Diff corrects typos in user-facing strings
854
+ \u2705 Commit message/body mentions "fix", "bug", "error", "crash"
855
+ \u274C NOT fix if adding new behavior
856
+
857
+ \u{1F9EA} test: Test files ONLY
858
+ \u2705 Diff shows files in test/, __tests__/, *.test.ts, *.spec.ts
859
+ \u2705 Adding test cases, updating test fixtures
860
+ \u274C NOT test if diff also changes source code
861
+
862
+ 13. IsNewFile flag (see RULE 0 above for detailed logic):
863
+ - IsNewFile: false \u2192 refactor/fix/chore/docs/test (NOT feat)
864
+ - IsNewFile: true \u2192 might be feat (check diff)
865
+
866
+ EXACT JSON SCHEMA (copy this structure):
867
+ {
868
+ "commits": [
869
+ {
870
+ "id": "c1",
871
+ "type": "refactor",
872
+ "scope": "workflow",
873
+ "message": "extract job executor to separate class with dependency injection",
874
+ "body": "- create JobExecutor class with injectable dependencies\\n- move execution logic from runtime to executor\\n- add unit tests for isolated executor behavior",
875
+ "files": ["src/runtime/workflow-runtime.ts", "src/executor/job-executor.ts", "tests/executor.test.ts"],
876
+ "releaseHint": "patch",
877
+ "breaking": false,
878
+ "reasoning": {
879
+ "newBehavior": false,
880
+ "fixesBug": false,
881
+ "internalOnly": true,
882
+ "explanation": "Diff shows extraction of JobExecutor class from runtime - improves testability through dependency injection pattern, no user-facing changes",
883
+ "confidence": 0.95
884
+ }
885
+ }
886
+ ]
887
+ }
888
+
889
+ VALID TYPE VALUES: feat, fix, refactor, chore, docs, test, build, ci, perf
890
+ VALID RELEASEHINT VALUES: none, patch, minor, major
891
+
892
+ EXAMPLE OUTPUT (based on actual diff content):
893
+ {
894
+ "commits": [
895
+ {
896
+ "id": "c1",
897
+ "type": "refactor",
898
+ "scope": "cli",
899
+ "message": "migrate command routing to builder pattern with fluent API",
900
+ "body": "- replace imperative routing with CommandRouterBuilder\\n- add fluent API for route registration (addRoute, withMiddleware)\\n- extract plugin discovery to PluginDiscoveryService class\\n- update integration tests for new routing pattern",
901
+ "files": ["src/commands/routing.ts", "src/commands/router-builder.ts", "src/plugins/discovery-service.ts", "tests/integration/routing.test.ts"],
902
+ "releaseHint": "patch",
903
+ "breaking": false,
904
+ "reasoning": {
905
+ "newBehavior": false,
906
+ "fixesBug": false,
907
+ "internalOnly": true,
908
+ "explanation": "IsNewFile: false for routing.ts (modified). Diff shows refactoring to builder pattern - improves code organization and testability without changing CLI behavior",
909
+ "confidence": 0.95
910
+ }
911
+ }
912
+ ]
913
+ }
914
+
915
+ REAL-WORLD EXAMPLES WITH DIFF CONTEXT (learn from these):
916
+
917
+ Example 1: Modified files - use diff to determine if feat or refactor
918
+ Diff shows:
919
+ - Renamed variables (oldName \u2192 newName)
920
+ - Restructured functions (extract to separate modules)
921
+ - Updated imports and exports
922
+ \u274C WRONG: feat(core): add new commit plan logic
923
+ \u2705 CORRECT: refactor(core): restructure commit plan and prompt generation
924
+ Reason: Diff shows reorganization, not new functionality
925
+
926
+ Example 2: New package - check IsNewFile flags in diff
927
+ All files show: IsNewFile: true, implements resource broker from scratch
928
+ \u2705 CORRECT: feat(core-resource-broker): add resource broker for rate limiting
929
+ Reason: Genuinely new package with new functionality
930
+
931
+ Example 3: Moved files - IsNewFile: false despite status "added"
932
+ Diff shows: [EXISTING FILE - was modified], same content as before
933
+ \u2705 CORRECT: refactor(analytics): reorganize analytics package structure
934
+ Reason: Files moved/reorganized, not newly created
935
+
936
+ Example 4: Bug fix in existing files
937
+ Diff shows: Fix null pointer exception, add validation check
938
+ \u2705 CORRECT: fix(auth): handle null token in authentication middleware
939
+ Reason: Fixing broken functionality = fix, not feat
940
+
941
+ Example 5: Documentation ONLY \u2192 docs, NOT chore or feat
942
+ Files:
943
+ - README.md (modified, +50/-20, IsNewFile: false)
944
+ - docs/API.md (modified, +30/-10, IsNewFile: false)
945
+ Diff shows: Updated markdown content, improved examples, no code changes
946
+ \u274C WRONG: chore(docs): update documentation
947
+ \u274C WRONG: feat(docs): add documentation
948
+ \u2705 CORRECT: docs: improve README and API documentation
949
+ Reason: ALL files are markdown = docs (regardless of IsNewFile)
950
+
951
+ Example 5b: NEW documentation files \u2192 docs, NOT feat
952
+ Files:
953
+ - docs/benchmarks/README.md (added, +200/-0, IsNewFile: true)
954
+ - docs/benchmarks/RESULTS.md (added, +100/-0, IsNewFile: true)
955
+ Diff shows: New markdown files with benchmarks documentation
956
+ \u274C WRONG: feat(docs): add benchmarks documentation
957
+ \u2705 CORRECT: docs(benchmarks): add benchmarks documentation
958
+ Reason: ALL files are .md = docs type (even if IsNewFile: true)
959
+
960
+ Example 6: Bug fix with error handling \u2192 fix, NOT refactor
961
+ Files:
962
+ - src/api/auth.ts (modified, +15/-5, IsNewFile: false)
963
+ Diff shows: Added try-catch around token validation, null check before access
964
+ \u2705 CORRECT: fix(api): handle null token in authentication
965
+ Reason: Adds error handling to prevent crash = bug fix
966
+
967
+ Example 7: Test files ONLY \u2192 test, NOT chore
968
+ Files:
969
+ - tests/auth.test.ts (added, +200/-0, IsNewFile: true)
970
+ - tests/helpers/mock-data.ts (added, +50/-0, IsNewFile: true)
971
+ Diff shows: New test suites for authentication module
972
+ \u2705 CORRECT: test(auth): add authentication test suite
973
+ Reason: Test files only = test type
974
+ `;
975
+ function buildPrompt(summaries, recentCommits) {
976
+ const fileList = summaries.map((s) => {
977
+ const stats = s.binary ? "binary" : `+${s.additions}/-${s.deletions}`;
978
+ const isNew = s.isNewFile ? "IsNewFile: true" : "IsNewFile: false";
979
+ return `- ${s.path} (${s.status}, ${stats}, ${isNew})`;
980
+ }).join("\n");
981
+ const styleHint = recentCommits.length > 0 ? `
982
+ Recent commit style:
983
+ ${recentCommits.slice(0, 5).map((c) => `- "${c}"`).join("\n")}` : "";
984
+ return `Files changed:
985
+ ${fileList}
986
+ ${styleHint}
987
+
988
+ Generate commit plan as JSON. If you're unsure about commit type/message from paths alone, set needsMoreContext: true and list files in requestedFiles.`;
989
+ }
990
+ function buildEnhancedPrompt(summaries, patternAnalysis, recentCommits) {
991
+ const fileList = summaries.map((s) => {
992
+ const stats = s.binary ? "binary" : `+${s.additions}/-${s.deletions}`;
993
+ const isNew = s.isNewFile ? "IsNewFile: true" : "IsNewFile: false";
994
+ return `- ${s.path} (${s.status}, ${stats}, ${isNew})`;
995
+ }).join("\n");
996
+ const styleHint = recentCommits.length > 0 ? `
997
+ Recent commit style:
998
+ ${recentCommits.slice(0, 5).map((c) => `- "${c}"`).join("\n")}` : "";
999
+ const patternHint = patternAnalysis.confidence > 0.7 ? `
1000
+
1001
+ \u{1F3AF} PATTERN DETECTED (confidence: ${(patternAnalysis.confidence * 100).toFixed(0)}%):
1002
+ Pattern type: ${patternAnalysis.patternType}
1003
+ Suggested commit type: ${patternAnalysis.suggestedType || "unknown"}
1004
+
1005
+ Hints:
1006
+ ${patternAnalysis.hints.map((h) => ` \u2022 ${h}`).join("\n")}
1007
+
1008
+ \u26A0\uFE0F IMPORTANT: Consider this pattern analysis when determining commit types!` : "";
1009
+ return `Files changed:
1010
+ ${fileList}
1011
+ ${styleHint}
1012
+ ${patternHint}
1013
+
1014
+ Generate commit plan as JSON. If you're unsure about commit type/message from paths alone, set needsMoreContext: true and list files in requestedFiles.`;
1015
+ }
1016
+ function buildPromptWithDiff(summaries, diffs, recentCommits) {
1017
+ const fileList = summaries.map((s) => {
1018
+ const stats = s.binary ? "binary" : `+${s.additions}/-${s.deletions}`;
1019
+ const isNew = s.isNewFile ? "IsNewFile: true" : "IsNewFile: false";
1020
+ return `- ${s.path} (${s.status}, ${stats}, ${isNew})`;
1021
+ }).join("\n");
1022
+ const diffContent = Array.from(diffs.entries()).map(([path, diff]) => {
1023
+ const summary = summaries.find((s) => s.path === path);
1024
+ const isNewLabel = summary?.isNewFile ? " [NEW FILE - never existed before]" : " [EXISTING FILE - was modified]";
1025
+ const truncatedDiff = diff.length > 2e3 ? diff.slice(0, 2e3) + "\n... (truncated)" : diff;
1026
+ return `### ${path}${isNewLabel}
1027
+ \`\`\`diff
1028
+ ${truncatedDiff}
1029
+ \`\`\``;
1030
+ }).join("\n\n");
1031
+ const styleHint = recentCommits.length > 0 ? `
1032
+ Recent commit style:
1033
+ ${recentCommits.slice(0, 5).map((c) => `- "${c}"`).join("\n")}` : "";
1034
+ return `Files changed:
1035
+ ${fileList}
1036
+ ${styleHint}
1037
+
1038
+ Diff content for requested files:
1039
+ ${diffContent}
1040
+
1041
+ Now generate accurate commit plan based on the actual changes you see:`;
1042
+ }
1043
+ function cleanJsonResponse(rawResponse) {
1044
+ let cleaned = rawResponse.trim();
1045
+ cleaned = cleaned.replace(/^```(?:json)?\s*/i, "");
1046
+ cleaned = cleaned.replace(/\s*```$/i, "");
1047
+ cleaned = cleaned.trim();
1048
+ const jsonStart = cleaned.indexOf("{");
1049
+ const jsonEnd = cleaned.lastIndexOf("}");
1050
+ if (jsonStart !== -1 && jsonEnd !== -1 && jsonStart < jsonEnd) {
1051
+ cleaned = cleaned.substring(jsonStart, jsonEnd + 1);
1052
+ }
1053
+ cleaned = cleaned.replace(/,(\s*[}\]])/g, "$1");
1054
+ return cleaned;
1055
+ }
1056
+ function parseResponse(response, summaries, patternAnalysis) {
1057
+ const cleaned = cleanJsonResponse(response);
1058
+ let parsed;
1059
+ try {
1060
+ parsed = JSON.parse(cleaned);
1061
+ } catch (error) {
1062
+ const preview = cleaned.substring(0, 300).replace(/\n/g, " ");
1063
+ throw new Error(
1064
+ `Failed to parse LLM response as JSON. Preview: "${preview}${cleaned.length > 300 ? "..." : ""}" Error: ${error instanceof Error ? error.message : String(error)}`
1065
+ );
1066
+ }
1067
+ if (!parsed.commits || !Array.isArray(parsed.commits)) {
1068
+ throw new Error(
1069
+ `LLM response missing "commits" array. Got: ${JSON.stringify(parsed).substring(0, 200)}`
1070
+ );
1071
+ }
1072
+ if (parsed.commits.length === 0) {
1073
+ if (parsed.needsMoreContext) {
1074
+ return {
1075
+ needsMoreContext: true,
1076
+ requestedFiles: Array.isArray(parsed.requestedFiles) ? parsed.requestedFiles : [],
1077
+ commits: [],
1078
+ averageConfidence: 0
1079
+ };
1080
+ }
1081
+ throw new Error("LLM response has empty commits array and needsMoreContext is not set");
1082
+ }
1083
+ const commits = parsed.commits.map((commit, index) => {
1084
+ if (!commit.type) {
1085
+ throw new Error(`Commit ${index + 1} missing required field "type"`);
1086
+ }
1087
+ if (!commit.files || !Array.isArray(commit.files)) {
1088
+ throw new Error(`Commit ${index + 1} missing required field "files" array`);
1089
+ }
1090
+ if (commit.files.length === 0) {
1091
+ throw new Error(`Commit ${index + 1} has empty files array`);
1092
+ }
1093
+ if (!commit.message) {
1094
+ throw new Error(`Commit ${index + 1} missing required field "message"`);
1095
+ }
1096
+ const type = normalizeType(commit.type);
1097
+ const files = commit.files;
1098
+ const reasoning = commit.reasoning;
1099
+ let parsedReasoning;
1100
+ let confidence = 0.5;
1101
+ if (reasoning && typeof reasoning === "object") {
1102
+ confidence = typeof reasoning.confidence === "number" ? reasoning.confidence : 0.5;
1103
+ parsedReasoning = {
1104
+ newBehavior: Boolean(reasoning.newBehavior),
1105
+ fixesBug: Boolean(reasoning.fixesBug),
1106
+ internalOnly: Boolean(reasoning.internalOnly),
1107
+ explanation: typeof reasoning.explanation === "string" ? reasoning.explanation : "No explanation provided",
1108
+ confidence
1109
+ };
1110
+ } else {
1111
+ confidence = typeof commit.confidence === "number" ? commit.confidence : 0.5;
1112
+ }
1113
+ return {
1114
+ id: commit.id || `c${index + 1}`,
1115
+ type,
1116
+ scope: typeof commit.scope === "string" ? commit.scope : void 0,
1117
+ message: typeof commit.message === "string" ? commit.message : "update files",
1118
+ body: typeof commit.body === "string" ? commit.body : void 0,
1119
+ files,
1120
+ releaseHint: normalizeReleaseHint(commit.releaseHint),
1121
+ breaking: Boolean(commit.breaking),
1122
+ reasoning: parsedReasoning,
1123
+ confidence
1124
+ // keep for internal use
1125
+ };
1126
+ });
1127
+ const totalConfidence = commits.reduce((sum, c) => sum + (c.confidence ?? 0.5), 0);
1128
+ const averageConfidence = commits.length > 0 ? totalConfidence / commits.length : 0;
1129
+ const commitsWithFixedTypes = summaries ? commits.map((c) => fixCommitType(c, summaries, patternAnalysis)) : commits;
1130
+ const commitsWithoutConfidence = commitsWithFixedTypes.map((c) => {
1131
+ const { confidence: _, ...commit } = c;
1132
+ return commit;
1133
+ });
1134
+ return {
1135
+ needsMoreContext: Boolean(parsed.needsMoreContext),
1136
+ requestedFiles: Array.isArray(parsed.requestedFiles) ? parsed.requestedFiles : [],
1137
+ commits: commitsWithoutConfidence,
1138
+ averageConfidence
1139
+ };
1140
+ }
1141
+ function fixCommitType(commit, summaries, patternAnalysis) {
1142
+ const commitFiles = commit.files;
1143
+ const commitSummaries = summaries.filter((s) => commitFiles.includes(s.path));
1144
+ if (commitSummaries.length === 0) {
1145
+ return commit;
1146
+ }
1147
+ if (commit.type === "feat" && commit.confidence < 0.7 && commit.reasoning) {
1148
+ const { newBehavior, internalOnly, fixesBug } = commit.reasoning;
1149
+ if (internalOnly && !newBehavior) {
1150
+ return {
1151
+ ...commit,
1152
+ type: "refactor",
1153
+ reasoning: {
1154
+ ...commit.reasoning,
1155
+ explanation: `[Conservative bias] ${commit.reasoning.explanation}. Low confidence + internalOnly \u2192 refactor`
1156
+ }
1157
+ };
1158
+ }
1159
+ if (fixesBug && !newBehavior) {
1160
+ return {
1161
+ ...commit,
1162
+ type: "fix",
1163
+ reasoning: {
1164
+ ...commit.reasoning,
1165
+ explanation: `[Conservative bias] ${commit.reasoning.explanation}. Fixes bug \u2192 fix`
1166
+ }
1167
+ };
1168
+ }
1169
+ if (!newBehavior) {
1170
+ return {
1171
+ ...commit,
1172
+ type: "refactor",
1173
+ reasoning: {
1174
+ ...commit.reasoning,
1175
+ explanation: `[Conservative bias] ${commit.reasoning.explanation}. No new behavior \u2192 refactor`
1176
+ }
1177
+ };
1178
+ }
1179
+ }
1180
+ const allDeleted = commitSummaries.every((s) => s.status === "deleted");
1181
+ if (allDeleted && commit.type === "feat") {
1182
+ return {
1183
+ ...commit,
1184
+ type: "chore",
1185
+ message: commit.message.replace(/^add /i, "remove ").replace(/^added /i, "removed ")
1186
+ };
1187
+ }
1188
+ const totalAdditions = commitSummaries.reduce((sum, s) => sum + s.additions, 0);
1189
+ const totalDeletions = commitSummaries.reduce((sum, s) => sum + s.deletions, 0);
1190
+ const totalChanges = totalAdditions + totalDeletions;
1191
+ if (totalChanges > 0) {
1192
+ const deletionRatio = totalDeletions / totalChanges;
1193
+ if (deletionRatio > 0.8 && commit.type === "feat") {
1194
+ return {
1195
+ ...commit,
1196
+ type: "refactor"
1197
+ };
1198
+ }
1199
+ }
1200
+ if (patternAnalysis && patternAnalysis.confidence > 0.8 && patternAnalysis.suggestedType) {
1201
+ const llmType = commit.type;
1202
+ const patternType = patternAnalysis.suggestedType;
1203
+ if (llmType !== patternType) {
1204
+ return {
1205
+ ...commit,
1206
+ type: patternType
1207
+ };
1208
+ }
1209
+ }
1210
+ const allModified = commitSummaries.every((s) => s.status === "modified");
1211
+ if (allModified && totalChanges > 0 && commit.type === "feat") {
1212
+ const additionRatio = totalAdditions / totalChanges;
1213
+ if (additionRatio < 0.4) {
1214
+ return {
1215
+ ...commit,
1216
+ type: "refactor"
1217
+ };
1218
+ }
1219
+ if (additionRatio < 0.6) {
1220
+ return {
1221
+ ...commit,
1222
+ type: "refactor"
1223
+ };
1224
+ }
1225
+ }
1226
+ const hasPackageJson = commitSummaries.some((s) => s.path.endsWith("package.json"));
1227
+ if (hasPackageJson && commitSummaries.length >= 10) {
1228
+ const allAdded = commitSummaries.every((s) => s.status === "added");
1229
+ const allIsNewFile = commitSummaries.every((s) => s.isNewFile === true);
1230
+ if (allAdded && allIsNewFile && commit.type === "chore") {
1231
+ return {
1232
+ ...commit,
1233
+ type: "feat",
1234
+ message: commit.message.replace(/^initialize /i, "add ").replace(/^setup /i, "add ")
1235
+ };
1236
+ }
1237
+ }
1238
+ return commit;
1239
+ }
1240
+ function normalizeType(type) {
1241
+ const validTypes = [
1242
+ "feat",
1243
+ "fix",
1244
+ "refactor",
1245
+ "chore",
1246
+ "docs",
1247
+ "test",
1248
+ "build",
1249
+ "ci",
1250
+ "perf"
1251
+ ];
1252
+ if (typeof type === "string") {
1253
+ const normalized = type.toLowerCase();
1254
+ if (validTypes.includes(normalized)) {
1255
+ return normalized;
1256
+ }
1257
+ }
1258
+ return "chore";
1259
+ }
1260
+ function normalizeReleaseHint(hint) {
1261
+ if (typeof hint === "string") {
1262
+ const normalized = hint.toLowerCase();
1263
+ if (["none", "patch", "minor", "major"].includes(normalized)) {
1264
+ return normalized;
1265
+ }
1266
+ }
1267
+ return "none";
1268
+ }
1269
+ function generateHeuristicPlan(summaries) {
1270
+ if (summaries.length === 0) {
1271
+ return [];
1272
+ }
1273
+ const commits = [];
1274
+ let commitIndex = 1;
1275
+ const { packageGroups, remainingFiles } = groupPackageJsonChanges(summaries);
1276
+ for (const group of packageGroups) {
1277
+ commits.push({
1278
+ id: `c${commitIndex++}`,
1279
+ type: "chore",
1280
+ scope: inferPackageScope(group.map((f) => f.path)),
1281
+ message: "update dependencies",
1282
+ files: group.map((f) => f.path),
1283
+ releaseHint: "none",
1284
+ breaking: false
1285
+ });
1286
+ }
1287
+ const { pairedGroups, unpairedFiles } = pairTestsWithImplementation(remainingFiles);
1288
+ for (const group of pairedGroups) {
1289
+ const implFile = group.find((f) => !isTestFile(f.path));
1290
+ const type = inferTypeFromChanges(implFile);
1291
+ commits.push({
1292
+ id: `c${commitIndex++}`,
1293
+ type,
1294
+ scope: inferScope(group.map((f) => f.path)),
1295
+ message: generateMessage(type, group),
1296
+ files: group.map((f) => f.path),
1297
+ releaseHint: inferReleaseHint(type),
1298
+ breaking: false
1299
+ });
1300
+ }
1301
+ const categoryGroups = groupByCategory(unpairedFiles);
1302
+ for (const [category, files] of categoryGroups) {
1303
+ const type = categoryToType(category);
1304
+ const scope = inferScope(files.map((f) => f.path));
1305
+ commits.push({
1306
+ id: `c${commitIndex++}`,
1307
+ type,
1308
+ scope,
1309
+ message: generateMessage(type, files),
1310
+ files: files.map((f) => f.path),
1311
+ releaseHint: inferReleaseHint(type),
1312
+ breaking: false
1313
+ });
1314
+ }
1315
+ return commits;
1316
+ }
1317
+ function groupPackageJsonChanges(summaries) {
1318
+ const packageFiles = summaries.filter((f) => f.path.endsWith("package.json"));
1319
+ const otherFiles = summaries.filter((f) => !f.path.endsWith("package.json"));
1320
+ if (packageFiles.length === 0) {
1321
+ return { packageGroups: [], remainingFiles: summaries };
1322
+ }
1323
+ const packageGroups = [];
1324
+ const claimed = /* @__PURE__ */ new Set();
1325
+ for (const pkgFile of packageFiles) {
1326
+ const pkgDir = dirname(pkgFile.path);
1327
+ const related = otherFiles.filter((f) => {
1328
+ const fileDir = dirname(f.path);
1329
+ return fileDir === pkgDir || fileDir.startsWith(pkgDir + "/");
1330
+ });
1331
+ const configRelated = related.filter((f) => {
1332
+ const name = basename(f.path);
1333
+ return name.includes("config") || name.includes("tsconfig") || name.endsWith(".json") || name.startsWith(".");
1334
+ });
1335
+ const group = [pkgFile, ...configRelated];
1336
+ packageGroups.push(group);
1337
+ claimed.add(pkgFile);
1338
+ configRelated.forEach((f) => claimed.add(f));
1339
+ }
1340
+ const remainingFiles = summaries.filter((f) => !claimed.has(f));
1341
+ return { packageGroups, remainingFiles };
1342
+ }
1343
+ function pairTestsWithImplementation(summaries) {
1344
+ const testFiles = summaries.filter((f) => isTestFile(f.path));
1345
+ const implFiles = summaries.filter((f) => !isTestFile(f.path));
1346
+ const pairedGroups = [];
1347
+ const pairedImpls = /* @__PURE__ */ new Set();
1348
+ const pairedTests = /* @__PURE__ */ new Set();
1349
+ for (const testFile of testFiles) {
1350
+ const implPath = getImplementationPath(testFile.path);
1351
+ const implFile = implFiles.find((f) => f.path === implPath);
1352
+ if (implFile) {
1353
+ pairedGroups.push([implFile, testFile]);
1354
+ pairedImpls.add(implFile);
1355
+ pairedTests.add(testFile);
1356
+ }
1357
+ }
1358
+ const unpairedFiles = [
1359
+ ...implFiles.filter((f) => !pairedImpls.has(f)),
1360
+ ...testFiles.filter((f) => !pairedTests.has(f))
1361
+ ];
1362
+ return { pairedGroups, unpairedFiles };
1363
+ }
1364
+ function isTestFile(path) {
1365
+ return path.includes(".test.") || path.includes(".spec.") || path.includes("/__tests__/");
1366
+ }
1367
+ function getImplementationPath(testPath) {
1368
+ let implPath = testPath.replace(/\.test\.(ts|tsx|js|jsx)$/, ".$1").replace(/\.spec\.(ts|tsx|js|jsx)$/, ".$1");
1369
+ implPath = implPath.replace("/__tests__/", "/");
1370
+ return implPath;
1371
+ }
1372
+ function groupByCategory(summaries) {
1373
+ const groups = /* @__PURE__ */ new Map();
1374
+ for (const summary of summaries) {
1375
+ const category = categorizeFile(summary.path);
1376
+ const existing = groups.get(category) || [];
1377
+ existing.push(summary);
1378
+ groups.set(category, existing);
1379
+ }
1380
+ return groups;
1381
+ }
1382
+ function inferPackageScope(paths) {
1383
+ const packagePath = paths.find((p) => p.endsWith("package.json"));
1384
+ if (!packagePath) {
1385
+ return void 0;
1386
+ }
1387
+ const parts = packagePath.split("/");
1388
+ if (parts.length > 1) {
1389
+ return parts[parts.length - 2];
1390
+ }
1391
+ return void 0;
1392
+ }
1393
+ function inferTypeFromChanges(file) {
1394
+ if (!file) {
1395
+ return "chore";
1396
+ }
1397
+ const { additions, deletions, status } = file;
1398
+ if (status === "added" || additions > 0 && deletions === 0) {
1399
+ return "feat";
1400
+ }
1401
+ if (status === "deleted" || deletions > 0 && additions === 0) {
1402
+ return "chore";
1403
+ }
1404
+ if (additions > deletions * 2) {
1405
+ return "feat";
1406
+ }
1407
+ if (deletions > additions * 2) {
1408
+ return "refactor";
1409
+ }
1410
+ return "refactor";
1411
+ }
1412
+ function categorizeFile(path) {
1413
+ const ext = extname(path);
1414
+ const name = basename(path);
1415
+ const dir = dirname(path);
1416
+ if (path.includes(".test.") || path.includes(".spec.") || path.includes("__tests__") || dir.includes("/test/") || dir.includes("/tests/")) {
1417
+ return "test";
1418
+ }
1419
+ if (ext === ".md" || dir.includes("/docs/") || name === "README.md") {
1420
+ return "docs";
1421
+ }
1422
+ if (name.startsWith(".") || name.includes("config") || ["package.json", "tsconfig.json", "eslint.config.js"].includes(name)) {
1423
+ return "config";
1424
+ }
1425
+ if (dir.includes(".github") || dir.includes(".gitlab") || name.includes("ci")) {
1426
+ return "ci";
1427
+ }
1428
+ if (dir.includes("/build/") || dir.includes("/dist/") || name.includes("build")) {
1429
+ return "build";
1430
+ }
1431
+ const topDir = dir.split("/")[0] || "root";
1432
+ return `src:${topDir}`;
1433
+ }
1434
+ function categoryToType(category) {
1435
+ if (category === "test") {
1436
+ return "test";
1437
+ }
1438
+ if (category === "docs") {
1439
+ return "docs";
1440
+ }
1441
+ if (category === "config") {
1442
+ return "chore";
1443
+ }
1444
+ if (category === "ci") {
1445
+ return "ci";
1446
+ }
1447
+ if (category === "build") {
1448
+ return "build";
1449
+ }
1450
+ return "chore";
1451
+ }
1452
+ function inferScope(paths) {
1453
+ if (paths.length === 0) {
1454
+ return void 0;
1455
+ }
1456
+ const dirs = paths.map((p) => dirname(p).split("/"));
1457
+ const firstDir = dirs[0];
1458
+ if (!firstDir) {
1459
+ return void 0;
1460
+ }
1461
+ if (dirs.length === 1) {
1462
+ return firstDir.length > 1 ? firstDir[1] : firstDir[0];
1463
+ }
1464
+ const commonPrefix = [];
1465
+ const minLength = Math.min(...dirs.map((d) => d.length));
1466
+ for (let i = 0; i < minLength; i++) {
1467
+ const segment = firstDir[i];
1468
+ if (segment && dirs.every((d) => d[i] === segment)) {
1469
+ commonPrefix.push(segment);
1470
+ } else {
1471
+ break;
1472
+ }
1473
+ }
1474
+ if (commonPrefix.length > 0) {
1475
+ const scope = commonPrefix[commonPrefix.length - 1];
1476
+ if (scope && scope !== "." && scope !== "src") {
1477
+ return scope;
1478
+ }
1479
+ }
1480
+ return void 0;
1481
+ }
1482
+ function generateMessage(type, files) {
1483
+ const count = files.length;
1484
+ const allAdded = files.every((f) => f.status === "added");
1485
+ const allDeleted = files.every((f) => f.status === "deleted");
1486
+ if (type === "test") {
1487
+ if (allAdded) {
1488
+ return `add ${count} test file${count > 1 ? "s" : ""}`;
1489
+ }
1490
+ return `update ${count} test file${count > 1 ? "s" : ""}`;
1491
+ }
1492
+ if (type === "docs") {
1493
+ if (allAdded) {
1494
+ return `add documentation`;
1495
+ }
1496
+ return `update documentation`;
1497
+ }
1498
+ if (type === "ci") {
1499
+ return `update ci configuration`;
1500
+ }
1501
+ if (type === "build") {
1502
+ return `update build configuration`;
1503
+ }
1504
+ if (type === "chore") {
1505
+ if (files.some((f) => f.path.includes("package.json"))) {
1506
+ return `update dependencies`;
1507
+ }
1508
+ return `update configuration`;
1509
+ }
1510
+ if (allAdded) {
1511
+ return `add ${count} file${count > 1 ? "s" : ""}`;
1512
+ }
1513
+ if (allDeleted) {
1514
+ return `remove ${count} file${count > 1 ? "s" : ""}`;
1515
+ }
1516
+ return `update ${count} file${count > 1 ? "s" : ""}`;
1517
+ }
1518
+ function inferReleaseHint(type, _files) {
1519
+ switch (type) {
1520
+ case "feat":
1521
+ return "minor";
1522
+ case "fix":
1523
+ return "patch";
1524
+ case "perf":
1525
+ return "patch";
1526
+ case "refactor":
1527
+ return "patch";
1528
+ default:
1529
+ return "none";
1530
+ }
1531
+ }
1532
+
1533
+ // src/generator/pattern-detector.ts
1534
+ function analyzePatterns(summaries) {
1535
+ if (summaries.length === 0) {
1536
+ return {
1537
+ patternType: "mixed",
1538
+ confidence: 0,
1539
+ hints: [],
1540
+ suggestedType: null
1541
+ };
1542
+ }
1543
+ if (isNewPackagePattern(summaries)) {
1544
+ const packagePath = summaries.find((s) => s.path.endsWith("package.json"))?.path;
1545
+ const packageName = packagePath ? extractPackageName(packagePath) : "unknown";
1546
+ return {
1547
+ patternType: "new-package",
1548
+ confidence: 0.95,
1549
+ hints: [
1550
+ `New package detected: ${packageName}`,
1551
+ `${summaries.length} new files including package.json`,
1552
+ "All files are truly new (isNewFile: true)",
1553
+ "This is a new feature (feat), not chore"
1554
+ ],
1555
+ suggestedType: "feat"
1556
+ };
1557
+ }
1558
+ if (isBulkMovePattern(summaries)) {
1559
+ const dirs = countUniqueDirs(summaries, 3);
1560
+ return {
1561
+ patternType: "refactor-move",
1562
+ confidence: 0.9,
1563
+ hints: [
1564
+ `Bulk move pattern: ${summaries.length} files added`,
1565
+ `Files existed before (isNewFile: false)`,
1566
+ `Organized into ${dirs} director${dirs === 1 ? "y" : "ies"}`,
1567
+ "This is refactoring (reorganization), not new feature"
1568
+ ],
1569
+ suggestedType: "refactor"
1570
+ };
1571
+ }
1572
+ if (isRefactorModificationPattern(summaries)) {
1573
+ const ratio = calculateAdditionRatio(summaries);
1574
+ return {
1575
+ patternType: "refactor-modify",
1576
+ confidence: 0.85,
1577
+ hints: [
1578
+ "All files are modified (not new)",
1579
+ `Low addition ratio: ${(ratio * 100).toFixed(0)}%`,
1580
+ "Mostly structural changes or deletions",
1581
+ "This is refactoring, not new feature"
1582
+ ],
1583
+ suggestedType: "refactor"
1584
+ };
1585
+ }
1586
+ const allDeleted = summaries.every((s) => s.status === "deleted");
1587
+ if (allDeleted) {
1588
+ return {
1589
+ patternType: "deletions",
1590
+ confidence: 0.98,
1591
+ hints: [
1592
+ "All files are deleted",
1593
+ "This is cleanup (chore), not feature"
1594
+ ],
1595
+ suggestedType: "chore"
1596
+ };
1597
+ }
1598
+ const totalAdd = summaries.reduce((sum, s) => sum + s.additions, 0);
1599
+ const totalDel = summaries.reduce((sum, s) => sum + s.deletions, 0);
1600
+ const deletionRatio = totalDel / (totalAdd + totalDel);
1601
+ if (deletionRatio > 0.8) {
1602
+ return {
1603
+ patternType: "deletions",
1604
+ confidence: 0.95,
1605
+ hints: [
1606
+ `Mostly deletions: ${(deletionRatio * 100).toFixed(0)}%`,
1607
+ "This is refactoring or cleanup, not feature"
1608
+ ],
1609
+ suggestedType: "refactor"
1610
+ };
1611
+ }
1612
+ return {
1613
+ patternType: "mixed",
1614
+ confidence: 0,
1615
+ hints: [],
1616
+ suggestedType: null
1617
+ };
1618
+ }
1619
+ function isNewPackagePattern(summaries) {
1620
+ const hasPackageJson = summaries.some((s) => s.path.endsWith("package.json"));
1621
+ if (!hasPackageJson) {
1622
+ return false;
1623
+ }
1624
+ if (summaries.length < 10) {
1625
+ return false;
1626
+ }
1627
+ const allAdded = summaries.every((s) => s.status === "added");
1628
+ if (!allAdded) {
1629
+ return false;
1630
+ }
1631
+ const allIsNewFile = summaries.every((s) => s.isNewFile === true);
1632
+ if (!allIsNewFile) {
1633
+ return false;
1634
+ }
1635
+ const packageJsonPath = summaries.find((s) => s.path.endsWith("package.json")).path;
1636
+ const packageDir = packageJsonPath.split("/").slice(0, -1).join("/");
1637
+ const filesInPackage = summaries.filter((s) => s.path.startsWith(packageDir + "/"));
1638
+ const inPackageRatio = filesInPackage.length / summaries.length;
1639
+ return inPackageRatio > 0.8;
1640
+ }
1641
+ function isBulkMovePattern(summaries) {
1642
+ if (summaries.length < 20) {
1643
+ return false;
1644
+ }
1645
+ const allAdded = summaries.every((s) => s.status === "added");
1646
+ if (!allAdded) {
1647
+ return false;
1648
+ }
1649
+ const notNewCount = summaries.filter((s) => s.isNewFile === false).length;
1650
+ const notNewRatio = notNewCount / summaries.length;
1651
+ if (notNewRatio <= 0.5) {
1652
+ return false;
1653
+ }
1654
+ const uniqueDirs = countUniqueDirs(summaries, 3);
1655
+ return uniqueDirs < 5;
1656
+ }
1657
+ function isRefactorModificationPattern(summaries) {
1658
+ const allModified = summaries.every((s) => s.status === "modified");
1659
+ if (!allModified) {
1660
+ return false;
1661
+ }
1662
+ const additionRatio = calculateAdditionRatio(summaries);
1663
+ return additionRatio < 0.4;
1664
+ }
1665
+ function countUniqueDirs(summaries, depth) {
1666
+ const dirs = /* @__PURE__ */ new Set();
1667
+ for (const summary of summaries) {
1668
+ const parts = summary.path.split("/");
1669
+ if (parts.length >= depth) {
1670
+ const dirPath = parts.slice(0, depth).join("/");
1671
+ dirs.add(dirPath);
1672
+ }
1673
+ }
1674
+ return dirs.size;
1675
+ }
1676
+ function calculateAdditionRatio(summaries) {
1677
+ const totalAdditions = summaries.reduce((sum, s) => sum + s.additions, 0);
1678
+ const totalDeletions = summaries.reduce((sum, s) => sum + s.deletions, 0);
1679
+ const totalChanges = totalAdditions + totalDeletions;
1680
+ if (totalChanges === 0) {
1681
+ return 0;
1682
+ }
1683
+ return totalAdditions / totalChanges;
1684
+ }
1685
+ function extractPackageName(path) {
1686
+ const parts = path.split("/");
1687
+ const packageJsonIndex = parts.indexOf("package.json");
1688
+ if (packageJsonIndex > 0) {
1689
+ const packageName = parts[packageJsonIndex - 1];
1690
+ return packageName ?? "unknown";
1691
+ }
1692
+ return "unknown";
1693
+ }
1694
+ var SecretsDetectedError = class _SecretsDetectedError extends Error {
1695
+ secretMatches;
1696
+ constructor(matches, message) {
1697
+ super(message);
1698
+ this.name = "SecretsDetectedError";
1699
+ this.secretMatches = matches;
1700
+ if (Error.captureStackTrace) {
1701
+ Error.captureStackTrace(this, _SecretsDetectedError);
1702
+ }
1703
+ }
1704
+ };
1705
+ var SECRET_FILE_PATTERNS = [
1706
+ // Environment files
1707
+ ".env",
1708
+ ".env.*",
1709
+ "*.env",
1710
+ ".envrc",
1711
+ // NPM/Node
1712
+ ".npmrc",
1713
+ ".yarnrc",
1714
+ ".yarnrc.yml",
1715
+ // SSH/GPG keys
1716
+ "*.key",
1717
+ "*.pem",
1718
+ "*.p12",
1719
+ "*.pfx",
1720
+ "id_rsa",
1721
+ "id_dsa",
1722
+ "id_ecdsa",
1723
+ "id_ed25519",
1724
+ "*.pub",
1725
+ // AWS
1726
+ ".aws/**",
1727
+ "credentials",
1728
+ // Docker
1729
+ ".docker/config.json",
1730
+ // Git credentials
1731
+ ".git-credentials",
1732
+ ".netrc",
1733
+ // Service account files
1734
+ "*-service-account.json",
1735
+ "*-serviceaccount.json",
1736
+ "service-account*.json",
1737
+ "serviceaccount*.json",
1738
+ // Kubernetes
1739
+ "kubeconfig",
1740
+ "*.kubeconfig",
1741
+ // Terraform
1742
+ "*.tfvars",
1743
+ "terraform.tfstate",
1744
+ "terraform.tfstate.backup",
1745
+ // Other common secrets
1746
+ "secrets.yml",
1747
+ "secrets.yaml",
1748
+ "secret.yml",
1749
+ "secret.yaml",
1750
+ "passwords.txt",
1751
+ "password.txt"
1752
+ ];
1753
+ var SECRET_CONTENT_PATTERNS = [
1754
+ // API keys/tokens
1755
+ { pattern: /api[_-]?key[s]?['":\s]*[a-zA-Z0-9_-]{20,}/i, name: "API Key" },
1756
+ { pattern: /auth[_-]?token[s]?['":\s]*[a-zA-Z0-9_-]{20,}/i, name: "Auth Token" },
1757
+ { pattern: /access[_-]?token[s]?['":\s]*[a-zA-Z0-9_-]{20,}/i, name: "Access Token" },
1758
+ // AWS
1759
+ { pattern: /AKIA[0-9A-Z]{16}/, name: "AWS Access Key ID" },
1760
+ { pattern: /aws[_-]?secret[_-]?access[_-]?key/i, name: "AWS Secret Access Key" },
1761
+ // NPM
1762
+ { pattern: /\/\/registry\.npmjs\.org\/:_authToken=/, name: "NPM Auth Token" },
1763
+ { pattern: /npm_[A-Za-z0-9]{30,}/, name: "NPM Token" },
1764
+ // GitHub
1765
+ { pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/, name: "GitHub Token" },
1766
+ // Slack
1767
+ { pattern: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[A-Za-z0-9]{24,}/, name: "Slack Token" },
1768
+ // Private keys
1769
+ { pattern: /-----BEGIN (RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY-----/, name: "Private Key" },
1770
+ // Passwords
1771
+ { pattern: /password['":\s]*['"]/i, name: "Password" },
1772
+ // Generic patterns
1773
+ { pattern: /secret['":\s]*['"]/i, name: "Secret" }
1774
+ ];
1775
+ function isSecretFile(filePath) {
1776
+ const normalizedPath = filePath.replace(/\\/g, "/");
1777
+ return SECRET_FILE_PATTERNS.some(
1778
+ (pattern) => minimatch(normalizedPath, pattern, { matchBase: true })
1779
+ );
1780
+ }
1781
+ function detectSecretFiles(files) {
1782
+ return files.filter(isSecretFile);
1783
+ }
1784
+ function formatSecretsWarning(secretFiles) {
1785
+ const count = secretFiles.length;
1786
+ const filesList = secretFiles.map((f) => ` - ${f}`).join("\n");
1787
+ return [
1788
+ `\u{1F6A8} CRITICAL SECURITY ERROR: Detected ${count} file(s) with potential secrets:`,
1789
+ filesList,
1790
+ "",
1791
+ "\u26D4\uFE0F COMMIT GENERATION ABORTED",
1792
+ "",
1793
+ "These files contain sensitive data (API keys, tokens, credentials)",
1794
+ "that MUST NOT be committed to git or sent to LLM.",
1795
+ "",
1796
+ "\u2705 Actions to fix:",
1797
+ " 1. Add these files to .gitignore",
1798
+ " 2. Remove secrets from the files (use environment variables instead)",
1799
+ " 3. If already committed, use git filter-branch or BFG to remove from history",
1800
+ "",
1801
+ "\u26A0\uFE0F If you already ran commit:generate before, the secrets may have been",
1802
+ "sent to OpenAI. Rotate your credentials immediately."
1803
+ ].join("\n");
1804
+ }
1805
+ function detectSecretsWithLocation(diffs) {
1806
+ const matches = [];
1807
+ for (const [file, diff] of diffs.entries()) {
1808
+ const lines = diff.split("\n");
1809
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
1810
+ const line = lines[lineIndex] ?? "";
1811
+ for (const { pattern, name } of SECRET_CONTENT_PATTERNS) {
1812
+ pattern.lastIndex = 0;
1813
+ const match = pattern.exec(line);
1814
+ if (match) {
1815
+ const matchedText = match[0];
1816
+ const column = match.index;
1817
+ const snippetStart = Math.max(0, column - 20);
1818
+ const snippetEnd = Math.min(line.length, column + matchedText.length + 20);
1819
+ const snippet = line.substring(snippetStart, snippetEnd);
1820
+ const displayText = matchedText.length > 50 ? matchedText.substring(0, 50) + "..." : matchedText;
1821
+ matches.push({
1822
+ file,
1823
+ line: lineIndex + 1,
1824
+ // 1-based line numbers
1825
+ column: column + 1,
1826
+ // 1-based column numbers
1827
+ pattern: pattern.source,
1828
+ patternName: name,
1829
+ snippet: snippet.trim(),
1830
+ matchedText: displayText
1831
+ });
1832
+ }
1833
+ }
1834
+ }
1835
+ }
1836
+ return matches;
1837
+ }
1838
+ function formatSecretsReport(matches) {
1839
+ const count = matches.length;
1840
+ const fileCount = new Set(matches.map((m) => m.file)).size;
1841
+ const lines = [
1842
+ `\u{1F6A8} CRITICAL SECURITY ERROR: Detected ${count} potential secret(s) in ${fileCount} file(s)`,
1843
+ "",
1844
+ "\u26D4\uFE0F COMMIT GENERATION BLOCKED",
1845
+ ""
1846
+ ];
1847
+ const byFile = /* @__PURE__ */ new Map();
1848
+ for (const match of matches) {
1849
+ const existing = byFile.get(match.file) ?? [];
1850
+ existing.push(match);
1851
+ byFile.set(match.file, existing);
1852
+ }
1853
+ for (const [file, fileMatches] of byFile.entries()) {
1854
+ lines.push(`\u{1F4C4} ${file}:`);
1855
+ for (const match of fileMatches) {
1856
+ lines.push(` Line ${match.line}:${match.column} - ${match.patternName}`);
1857
+ lines.push(` Pattern: ${match.pattern.substring(0, 60)}${match.pattern.length > 60 ? "..." : ""}`);
1858
+ lines.push(` Matched: ${match.matchedText}`);
1859
+ lines.push(` Context: ...${match.snippet}...`);
1860
+ lines.push("");
1861
+ }
1862
+ }
1863
+ lines.push(
1864
+ "\u{1F512} These files contain sensitive data (API keys, tokens, credentials)",
1865
+ "that MUST NOT be committed to git or sent to LLM.",
1866
+ "",
1867
+ "\u2705 Actions to fix:",
1868
+ " 1. Review each match above - some may be false positives (e.g., examples in comments)",
1869
+ " 2. If real secrets: Add files to .gitignore and remove secrets (use env vars)",
1870
+ " 3. If false positives: Use --allow-secrets flag to proceed with confirmation",
1871
+ "",
1872
+ "\u26A0\uFE0F If secrets were already sent to LLM in previous runs, rotate credentials immediately!"
1873
+ );
1874
+ return lines.join("\n");
1875
+ }
1876
+ async function promptUserConfirmation(question, defaultValue = false, autoConfirm = false) {
1877
+ if (autoConfirm) {
1878
+ console.log(`${question} [auto-confirmed with --yes]`);
1879
+ return true;
1880
+ }
1881
+ const rl = readline.createInterface({
1882
+ input: process.stdin,
1883
+ output: process.stdout
1884
+ });
1885
+ const defaultHint = defaultValue ? "[Y/n]" : "[y/N]";
1886
+ const fullQuestion = `${question} ${defaultHint}: `;
1887
+ return new Promise((resolve) => {
1888
+ rl.question(fullQuestion, (answer) => {
1889
+ rl.close();
1890
+ const normalized = answer.trim().toLowerCase();
1891
+ if (normalized === "") {
1892
+ resolve(defaultValue);
1893
+ return;
1894
+ }
1895
+ if (normalized === "y" || normalized === "yes") {
1896
+ resolve(true);
1897
+ return;
1898
+ }
1899
+ if (normalized === "n" || normalized === "no") {
1900
+ resolve(false);
1901
+ return;
1902
+ }
1903
+ resolve(defaultValue);
1904
+ });
1905
+ });
1906
+ }
1907
+
1908
+ // src/generator/commit-plan.ts
1909
+ var CONFIDENCE_THRESHOLD = 0.7;
1910
+ var MAX_LLM_RETRIES = 2;
1911
+ var VALID_COMMIT_TYPES = /* @__PURE__ */ new Set(["feat", "fix", "refactor", "chore", "docs", "test", "build", "ci", "perf"]);
1912
+ function normalizeCommitId(id) {
1913
+ if (!id) {
1914
+ return void 0;
1915
+ }
1916
+ const trimmed = id.trim();
1917
+ if (!trimmed) {
1918
+ return void 0;
1919
+ }
1920
+ if (/^c\d+$/i.test(trimmed)) {
1921
+ return `c${trimmed.replace(/^c/i, "")}`;
1922
+ }
1923
+ if (/^\d+$/.test(trimmed)) {
1924
+ return `c${trimmed}`;
1925
+ }
1926
+ return trimmed;
1927
+ }
1928
+ function buildFallbackMessage(files) {
1929
+ const firstFile = files[0];
1930
+ const fileName = firstFile ? firstFile.split("/").pop() ?? firstFile : "files";
1931
+ return files.length === 1 ? `update ${fileName}` : `update ${files.length} files`;
1932
+ }
1933
+ function toSafeCommitGroup(commit, id) {
1934
+ const files = Array.isArray(commit.files) ? commit.files.filter((file) => typeof file === "string" && file.length > 0) : [];
1935
+ if (files.length === 0) {
1936
+ return null;
1937
+ }
1938
+ const uniqueFiles = [...new Set(files)];
1939
+ const type = typeof commit.type === "string" && VALID_COMMIT_TYPES.has(commit.type) ? commit.type : "chore";
1940
+ const message = typeof commit.message === "string" && commit.message.trim().length > 0 ? commit.message.trim() : buildFallbackMessage(uniqueFiles);
1941
+ const releaseHint = commit.releaseHint ?? (type === "feat" ? "minor" : type === "fix" || type === "refactor" ? "patch" : "none");
1942
+ return {
1943
+ id,
1944
+ type,
1945
+ scope: commit.scope,
1946
+ message,
1947
+ body: commit.body,
1948
+ files: uniqueFiles,
1949
+ releaseHint,
1950
+ breaking: Boolean(commit.breaking),
1951
+ reasoning: commit.reasoning ?? {
1952
+ newBehavior: false,
1953
+ fixesBug: false,
1954
+ internalOnly: true,
1955
+ explanation: "Generated from Phase 3 fallback due invalid or incomplete LLM commit action.",
1956
+ confidence: 0.3
1957
+ }
1958
+ };
1959
+ }
1960
+ async function generateCommitPlan(options) {
1961
+ const { cwd, onProgress } = options;
1962
+ const logger = useLogger();
1963
+ const analytics = useAnalytics();
1964
+ const llm = useLLM();
1965
+ const startTime = Date.now();
1966
+ const gitStatus = await getGitStatus(cwd);
1967
+ const allFiles = getAllChangedFiles(gitStatus);
1968
+ if (allFiles.length === 0) {
1969
+ return createEmptyPlan(cwd, gitStatus);
1970
+ }
1971
+ const secretFiles = detectSecretFiles(allFiles);
1972
+ if (secretFiles.length > 0) {
1973
+ const basicMatches = secretFiles.map((file) => ({
1974
+ file,
1975
+ line: 0,
1976
+ column: 0,
1977
+ pattern: "SECRET_FILE_PATTERN",
1978
+ patternName: "Secret File Pattern",
1979
+ snippet: "",
1980
+ matchedText: file
1981
+ }));
1982
+ const warning = formatSecretsWarning(secretFiles);
1983
+ await logger.error("\u{1F6A8} SECRETS DETECTED", new Error("Secrets detected"), {
1984
+ secretFiles
1985
+ });
1986
+ console.error("\n" + warning + "\n");
1987
+ if (!options.allowSecrets) {
1988
+ throw new SecretsDetectedError(
1989
+ basicMatches,
1990
+ `Secrets detected in ${secretFiles.length} file(s). Use --allow-secrets to bypass after review, or add files to .gitignore.`
1991
+ );
1992
+ }
1993
+ console.log("\n\u26A0\uFE0F WARNING: --allow-secrets flag detected\n");
1994
+ const confirmed = await promptUserConfirmation(
1995
+ `\u26A0\uFE0F Proceed with committing ${secretFiles.length} file(s) that may contain secrets?`,
1996
+ false
1997
+ // default: NO
1998
+ );
1999
+ if (!confirmed) {
2000
+ throw new SecretsDetectedError(
2001
+ basicMatches,
2002
+ "User declined to commit files with potential secrets."
2003
+ );
2004
+ }
2005
+ await logger.warn("User confirmed to proceed with files containing potential secrets", {
2006
+ secretFiles,
2007
+ confirmedAt: (/* @__PURE__ */ new Date()).toISOString()
2008
+ });
2009
+ console.log("\u2705 User confirmed - continuing with commit generation...\n");
2010
+ }
2011
+ const summaries = await getFileSummaries(cwd, allFiles);
2012
+ const patternAnalysis = analyzePatterns(summaries);
2013
+ if (patternAnalysis.confidence > 0.7) {
2014
+ await analytics?.track("commit.pattern-detected", {
2015
+ patternType: patternAnalysis.patternType,
2016
+ confidence: patternAnalysis.confidence,
2017
+ suggestedType: patternAnalysis.suggestedType,
2018
+ fileCount: summaries.length,
2019
+ hints: patternAnalysis.hints
2020
+ });
2021
+ await logger.debug(`Pattern detected: ${patternAnalysis.patternType} (confidence: ${patternAnalysis.confidence.toFixed(2)})`, {
2022
+ suggestedType: patternAnalysis.suggestedType,
2023
+ hints: patternAnalysis.hints
2024
+ });
2025
+ }
2026
+ const recentCommits = options.recentCommits ?? await getRecentCommits(cwd, 10);
2027
+ let commits;
2028
+ let llmUsed = false;
2029
+ let tokensUsed;
2030
+ let escalated = false;
2031
+ if (llm) {
2032
+ try {
2033
+ const supportsNativeTools = typeof llm.chatWithTools === "function";
2034
+ let parsed;
2035
+ if (supportsNativeTools) {
2036
+ await logger.debug("Using native tools approach (chatWithTools)");
2037
+ onProgress?.("Analyzing with native tools...");
2038
+ const result = await generateWithNativeTools(
2039
+ llm,
2040
+ summaries,
2041
+ patternAnalysis,
2042
+ recentCommits,
2043
+ logger,
2044
+ onProgress
2045
+ );
2046
+ parsed = result.parsed;
2047
+ tokensUsed = result.tokensUsed;
2048
+ llmUsed = true;
2049
+ } else {
2050
+ await logger.debug("Using text-based parsing (fallback)");
2051
+ onProgress?.("Analyzing with text-based LLM...");
2052
+ const prompt = buildEnhancedPrompt(summaries, patternAnalysis, recentCommits);
2053
+ const result = await retryLLMCall(
2054
+ () => llm.complete(prompt, {
2055
+ systemPrompt: SYSTEM_PROMPT,
2056
+ temperature: 0.3,
2057
+ maxTokens: 2e3
2058
+ }),
2059
+ "Phase 1",
2060
+ logger,
2061
+ onProgress
2062
+ );
2063
+ parsed = parseResponse(result.content, summaries, patternAnalysis);
2064
+ llmUsed = true;
2065
+ tokensUsed = result.tokensUsed;
2066
+ }
2067
+ const shouldEscalate = parsed.needsMoreContext || parsed.averageConfidence < CONFIDENCE_THRESHOLD || summaries.length >= 10;
2068
+ if (shouldEscalate) {
2069
+ const reason = summaries.length >= 10 ? `${summaries.length} files (\u226510)` : `confidence ${(parsed.averageConfidence * 100).toFixed(0)}%`;
2070
+ await logger.debug(`Escalating to Phase 2: ${reason}`, {
2071
+ fileCount: summaries.length,
2072
+ confidence: parsed.averageConfidence,
2073
+ needsMoreContext: parsed.needsMoreContext,
2074
+ requestedFiles: parsed.requestedFiles
2075
+ });
2076
+ onProgress?.(`${reason} - fetching diff...`);
2077
+ let filesToDiff;
2078
+ if (parsed.requestedFiles.length > 0) {
2079
+ const validFiles = parsed.requestedFiles.filter((f) => summaries.some((s) => s.path === f));
2080
+ filesToDiff = validFiles.slice(0, 15);
2081
+ if (validFiles.length > 15) {
2082
+ await logger.warn(`LLM requested ${validFiles.length} files, truncated to 15 most critical`, {
2083
+ requestedCount: validFiles.length,
2084
+ truncatedCount: 15
2085
+ });
2086
+ }
2087
+ } else {
2088
+ const sortedByChanges = [...summaries].sort((a, b) => {
2089
+ const aChanges = (a.additions ?? 0) + (a.deletions ?? 0);
2090
+ const bChanges = (b.additions ?? 0) + (b.deletions ?? 0);
2091
+ return bChanges - aChanges;
2092
+ });
2093
+ filesToDiff = sortedByChanges.slice(0, 15).map((s) => s.path);
2094
+ await logger.debug("Auto-selected top 15 most changed files for Phase 2", {
2095
+ totalFiles: summaries.length,
2096
+ selectedCount: filesToDiff.length
2097
+ });
2098
+ }
2099
+ const diffs = await getFileDiffs(cwd, filesToDiff);
2100
+ const secretMatches = detectSecretsWithLocation(diffs);
2101
+ if (secretMatches.length > 0) {
2102
+ const report = formatSecretsReport(secretMatches);
2103
+ await logger.error("\u{1F6A8} SECRETS DETECTED IN DIFF CONTENT", new Error("Secrets in diff"), {
2104
+ secretMatches
2105
+ });
2106
+ onProgress?.("\u{1F6A8} Secrets detected in diff");
2107
+ console.error("\n" + report + "\n");
2108
+ if (!options.allowSecrets) {
2109
+ throw new SecretsDetectedError(
2110
+ secretMatches,
2111
+ `Secrets detected in ${secretMatches.length} location(s). Use --allow-secrets to bypass after review, or remove secrets before committing.`
2112
+ );
2113
+ }
2114
+ console.log("\n\u26A0\uFE0F WARNING: --allow-secrets flag detected\n");
2115
+ const confirmed = await promptUserConfirmation(
2116
+ `\u26A0\uFE0F Proceed with committing changes that contain ${secretMatches.length} potential secret(s)?`,
2117
+ false,
2118
+ // default: NO
2119
+ options.autoConfirm
2120
+ // auto-confirm if --yes flag
2121
+ );
2122
+ if (!confirmed) {
2123
+ throw new SecretsDetectedError(
2124
+ secretMatches,
2125
+ "User declined to commit changes with potential secrets."
2126
+ );
2127
+ }
2128
+ await logger.warn("User confirmed to proceed with diff containing potential secrets", {
2129
+ secretMatches: secretMatches.map((m) => ({ file: m.file, line: m.line, pattern: m.patternName })),
2130
+ confirmedAt: (/* @__PURE__ */ new Date()).toISOString()
2131
+ });
2132
+ console.log("\u2705 User confirmed - continuing with Phase 2 analysis...\n");
2133
+ onProgress?.("Re-analyzing with diff context (Phase 2)...");
2134
+ }
2135
+ if (diffs.size > 0) {
2136
+ const maxTokensPhase2 = Math.min(6e3, 3e3 + Math.floor(summaries.length / 20) * 500);
2137
+ await logger.debug("Re-analyzing with diff context (Phase 2)", {
2138
+ filesWithDiff: filesToDiff.length,
2139
+ supportsNativeTools
2140
+ });
2141
+ onProgress?.("Re-analyzing with diff context (Phase 2)...");
2142
+ if (supportsNativeTools) {
2143
+ const resultWithDiff = await generateWithNativeToolsPhase2(
2144
+ llm,
2145
+ summaries,
2146
+ diffs,
2147
+ recentCommits,
2148
+ logger,
2149
+ onProgress
2150
+ );
2151
+ parsed = resultWithDiff.parsed;
2152
+ tokensUsed = (tokensUsed ?? 0) + (resultWithDiff.tokensUsed ?? 0);
2153
+ escalated = true;
2154
+ } else {
2155
+ const promptWithDiff = buildPromptWithDiff(summaries, diffs, recentCommits);
2156
+ const resultWithDiff = await retryLLMCall(
2157
+ () => llm.complete(promptWithDiff, {
2158
+ systemPrompt: SYSTEM_PROMPT_WITH_DIFF,
2159
+ temperature: 0.3,
2160
+ maxTokens: maxTokensPhase2
2161
+ }),
2162
+ "Phase 2",
2163
+ logger,
2164
+ onProgress
2165
+ );
2166
+ parsed = parseResponse(resultWithDiff.content, summaries, patternAnalysis);
2167
+ tokensUsed = (tokensUsed ?? 0) + (resultWithDiff.tokensUsed ?? 0);
2168
+ escalated = true;
2169
+ }
2170
+ }
2171
+ }
2172
+ commits = parsed.commits;
2173
+ commits = await validateAndFixCommits(commits, summaries, llm, llmUsed, logger, onProgress);
2174
+ } catch (error) {
2175
+ if (error instanceof SecretsDetectedError) {
2176
+ throw error;
2177
+ }
2178
+ await logger.warn("LLM generation failed after retries, falling back to heuristics", {
2179
+ error: error instanceof Error ? error.message : String(error)
2180
+ });
2181
+ onProgress?.("LLM failed, using heuristics...");
2182
+ commits = generateHeuristicPlan(summaries);
2183
+ }
2184
+ } else {
2185
+ commits = generateHeuristicPlan(summaries);
2186
+ }
2187
+ const typeDistribution = commits.reduce((acc, commit) => {
2188
+ acc[commit.type] = (acc[commit.type] || 0) + 1;
2189
+ return acc;
2190
+ }, {});
2191
+ await analytics?.track("commit.generation-complete", {
2192
+ totalFiles: summaries.length,
2193
+ totalCommits: commits.length,
2194
+ llmUsed,
2195
+ escalated,
2196
+ tokensUsed,
2197
+ durationMs: Date.now() - startTime,
2198
+ typeDistribution,
2199
+ cwd
2200
+ });
2201
+ return {
2202
+ schemaVersion: "1.0",
2203
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2204
+ repoRoot: cwd,
2205
+ gitStatus,
2206
+ commits,
2207
+ metadata: {
2208
+ totalFiles: summaries.length,
2209
+ totalCommits: commits.length,
2210
+ llmUsed,
2211
+ tokensUsed,
2212
+ escalated
2213
+ }
2214
+ };
2215
+ }
2216
+ function createEmptyPlan(cwd, gitStatus) {
2217
+ return {
2218
+ schemaVersion: "1.0",
2219
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2220
+ repoRoot: cwd,
2221
+ gitStatus,
2222
+ commits: [],
2223
+ metadata: {
2224
+ totalFiles: 0,
2225
+ totalCommits: 0,
2226
+ llmUsed: false
2227
+ }
2228
+ };
2229
+ }
2230
+ async function generateWithNativeTools(llm, summaries, patternAnalysis, recentCommits, logger, _onProgress) {
2231
+ if (!llm || !llm.chatWithTools) {
2232
+ throw new Error("LLM does not support native tools (chatWithTools)");
2233
+ }
2234
+ const userPrompt = buildEnhancedPrompt(summaries, patternAnalysis, recentCommits);
2235
+ const messages = [
2236
+ {
2237
+ role: "system",
2238
+ content: SYSTEM_PROMPT
2239
+ },
2240
+ {
2241
+ role: "user",
2242
+ content: userPrompt
2243
+ }
2244
+ ];
2245
+ const response = await llm.chatWithTools(messages, {
2246
+ tools: [COMMIT_PLAN_TOOL],
2247
+ toolChoice: {
2248
+ type: "function",
2249
+ function: { name: "generate_commit_plan" }
2250
+ },
2251
+ temperature: 0.3
2252
+ });
2253
+ const toolCall = response.toolCalls?.[0];
2254
+ if (!toolCall || toolCall.name !== "generate_commit_plan") {
2255
+ throw new Error("LLM did not call generate_commit_plan tool");
2256
+ }
2257
+ const toolArgs = toolCall.input;
2258
+ const totalConfidence = toolArgs.commits.reduce((sum, c) => {
2259
+ const confidence = c.reasoning?.confidence ?? 0.5;
2260
+ return sum + confidence;
2261
+ }, 0);
2262
+ const averageConfidence = toolArgs.commits.length > 0 ? totalConfidence / toolArgs.commits.length : 0;
2263
+ await logger.debug("Native tools Phase 1 result", {
2264
+ needsMoreContext: toolArgs.needsMoreContext,
2265
+ requestedFiles: toolArgs.requestedFiles?.length ?? 0,
2266
+ commitsCount: toolArgs.commits.length,
2267
+ averageConfidence
2268
+ });
2269
+ return {
2270
+ parsed: {
2271
+ needsMoreContext: Boolean(toolArgs.needsMoreContext),
2272
+ requestedFiles: toolArgs.requestedFiles ?? [],
2273
+ commits: toolArgs.commits,
2274
+ averageConfidence
2275
+ },
2276
+ tokensUsed: response.usage ? response.usage.promptTokens + response.usage.completionTokens : void 0
2277
+ };
2278
+ }
2279
+ async function generateMissingFilesCommit(llm, missingSummaries, existingCommits, logger, onProgress) {
2280
+ if (!llm || !llm.chatWithTools) {
2281
+ return null;
2282
+ }
2283
+ const existingCommitsContext = existingCommits.map((c) => {
2284
+ const filesPreview = c.files.length <= 3 ? c.files.join(", ") : `${c.files.slice(0, 3).join(", ")} and ${c.files.length - 3} more`;
2285
+ return `[${c.id}] ${c.type}${c.scope ? `(${c.scope})` : ""}: ${c.message}
2286
+ Files: ${filesPreview}`;
2287
+ }).join("\n\n");
2288
+ const missingFilesList = missingSummaries.map((s) => {
2289
+ const stats = s.binary ? "binary" : `+${s.additions}/-${s.deletions}`;
2290
+ const isNew = s.isNewFile ? "IsNewFile: true" : "IsNewFile: false";
2291
+ return `- ${s.path} (${s.status}, ${stats}, ${isNew})`;
2292
+ }).join("\n");
2293
+ const systemPrompt = `You are analyzing files that were not included in the initial commit plan.
2294
+
2295
+ CONTEXT: These files were not classified by the LLM in previous phases. Your task is to determine:
2296
+ 1. Why they were missed (config files, minor changes, unrelated changes, etc.)
2297
+ 2. Whether they belong to an EXISTING commit or need a NEW commit
2298
+ 3. What type of commit they should be (chore, refactor, fix, feat, docs, test, etc.)
2299
+
2300
+ CRITICAL: You have TWO actions available:
2301
+
2302
+ ACTION 1: extend_existing (PREFER THIS if file is related to existing commit)
2303
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
2304
+ Use when a file logically belongs to an already created commit.
2305
+
2306
+ Example: If existing commit is "feat(fs): add fs adapter" and you see:
2307
+ - packages/adapters-fs/src/secure-storage.test.ts (test for fs adapter)
2308
+
2309
+ Then use:
2310
+ {
2311
+ "action": "extend_existing",
2312
+ "existingCommitId": "c1",
2313
+ "files": ["packages/adapters-fs/src/secure-storage.test.ts"]
2314
+ }
2315
+
2316
+ The file will be added to commit c1 instead of creating a new commit.
2317
+
2318
+ ACTION 2: create_new (use when file is unrelated to existing commits)
2319
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
2320
+ Use when files don't fit into any existing commit.
2321
+
2322
+ DEFAULT to "chore" unless you see clear evidence of feat/fix/refactor.
2323
+
2324
+ IMPORTANT: These are leftover files - they are usually:
2325
+ - Configuration files (package.json, tsconfig.json, etc.)
2326
+ - Minor updates to existing files
2327
+ - Test files or documentation
2328
+ - Build/tooling changes
2329
+
2330
+ CRITICAL: WRITE INFORMATIVE COMMIT MESSAGES (not generic):
2331
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
2332
+ \u274C BAD (too generic):
2333
+ - "update files"
2334
+ - "update configuration"
2335
+ - "update additional files"
2336
+ - "update 30 files"
2337
+
2338
+ \u2705 GOOD (specific and descriptive):
2339
+ - "update TypeScript and ESLint configuration for strict mode"
2340
+ - "update package dependencies for security patches"
2341
+ - "update build configuration to support ESM modules"
2342
+
2343
+ Guidelines:
2344
+ - Include WHAT was changed (specific files/components)
2345
+ - Include WHY if relevant (context about the change)
2346
+ - Use concrete nouns (not "files", "additional files")
2347
+ - Add context that helps reviewers understand the change`;
2348
+ const userPrompt = `Existing commits (you can add files to these using extend_existing action):
2349
+ ${existingCommitsContext}
2350
+
2351
+ Files that need classification (${missingSummaries.length} remaining):
2352
+ ${missingFilesList}
2353
+
2354
+ IMPORTANT: You don't need to classify ALL files in one response if there are too many.
2355
+ - If you can confidently classify some files \u2192 do it
2356
+ - If you're unsure about some files \u2192 skip them (they'll be processed in next iteration)
2357
+ - Focus on files you can group logically
2358
+
2359
+ For each file, choose the appropriate action:
2360
+ 1. extend_existing - if file belongs to an existing commit (e.g., test file for fs adapter \u2192 add to "feat(fs): add fs adapter")
2361
+ 2. create_new - if file doesn't fit into any existing commit
2362
+
2363
+ You may create:
2364
+ - ONE new commit if all remaining files are related (same purpose, same change type)
2365
+ - MULTIPLE new commits if files have different purposes (e.g., separate config changes from test updates)
2366
+ - MIX of extend_existing and create_new actions (recommended!)
2367
+
2368
+ CRITICAL: Be specific in each commit message - describe WHAT is being changed, not just "update files".
2369
+ PREFER extend_existing when possible to avoid creating unnecessary commits.`;
2370
+ const messages = [
2371
+ { role: "system", content: systemPrompt },
2372
+ { role: "user", content: userPrompt }
2373
+ ];
2374
+ try {
2375
+ onProgress?.("Classifying remaining files (Phase 3)...");
2376
+ const response = await llm.chatWithTools(messages, {
2377
+ tools: [COMMIT_PLAN_TOOL_PHASE3],
2378
+ toolChoice: {
2379
+ type: "function",
2380
+ function: { name: "generate_commit_plan" }
2381
+ },
2382
+ temperature: 0.3
2383
+ });
2384
+ const toolCall = response.toolCalls?.[0];
2385
+ if (!toolCall || toolCall.name !== "generate_commit_plan") {
2386
+ await logger.warn("Phase 3: LLM did not call tool, using fallback");
2387
+ return null;
2388
+ }
2389
+ const toolArgs = toolCall.input;
2390
+ if (!toolArgs.commits || toolArgs.commits.length === 0) {
2391
+ await logger.warn("Phase 3: LLM returned empty commits, using fallback");
2392
+ return null;
2393
+ }
2394
+ const newCommits = [];
2395
+ let extendedCount = 0;
2396
+ let createdCount = 0;
2397
+ for (const commit of toolArgs.commits) {
2398
+ if (!Array.isArray(commit.files) || commit.files.length === 0) {
2399
+ await logger.warn("Phase 3: Skipping commit action with empty files", {
2400
+ action: commit.action,
2401
+ existingCommitId: commit.existingCommitId
2402
+ });
2403
+ continue;
2404
+ }
2405
+ const action = commit.action || "create_new";
2406
+ if (action === "extend_existing") {
2407
+ const normalizedRequestedId = normalizeCommitId(commit.existingCommitId);
2408
+ const existingCommit = existingCommits.find((c) => normalizeCommitId(c.id) === normalizedRequestedId);
2409
+ if (existingCommit) {
2410
+ existingCommit.files.push(...commit.files);
2411
+ extendedCount++;
2412
+ await logger.debug("Phase 3: Extended existing commit", {
2413
+ commitId: existingCommit.id,
2414
+ message: existingCommit.message,
2415
+ addedFiles: commit.files.length
2416
+ });
2417
+ } else {
2418
+ await logger.warn("Phase 3: Commit ID not found, creating new instead", {
2419
+ requestedId: commit.existingCommitId,
2420
+ normalizedRequestedId,
2421
+ availableIds: existingCommits.map((c) => c.id).slice(0, 10)
2422
+ });
2423
+ const safeCommit = toSafeCommitGroup(
2424
+ commit,
2425
+ `c${existingCommits.length + newCommits.length + 1}`
2426
+ );
2427
+ if (safeCommit) {
2428
+ newCommits.push(safeCommit);
2429
+ createdCount++;
2430
+ } else {
2431
+ await logger.warn("Phase 3: Fallback commit had no valid files, skipped", {
2432
+ requestedId: commit.existingCommitId
2433
+ });
2434
+ }
2435
+ }
2436
+ } else {
2437
+ const safeCommit = toSafeCommitGroup(
2438
+ commit,
2439
+ `c${existingCommits.length + newCommits.length + 1}`
2440
+ );
2441
+ if (safeCommit) {
2442
+ newCommits.push(safeCommit);
2443
+ createdCount++;
2444
+ } else {
2445
+ await logger.warn("Phase 3: create_new action had no valid files, skipped");
2446
+ }
2447
+ }
2448
+ }
2449
+ await logger.debug("Phase 3: Processed commits", {
2450
+ total: toolArgs.commits.length,
2451
+ extended: extendedCount,
2452
+ created: createdCount,
2453
+ newCommitsCount: newCommits.length
2454
+ });
2455
+ return newCommits;
2456
+ } catch (error) {
2457
+ await logger.warn("Phase 3 failed, using fallback", {
2458
+ error: error instanceof Error ? error.message : String(error)
2459
+ });
2460
+ return null;
2461
+ }
2462
+ }
2463
+ async function generateWithNativeToolsPhase2(llm, summaries, diffs, recentCommits, logger, _onProgress) {
2464
+ if (!llm || !llm.chatWithTools) {
2465
+ throw new Error("LLM does not support native tools (chatWithTools)");
2466
+ }
2467
+ const userPrompt = buildPromptWithDiff(summaries, diffs, recentCommits);
2468
+ const messages = [
2469
+ {
2470
+ role: "system",
2471
+ content: SYSTEM_PROMPT_WITH_DIFF
2472
+ },
2473
+ {
2474
+ role: "user",
2475
+ content: userPrompt
2476
+ }
2477
+ ];
2478
+ const response = await llm.chatWithTools(messages, {
2479
+ tools: [COMMIT_PLAN_TOOL],
2480
+ toolChoice: {
2481
+ type: "function",
2482
+ function: { name: "generate_commit_plan" }
2483
+ },
2484
+ temperature: 0.3
2485
+ });
2486
+ const toolCall = response.toolCalls?.[0];
2487
+ if (!toolCall || toolCall.name !== "generate_commit_plan") {
2488
+ throw new Error("LLM did not call generate_commit_plan tool in Phase 2");
2489
+ }
2490
+ const toolArgs = toolCall.input;
2491
+ const totalConfidence = toolArgs.commits.reduce((sum, c) => {
2492
+ const confidence = c.reasoning?.confidence ?? 0.5;
2493
+ return sum + confidence;
2494
+ }, 0);
2495
+ const averageConfidence = toolArgs.commits.length > 0 ? totalConfidence / toolArgs.commits.length : 0;
2496
+ await logger.debug("Native tools Phase 2 result", {
2497
+ commitsCount: toolArgs.commits.length,
2498
+ averageConfidence
2499
+ });
2500
+ return {
2501
+ parsed: {
2502
+ needsMoreContext: false,
2503
+ // Phase 2 is final, no more escalation
2504
+ requestedFiles: [],
2505
+ commits: toolArgs.commits,
2506
+ averageConfidence
2507
+ },
2508
+ tokensUsed: response.usage ? response.usage.promptTokens + response.usage.completionTokens : void 0
2509
+ };
2510
+ }
2511
+ async function validateAndFixCommits(commits, summaries, llm, llmUsed, logger, onProgress) {
2512
+ const realFiles = new Set(summaries.map((s) => s.path));
2513
+ for (const commit of commits) {
2514
+ const validFiles = [];
2515
+ for (const file of commit.files) {
2516
+ if (realFiles.has(file)) {
2517
+ validFiles.push(file);
2518
+ }
2519
+ }
2520
+ commit.files = validFiles;
2521
+ }
2522
+ let nonEmptyCommits = commits.filter((c) => c.files.length > 0);
2523
+ const seenFiles = /* @__PURE__ */ new Set();
2524
+ const duplicates = [];
2525
+ for (const commit of nonEmptyCommits) {
2526
+ const uniqueFiles = [];
2527
+ for (const file of commit.files) {
2528
+ if (!seenFiles.has(file)) {
2529
+ uniqueFiles.push(file);
2530
+ seenFiles.add(file);
2531
+ } else {
2532
+ duplicates.push({ file, commit: commit.id });
2533
+ }
2534
+ }
2535
+ commit.files = uniqueFiles;
2536
+ }
2537
+ if (duplicates.length > 0) {
2538
+ logger.warn(`LLM returned ${duplicates.length} duplicate file(s) across commits - removed duplicates`, {
2539
+ duplicateCount: duplicates.length,
2540
+ samples: duplicates.slice(0, 5).map((d) => `${d.file} in ${d.commit}`)
2541
+ });
2542
+ }
2543
+ nonEmptyCommits = nonEmptyCommits.filter((c) => c.files.length > 0);
2544
+ const MAX_PHASE3_ITERATIONS = 5;
2545
+ let phase3Iteration = 0;
2546
+ while (phase3Iteration < MAX_PHASE3_ITERATIONS) {
2547
+ const allFilesInCommits = new Set(nonEmptyCommits.flatMap((c) => c.files));
2548
+ const missingFiles = summaries.map((s) => s.path).filter((f) => !allFilesInCommits.has(f));
2549
+ if (missingFiles.length === 0) {
2550
+ if (phase3Iteration > 0) {
2551
+ await logger.info("Phase 3: All files classified", {
2552
+ totalIterations: phase3Iteration,
2553
+ totalCommits: nonEmptyCommits.length
2554
+ });
2555
+ }
2556
+ break;
2557
+ }
2558
+ phase3Iteration++;
2559
+ await logger.debug(`Phase 3 iteration ${phase3Iteration}/${MAX_PHASE3_ITERATIONS}`, {
2560
+ missingFiles: missingFiles.length,
2561
+ processedSoFar: summaries.length - missingFiles.length,
2562
+ totalFiles: summaries.length,
2563
+ progress: `${Math.round((summaries.length - missingFiles.length) / summaries.length * 100)}%`
2564
+ });
2565
+ if (onProgress) {
2566
+ const progress = Math.round((summaries.length - missingFiles.length) / summaries.length * 100);
2567
+ onProgress(`Classifying remaining files (Phase 3, iteration ${phase3Iteration}, ${progress}%)...`);
2568
+ }
2569
+ const missingSummaries = summaries.filter((s) => missingFiles.includes(s.path));
2570
+ let missingCommits = null;
2571
+ if (llmUsed && llm) {
2572
+ missingCommits = await generateMissingFilesCommit(
2573
+ llm,
2574
+ missingSummaries,
2575
+ nonEmptyCommits,
2576
+ // ← pass updated commits list (includes previous iterations)
2577
+ logger,
2578
+ onProgress
2579
+ );
2580
+ if (missingCommits && missingCommits.length > 0) {
2581
+ await logger.debug(`Phase 3 iteration ${phase3Iteration}: Processed commits`, {
2582
+ commitCount: missingCommits.length,
2583
+ filesCount: missingCommits.reduce((sum, c) => sum + c.files.length, 0)
2584
+ });
2585
+ nonEmptyCommits.push(...missingCommits);
2586
+ }
2587
+ }
2588
+ const newMissingFiles = summaries.map((s) => s.path).filter((f) => !new Set(nonEmptyCommits.flatMap((c) => c.files)).has(f));
2589
+ if (newMissingFiles.length === missingFiles.length) {
2590
+ await logger.warn("Phase 3: LLM made no progress, stopping iterations", {
2591
+ iteration: phase3Iteration,
2592
+ stillMissing: newMissingFiles.length
2593
+ });
2594
+ break;
2595
+ }
2596
+ if (newMissingFiles.length <= 3 && phase3Iteration >= 2) {
2597
+ await logger.debug("Phase 3: Few files left, proceeding to fallback");
2598
+ break;
2599
+ }
2600
+ }
2601
+ const finalMissingFiles = summaries.map((s) => s.path).filter((f) => !new Set(nonEmptyCommits.flatMap((c) => c.files)).has(f));
2602
+ if (finalMissingFiles.length > 0) {
2603
+ await logger.warn("Phase 3: Max iterations reached or LLM struggled, using fallback", {
2604
+ totalIterations: phase3Iteration,
2605
+ remainingFiles: finalMissingFiles.length
2606
+ });
2607
+ const firstFile = finalMissingFiles[0];
2608
+ const fileName = firstFile ? firstFile.split("/").pop() ?? firstFile : "files";
2609
+ const commitMessage = finalMissingFiles.length === 1 ? `update ${fileName}` : `update ${finalMissingFiles.length} remaining files`;
2610
+ const fallbackCommit = {
2611
+ id: `c${nonEmptyCommits.length + 1}`,
2612
+ type: "chore",
2613
+ message: commitMessage,
2614
+ files: finalMissingFiles,
2615
+ releaseHint: "none",
2616
+ breaking: false,
2617
+ reasoning: {
2618
+ newBehavior: false,
2619
+ fixesBug: false,
2620
+ internalOnly: true,
2621
+ explanation: `Files not classified after ${phase3Iteration} Phase 3 iteration(s): ${finalMissingFiles.slice(0, 3).join(", ")}${finalMissingFiles.length > 3 ? ` and ${finalMissingFiles.length - 3} more` : ""}`,
2622
+ confidence: 0.3
2623
+ }
2624
+ };
2625
+ nonEmptyCommits.push(fallbackCommit);
2626
+ }
2627
+ return nonEmptyCommits;
2628
+ }
2629
+ async function retryLLMCall(llmFn, phase, logger, onProgress) {
2630
+ let lastError;
2631
+ for (let attempt = 1; attempt <= MAX_LLM_RETRIES; attempt++) {
2632
+ try {
2633
+ const result = await llmFn();
2634
+ parseResponse(result.content);
2635
+ if (attempt > 1) {
2636
+ await logger.debug(`${phase} succeeded on attempt ${attempt}/${MAX_LLM_RETRIES}`);
2637
+ }
2638
+ return result;
2639
+ } catch (error) {
2640
+ lastError = error instanceof Error ? error : new Error(String(error));
2641
+ await logger.error(`\u{1F50D} LLM call failed (attempt ${attempt})`, lastError, {
2642
+ errorName: lastError.name,
2643
+ errorMessage: lastError.message,
2644
+ errorStack: lastError.stack
2645
+ });
2646
+ const errorType = getErrorType(lastError);
2647
+ if (attempt < MAX_LLM_RETRIES) {
2648
+ await logger.warn(`${phase} failed (attempt ${attempt}/${MAX_LLM_RETRIES}): ${errorType}`, {
2649
+ errorMessage: lastError.message,
2650
+ errorType,
2651
+ attempt,
2652
+ stack: lastError.stack?.split("\n").slice(0, 3).join("\n")
2653
+ // First 3 lines only
2654
+ });
2655
+ onProgress?.(`${phase} ${errorType}, retrying (${attempt}/${MAX_LLM_RETRIES})...`);
2656
+ await new Promise((resolve) => {
2657
+ setTimeout(resolve, 1e3 * Math.pow(2, attempt - 1));
2658
+ });
2659
+ } else {
2660
+ await logger.error(`${phase} failed after ${MAX_LLM_RETRIES} attempts: ${errorType}`, lastError, {
2661
+ errorType
2662
+ });
2663
+ }
2664
+ }
2665
+ }
2666
+ throw lastError || new Error(`${phase} failed after ${MAX_LLM_RETRIES} attempts`);
2667
+ }
2668
+ function getErrorType(error) {
2669
+ const message = error.message.toLowerCase();
2670
+ if (message.includes("429") || message.includes("rate limit") || message.includes("too many requests")) {
2671
+ return "rate limited (429)";
2672
+ }
2673
+ if (message.includes("500") || message.includes("502") || message.includes("503")) {
2674
+ return "server error (5xx)";
2675
+ }
2676
+ if (message.includes("timeout") || message.includes("timed out")) {
2677
+ return "timeout";
2678
+ }
2679
+ if (message.includes("network") || message.includes("econnrefused") || message.includes("enotfound")) {
2680
+ return "network error";
2681
+ }
2682
+ if (message.includes("json") || message.includes("parse") || message.includes("unexpected token")) {
2683
+ return "invalid JSON";
2684
+ }
2685
+ if (message.includes("missing") || message.includes("missing required field")) {
2686
+ return "invalid structure";
2687
+ }
2688
+ const preview = error.message.substring(0, 50).replace(/\n/g, " ");
2689
+ return `error: ${preview}${error.message.length > 50 ? "..." : ""}`;
2690
+ }
2691
+
2692
+ export { COMMIT_PLAN_TOOL, COMMIT_PLAN_TOOL_PHASE3, SYSTEM_PROMPT, SYSTEM_PROMPT_WITH_DIFF, buildPrompt, buildPromptWithDiff, generateCommitPlan, generateHeuristicPlan, parseResponse };
2693
+ //# sourceMappingURL=index.js.map
2694
+ //# sourceMappingURL=index.js.map