@entro314labs/ai-changelog-generator 3.6.1 → 3.7.1

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.
@@ -44,6 +44,7 @@ export class CLIController {
44
44
  this.commands.set('commit-message', new CommitMessageCommand())
45
45
  this.commands.set('commit', new CommitCommand())
46
46
  this.commands.set('providers', new ProvidersCommand())
47
+ this.commands.set('stash', new StashCommand())
47
48
  }
48
49
 
49
50
  async runCLI() {
@@ -122,6 +123,22 @@ export class CLIController {
122
123
  type: 'string',
123
124
  description: 'Generate changelog since a specific git ref (tag/commit).',
124
125
  })
126
+ .option('author', {
127
+ alias: 'a',
128
+ type: 'string',
129
+ description: 'Filter commits by author name or email.',
130
+ })
131
+ .option('tag-range', {
132
+ type: 'string',
133
+ description: 'Generate changelog between two tags (format: v1.0.0..v2.0.0).',
134
+ })
135
+ .option('format', {
136
+ alias: 'f',
137
+ type: 'string',
138
+ choices: ['markdown', 'json', 'html'],
139
+ default: 'markdown',
140
+ description: 'Output format for the changelog.',
141
+ })
125
142
  .option('model', {
126
143
  alias: 'm',
127
144
  type: 'string',
@@ -137,6 +154,11 @@ export class CLIController {
137
154
  type: 'boolean',
138
155
  description: 'Disable the attribution footer.',
139
156
  })
157
+ .option('output', {
158
+ alias: 'o',
159
+ type: 'string',
160
+ description: 'Output file path.',
161
+ })
140
162
  })
141
163
 
142
164
  // Analysis commands
@@ -223,12 +245,38 @@ export class CLIController {
223
245
  })
224
246
  .option('model', { type: 'string', description: 'Override the default AI model.' })
225
247
  })
248
+ .command('stash', 'Analyze stashed changes.', (yargs) => {
249
+ yargs
250
+ .command('list', 'List all stashed changes.')
251
+ .command('analyze [stash]', 'Analyze a specific stash entry.', (y) => {
252
+ y.positional('stash', {
253
+ describe: 'Stash reference (e.g., stash@{0})',
254
+ default: 'stash@{0}',
255
+ type: 'string',
256
+ })
257
+ })
258
+ .command('changelog [stash]', 'Generate changelog from stashed changes.', (y) => {
259
+ y.positional('stash', {
260
+ describe: 'Stash reference (e.g., stash@{0})',
261
+ default: 'stash@{0}',
262
+ type: 'string',
263
+ })
264
+ })
265
+ .demandCommand(1, 'Please specify a stash subcommand.')
266
+ })
226
267
  .command('providers', 'Manage AI providers.', (yargs) => {
227
268
  yargs
228
269
  .command('list', 'List available providers.')
229
270
  .command('switch <provider>', 'Switch to a different provider.')
230
271
  .command('configure [provider]', 'Configure AI provider settings.')
231
272
  .command('validate [provider]', 'Validate provider models and capabilities.')
273
+ .command('status', 'Check health status of all providers.')
274
+ .command('models [provider]', 'List available models for a provider.', (y) => {
275
+ y.positional('provider', {
276
+ describe: 'Provider name (optional, shows all if not specified)',
277
+ type: 'string',
278
+ })
279
+ })
232
280
  .demandCommand(1, 'Please specify a provider subcommand.')
233
281
  })
234
282
 
