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