@@ -251,12 +299,14 @@ export class CLIController {
251
299
  .option('format', {
252
300
  alias: 'f',
253
301
  type: 'string',
254
- choices: ['markdown', 'json'],
302
+ choices: ['markdown', 'json', 'html'],
255
303
  default: 'markdown',
256
304
  description: 'Output format',
257
305
  })
258
306
  .option('output', { alias: 'o', type: 'string', description: 'Output file path' })
259
307
  .option('since', { type: 'string', description: 'Analyze changes since this git ref' })
308
+ .option('author', { alias: 'a', type: 'string', description: 'Filter commits by author' })
309
+ .option('tag-range', { type: 'string', description: 'Generate changelog between tags (v1.0..v2.0)' })
260
310
  .option('silent', { type: 'boolean', description: 'Suppress non-essential output' })
261
311
  .option('dry-run', { type: 'boolean', description: 'Preview without writing files' })
262
312
  .option('detailed', { type: 'boolean', description: 'Use detailed analysis mode' })
@@ -304,6 +354,8 @@ class BaseCommand {
304
354
  format: argv.format || 'markdown',
305
355
  output: argv.output,
306
356
  since: argv.since,
357
+ author: argv.author,
358
+ tagRange: argv.tagRange,
307
359
  silent: argv.silent,
308
360
  dryRun: argv.dryRun,
309
361
  }
@@ -326,17 +378,83 @@ class BaseCommand {
326
378
  // Command implementations
327
379
  class DefaultCommand extends BaseCommand {
328
380
  async execute(argv, appService) {
329
- const _config = this.processStandardFlags(argv, appService)
381
+ const config = this.processStandardFlags(argv, appService)
330
382
 
331
383
  if (argv.interactive) {
332
384
  await appService.runInteractive()
333
385
  } else {
334
- await appService.generateChangelog({
386
+ const result = await appService.generateChangelog({
335
387
  version: argv.releaseVersion,
336
388
  since: argv.since,
389
+ author: argv.author,
390
+ tagRange: argv.tagRange,
391
+ format: config.format,
392
+ output: config.output,
393
+ dryRun: config.dryRun,
337
394
  })
395
+
396
+ // Handle output formatting
397
+ if (result?.changelog && config.format !== 'markdown') {
398
+ const formattedOutput = this.formatOutput(result.changelog, config.format)
399
+ if (config.dryRun || !config.output) {
400
+ console.log(formattedOutput)
401
+ }
402
+ }
338
403
  }
339
404
  }
405
+
406
+ formatOutput(changelog, format) {
407
+ if (format === 'json') {
408
+ return JSON.stringify({ changelog, generatedAt: new Date().toISOString() }, null, 2)
409
+ }
410
+ if (format === 'html') {
411
+ return this.convertToHtml(changelog)
412
+ }
413
+ return changelog
414
+ }
415
+
416
+ convertToHtml(markdown) {
417
+ // Simple markdown to HTML conversion
418
+ let html = markdown
419
+ // Headers
420
+ .replace(/^### (.*$)/gim, '<h3>$1</h3>')
421
+ .replace(/^## (.*$)/gim, '<h2>$1</h2>')
422
+ .replace(/^# (.*$)/gim, '<h1>$1</h1>')
423
+ // Bold
424
+ .replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
425
+ // Italic
426
+ .replace(/\*(.*)\*/gim, '<em>$1</em>')
427
+ // Links
428
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/gim, '<a href="$2">$1</a>')
429
+ // List items
430
+ .replace(/^\s*-\s+(.*$)/gim, '<li>$1</li>')
431
+ // Inline code
432
+ .replace(/`([^`]+)`/gim, '<code>$1</code>')
433
+ // Paragraphs
434
+ .replace(/\n\n/gim, '</p><p>')
435
+
436
+ return `<!DOCTYPE html>
437
+ <html lang="en">
438
+ <head>
439
+ <meta charset="UTF-8">
440
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
441
+ <title>Changelog</title>
442
+ <style>
443
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.6; }
444
+ h1, h2, h3 { color: #333; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
445
+ ul { padding-left: 2em; }
446
+ li { margin: 0.5em 0; }
447
+ code { background: #f4f4f4; padding: 0.2em 0.4em; border-radius: 3px; }
448
+ a { color: #0066cc; }
449
+ .generated { color: #666; font-size: 0.9em; margin-top: 2em; border-top: 1px solid #eee; padding-top: 1em; }
450
+ </style>
451
+ </head>
452
+ <body>
453
+ <p>${html}</p>
454
+ <div class="generated">Generated by AI Changelog Generator on ${new Date().toLocaleString()}</div>
455
+ </body>
456
+ </html>`
457
+ }
340
458
  }
341
459
 
342
460
  class InitCommand extends BaseCommand {
@@ -426,8 +544,21 @@ class WorkingDirCommand extends BaseCommand {
426
544
  class FromCommitsCommand extends BaseCommand {
427
545
  async execute(argv, appService) {
428
546
  const _config = this.processStandardFlags(argv, appService)
429
- // Implementation would generate changelog from specific commits
430
- console.log(colors.infoMessage(`Generating changelog from commits: ${argv.commits.join(', ')}`))
547
+ console.log(colors.processingMessage(`šŸ” Generating changelog from commits: ${argv.commits.join(', ')}`))
548
+
549
+ try {
550
+ const result = await appService.generateChangelogFromCommits(argv.commits)
551
+
552
+ if (result?.changelog) {
553
+ console.log(colors.successMessage('\nāœ… Changelog generated successfully!'))
554
+ console.log(colors.dim('─'.repeat(50)))
555
+ console.log(result.changelog)
556
+ } else {
557
+ console.log(colors.warningMessage('No changelog could be generated from the specified commits.'))
558
+ }
559
+ } catch (error) {
560
+ console.error(colors.errorMessage(`Error generating changelog: ${error.message}`))
561
+ }
431
562
  }
432
563
  }
433
564
 
@@ -526,9 +657,15 @@ class ProvidersCommand extends BaseCommand {
526
657
  case 'validate':
527
658
  await this.validateProvider(appService, argv.provider)
528
659
  break
660
+ case 'status':
661
+ await this.checkProviderStatus(appService)
662
+ break
663
+ case 'models':
664
+ await this.listModels(appService, argv.provider)
665
+ break
529
666
  default:
530
667
  console.log(colors.errorMessage('Unknown provider subcommand'))
531
- console.log(colors.infoMessage('Available subcommands: list, switch, configure, validate'))
668
+ console.log(colors.infoMessage('Available subcommands: list, switch, configure, validate, status, models'))
532
669
  }
533
670
  }
534
671
 
@@ -684,6 +821,305 @@ class ProvidersCommand extends BaseCommand {
684
821
  EnhancedConsole.error(`Error validating provider: ${error.message}`)
685
822
  }
686
823
  }
824
+
825
+ async checkProviderStatus(appService) {
826
+ console.log(colors.processingMessage('šŸ„ Checking provider health status...'))
827
+
828
+ try {
829
+ const providers = await appService.listProviders()
830
+ const healthResults = []
831
+
832
+ for (const provider of providers) {
833
+ if (provider.available) {
834
+ const startTime = Date.now()
835
+ try {
836
+ const validation = await appService.validateProvider(provider.name)
837
+ const responseTime = Date.now() - startTime
838
+
839
+ healthResults.push({
840
+ name: provider.name,
841
+ status: validation.success ? 'healthy' : 'unhealthy',
842
+ responseTime,
843
+ model: validation.model || 'N/A',
844
+ error: validation.error || null,
845
+ active: provider.active,
846
+ })
847
+ } catch (error) {
848
+ healthResults.push({
849
+ name: provider.name,
850
+ status: 'error',
851
+ responseTime: Date.now() - startTime,
852
+ error: error.message,
853
+ active: provider.active,
854
+ })
855
+ }
856
+ } else {
857
+ healthResults.push({
858
+ name: provider.name,
859
+ status: 'unconfigured',
860
+ responseTime: null,
861
+ error: 'Not configured',
862
+ active: false,
863
+ })
864
+ }
865
+ }
866
+
867
+ // Display results
868
+ console.log(colors.header('\nšŸ„ Provider Health Status:\n'))
869
+
870
+ const statusIcons = {
871
+ healthy: '🟢',
872
+ unhealthy: 'šŸ”“',
873
+ error: 'šŸ”“',
874
+ unconfigured: '⚪',
875
+ }
876
+
877
+ healthResults.forEach((result) => {
878
+ const icon = statusIcons[result.status] || '⚪'
879
+ const activeMarker = result.active ? ' šŸŽÆ' : ''
880
+ const responseInfo = result.responseTime ? ` (${result.responseTime}ms)` : ''
881
+
882
+ console.log(` ${icon} ${colors.highlight(result.name)}${activeMarker}`)
883
+ console.log(` Status: ${result.status}${responseInfo}`)
884
+
885
+ if (result.model && result.status === 'healthy') {
886
+ console.log(` Model: ${colors.dim(result.model)}`)
887
+ }
888
+
889
+ if (result.error && result.status !== 'unconfigured') {
890
+ console.log(` Error: ${colors.errorMessage(result.error)}`)
891
+ }
892
+
893
+ console.log('')
894
+ })
895
+
896
+ // Summary
897
+ const healthy = healthResults.filter((r) => r.status === 'healthy').length
898
+ const unhealthy = healthResults.filter((r) => ['unhealthy', 'error'].includes(r.status)).length
899
+ const unconfigured = healthResults.filter((r) => r.status === 'unconfigured').length
900
+
901
+ console.log(colors.dim('─'.repeat(40)))
902
+ console.log(
903
+ `Summary: ${colors.successMessage(`${healthy} healthy`)}, ${unhealthy > 0 ? colors.errorMessage(`${unhealthy} unhealthy`) : `${unhealthy} unhealthy`}, ${unconfigured} unconfigured`
904
+ )
905
+ } catch (error) {
906
+ EnhancedConsole.error(`Error checking provider status: ${error.message}`)
907
+ }
908
+ }
909
+
910
+ async listModels(appService, providerName) {
911
+ console.log(colors.processingMessage('šŸ” Discovering available models...'))
912
+
913
+ try {
914
+ const providers = await appService.listProviders()
915
+
916
+ if (providerName) {
917
+ // List models for specific provider
918
+ const provider = providers.find((p) => p.name.toLowerCase() === providerName.toLowerCase())
919
+ if (!provider) {
920
+ console.log(colors.errorMessage(`Provider '${providerName}' not found.`))
921
+ console.log(colors.infoMessage('Use "ai-changelog providers list" to see available providers.'))
922
+ return
923
+ }
924
+
925
+ console.log(colors.header(`\nšŸ“¦ Models for ${provider.name}:\n`))
926
+
927
+ if (provider.models && provider.models.length > 0) {
928
+ provider.models.forEach((model) => {
929
+ const isDefault = model === provider.defaultModel ? ' šŸŽÆ (default)' : ''
930
+ console.log(` ${colors.highlight(model)}${isDefault}`)
931
+ })
932
+ } else {
933
+ // Show known models from config
934
+ const knownModels = this.getKnownModelsForProvider(provider.name)
935
+ if (knownModels.length > 0) {
936
+ console.log(colors.dim(' Known models (from configuration):'))
937
+ knownModels.forEach((model) => {
938
+ console.log(` ${colors.highlight(model)}`)
939
+ })
940
+ } else {
941
+ console.log(colors.infoMessage(' No model information available.'))
942
+ }
943
+ }
944
+ } else {
945
+ // List models for all available providers
946
+ console.log(colors.header('\nšŸ“¦ Available Models by Provider:\n'))
947
+
948
+ for (const provider of providers) {
949
+ if (!provider.available) continue
950
+
951
+ console.log(`${colors.highlight(provider.name)}:`)
952
+
953
+ if (provider.models && provider.models.length > 0) {
954
+ provider.models.slice(0, 5).forEach((model) => {
955
+ const isDefault = model === provider.defaultModel ? ' šŸŽÆ' : ''
956
+ console.log(` ${colors.dim('•')} ${model}${isDefault}`)
957
+ })
958
+ if (provider.models.length > 5) {
959
+ console.log(` ${colors.dim(`... and ${provider.models.length - 5} more`)}`)
960
+ }
961
+ } else {
962
+ const knownModels = this.getKnownModelsForProvider(provider.name)
963
+ knownModels.slice(0, 3).forEach((model) => {
964
+ console.log(` ${colors.dim('•')} ${model}`)
965
+ })
966
+ }
967
+ console.log('')
968
+ }
969
+ }
970
+ } catch (error) {
971
+ EnhancedConsole.error(`Error listing models: ${error.message}`)
972
+ }
973
+ }
974
+
975
+ getKnownModelsForProvider(providerName) {
976
+ const knownModels = {
977
+ openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo', 'o1', 'o1-mini', 'o3-mini'],
978
+ anthropic: ['claude-sonnet-4-20250514', 'claude-3-5-sonnet-20241022', 'claude-3-opus-20240229', 'claude-3-haiku-20240307'],
979
+ google: ['gemini-2.0-flash-exp', 'gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-pro'],
980
+ azure: ['gpt-4o', 'gpt-4-turbo', 'gpt-35-turbo'],
981
+ ollama: ['llama3.2', 'llama3.1', 'mistral', 'codellama', 'deepseek-coder'],
982
+ lmstudio: ['local-model'],
983
+ bedrock: ['anthropic.claude-3-sonnet', 'anthropic.claude-3-haiku', 'amazon.titan-text-express'],
984
+ vertex: ['gemini-1.5-pro', 'gemini-1.5-flash'],
985
+ huggingface: ['mistralai/Mistral-7B-Instruct-v0.2', 'google/flan-t5-xxl'],
986
+ }
987
+ return knownModels[providerName.toLowerCase()] || []
988
+ }
989
+ }
990
+
991
+ class StashCommand extends BaseCommand {
992
+ async execute(argv, appService) {
993
+ const subcommand = argv._[1]
994
+
995
+ switch (subcommand) {
996
+ case 'list':
997
+ await this.listStashes(appService)
998
+ break
999
+ case 'analyze':
1000
+ await this.analyzeStash(appService, argv.stash)
1001
+ break
1002
+ case 'changelog':
1003
+ await this.generateStashChangelog(appService, argv.stash)
1004
+ break
1005
+ default:
1006
+ console.log(colors.errorMessage('Unknown stash subcommand'))
1007
+ console.log(colors.infoMessage('Available subcommands: list, analyze, changelog'))
1008
+ }
1009
+ }
1010
+
1011
+ async listStashes(appService) {
1012
+ try {
1013
+ const stashes = appService.orchestrator.gitManager.getStashList()
1014
+
1015
+ if (stashes.length === 0) {
1016
+ console.log(colors.infoMessage('No stashed changes found.'))
1017
+ return
1018
+ }
1019
+
1020
+ console.log(colors.header(`\nšŸ“¦ Stashed Changes (${stashes.length}):\n`))
1021
+
1022
+ stashes.forEach((stash, index) => {
1023
+ console.log(` ${colors.highlight(stash.index)}`)
1024
+ console.log(` ${colors.dim('Message:')} ${stash.message}`)
1025
+ console.log(` ${colors.dim('Date:')} ${stash.date}`)
1026
+ if (index < stashes.length - 1) console.log('')
1027
+ })
1028
+
1029
+ console.log(colors.dim('\nUse "ai-changelog stash analyze <stash>" to see details'))
1030
+ } catch (error) {
1031
+ EnhancedConsole.error(`Error listing stashes: ${error.message}`)
1032
+ }
1033
+ }
1034
+
1035
+ async analyzeStash(appService, stashRef = 'stash@{0}') {
1036
+ console.log(colors.processingMessage(`šŸ” Analyzing ${stashRef}...`))
1037
+
1038
+ try {
1039
+ const details = appService.orchestrator.gitManager.getStashDetails(stashRef)
1040
+
1041
+ if (!details) {
1042
+ console.log(colors.errorMessage(`Stash '${stashRef}' not found.`))
1043
+ return
1044
+ }
1045
+
1046
+ console.log(colors.header(`\nšŸ“¦ Stash Analysis: ${stashRef}\n`))
1047
+ console.log(`${colors.dim('Message:')} ${details.message}`)
1048
+ console.log(`${colors.dim('Files changed:')} ${details.stats.filesChanged}`)
1049
+ console.log(`${colors.dim('Insertions:')} ${colors.success(`+${details.stats.insertions}`)}`)
1050
+ console.log(`${colors.dim('Deletions:')} ${colors.error(`-${details.stats.deletions}`)}`)
1051
+
1052
+ console.log(colors.header('\nšŸ“ Files:\n'))
1053
+ details.files.forEach((file) => {
1054
+ console.log(` ${colors.highlight(file.path)} (${file.changes} changes)`)
1055
+ })
1056
+
1057
+ console.log(colors.dim('\nUse "ai-changelog stash changelog" to generate changelog'))
1058
+ } catch (error) {
1059
+ EnhancedConsole.error(`Error analyzing stash: ${error.message}`)
1060
+ }
1061
+ }
1062
+
1063
+ async generateStashChangelog(appService, stashRef = 'stash@{0}') {
1064
+ console.log(colors.processingMessage(`šŸ“ Generating changelog from ${stashRef}...`))
1065
+
1066
+ try {
1067
+ const details = appService.orchestrator.gitManager.getStashDetails(stashRef)
1068
+
1069
+ if (!details) {
1070
+ console.log(colors.errorMessage(`Stash '${stashRef}' not found.`))
1071
+ return
1072
+ }
1073
+
1074
+ // Create pseudo-commit data for AI analysis
1075
+ const stashData = {
1076
+ hash: stashRef.replace(/[{}@]/g, ''),
1077
+ message: details.message || 'Stashed changes',
1078
+ files: details.files.map((f) => ({
1079
+ filePath: f.path,
1080
+ status: 'modified',
1081
+ diff: '',
1082
+ })),
1083
+ diff: details.diff,
1084
+ stats: details.stats,
1085
+ }
1086
+
1087
+ // Analyze with AI if available
1088
+ if (appService.orchestrator.aiAnalysisService?.hasAI) {
1089
+ console.log(colors.processingMessage('šŸ¤– Analyzing stashed changes with AI...'))
1090
+
1091
+ const analysis = await appService.orchestrator.aiAnalysisService.analyzeCommit(stashData)
1092
+
1093
+ console.log(colors.header('\nšŸ“‹ Stash Changelog:\n'))
1094
+ console.log(`## Stashed Changes\n`)
1095
+ console.log(`**Summary:** ${analysis?.summary || details.message}`)
1096
+ console.log(`**Impact:** ${analysis?.impact || 'medium'}`)
1097
+ console.log(`**Category:** ${analysis?.category || 'chore'}`)
1098
+
1099
+ if (analysis?.description) {
1100
+ console.log(`\n${analysis.description}`)
1101
+ }
1102
+
1103
+ console.log(`\n**Files affected:** ${details.files.length}`)
1104
+ details.files.forEach((f) => {
1105
+ console.log(`- ${f.path}`)
1106
+ })
1107
+ } else {
1108
+ // Basic changelog without AI
1109
+ console.log(colors.header('\nšŸ“‹ Stash Changelog:\n'))
1110
+ console.log(`## Stashed Changes\n`)
1111
+ console.log(`**Message:** ${details.message}`)
1112
+ console.log(`**Stats:** ${details.stats.filesChanged} files, +${details.stats.insertions}/-${details.stats.deletions}`)
1113
+
1114
+ console.log(`\n**Files affected:**`)
1115
+ details.files.forEach((f) => {
1116
+ console.log(`- ${f.path}`)
1117
+ })
1118
+ }
1119
+ } catch (error) {
1120
+ EnhancedConsole.error(`Error generating stash changelog: ${error.message}`)
1121
+ }
1122
+ }
687
1123
  }
688
1124
 
689
1125
  // Export the controller
@@ -1134,24 +1134,6 @@ ${enhancedMergeSummary}
1134
1134
  : ''
1135
1135
  }
1136
1136
 
1137
- // Debug logging for merge commits with "pull request"
1138
- if (isMergeCommit && subject.includes('pull request')) {
1139
- try {
1140
- const fs = require('fs')
1141
- const debugContent = '=== EXACT AI INPUT FOR fd97ab7 ===\n' +
1142
- 'SUBJECT: ' + subject + '\n' +
1143
- 'FILES COUNT: ' + files.length + '\n' +
1144
- 'ENHANCED MERGE SUMMARY:\n' + (enhancedMergeSummary || 'None') + '\n\n' +
1145
- 'FILES SECTION (first 3000 chars):\n' + filesSection.substring(0, 3000) + '\n\n' +
1146
- 'FULL PROMPT PREVIEW:\n' + prompt.substring(0, 2000) + '\n=== END DEBUG ==='
1147
-
1148
- fs.writeFileSync('AI_INPUT_DEBUG.txt', debugContent)
1149
- console.log('*** AI INPUT SAVED TO AI_INPUT_DEBUG.txt ***')
1150
- } catch (error) {
1151
- console.log('DEBUG ERROR:', error.message)
1152
- }
1153
- }
1154
-
1155
1137
  **TARGET AUDIENCE:** End users and project stakeholders who need to understand what changed and why it matters.
1156
1138
 
1157
1139
  **YOUR ROLE:** You are a release manager translating technical changes into clear, user-focused release notes.