@eldrforge/kodrdriv 1.2.124 → 1.2.126

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.
@@ -5,7 +5,7 @@ import { DEFAULT_TO_COMMIT_ALIAS, DEFAULT_MAX_DIFF_BYTES, DEFAULT_EXCLUDED_PATTE
5
5
  import { getCurrentBranch, getDefaultFromRef, safeJsonParse } from '@eldrforge/git-tools';
6
6
  import { create } from '../content/log.js';
7
7
  import { create as create$1, truncateDiffByFiles } from '../content/diff.js';
8
- import { createReleasePrompt, createCompletionWithRetry, requireTTY, getUserChoice, STANDARD_CHOICES, getLLMFeedbackInEditor, editContentInEditor } from '@eldrforge/ai-service';
8
+ import { runAgenticRelease, requireTTY, createReleasePrompt, createCompletionWithRetry, getUserChoice, STANDARD_CHOICES, getLLMFeedbackInEditor, editContentInEditor } from '@eldrforge/ai-service';
9
9
  import { improveContentWithLLM } from '../util/interactive.js';
10
10
  import { toAIConfig } from '../util/aiAdapter.js';
11
11
  import { createStorageAdapter } from '../util/storageAdapter.js';
@@ -15,6 +15,7 @@ import { getOutputPath, getTimestampedResponseFilename, getTimestampedRequestFil
15
15
  import { createStorage } from '@eldrforge/shared';
16
16
  import { validateReleaseSummary } from '../util/validation.js';
17
17
  import * as GitHub from '@eldrforge/github-tools';
18
+ import { filterContent } from '../util/stopContext.js';
18
19
 
19
20
  // Helper function to edit release notes using editor
20
21
  async function editReleaseNotesInteractively(releaseSummary) {
@@ -87,6 +88,149 @@ Please revise the release notes according to the user's feedback while maintaini
87
88
  };
88
89
  return await improveContentWithLLM(releaseSummary, runConfig, promptConfig, promptContext, outputDirectory, improvementConfig);
89
90
  }
91
+ // Helper function to generate self-reflection output for release notes
92
+ async function generateSelfReflection(agenticResult, outputDirectory, storage, logger) {
93
+ try {
94
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('.')[0];
95
+ const reflectionPath = getOutputPath(outputDirectory, `agentic-reflection-release-${timestamp}.md`);
96
+ // Calculate tool effectiveness metrics
97
+ const toolMetrics = agenticResult.toolMetrics || [];
98
+ const toolStats = new Map();
99
+ for (const metric of toolMetrics){
100
+ if (!toolStats.has(metric.name)) {
101
+ toolStats.set(metric.name, {
102
+ total: 0,
103
+ success: 0,
104
+ failures: 0,
105
+ totalDuration: 0
106
+ });
107
+ }
108
+ const stats = toolStats.get(metric.name);
109
+ stats.total++;
110
+ stats.totalDuration += metric.duration;
111
+ if (metric.success) {
112
+ stats.success++;
113
+ } else {
114
+ stats.failures++;
115
+ }
116
+ }
117
+ // Build reflection document
118
+ const sections = [];
119
+ sections.push('# Agentic Release Notes - Self-Reflection Report');
120
+ sections.push('');
121
+ sections.push(`Generated: ${new Date().toISOString()}`);
122
+ sections.push('');
123
+ sections.push('## Execution Summary');
124
+ sections.push('');
125
+ sections.push(`- **Iterations**: ${agenticResult.iterations}`);
126
+ sections.push(`- **Tool Calls**: ${agenticResult.toolCallsExecuted}`);
127
+ sections.push(`- **Unique Tools Used**: ${toolStats.size}`);
128
+ sections.push('');
129
+ sections.push('## Tool Effectiveness Analysis');
130
+ sections.push('');
131
+ if (toolStats.size === 0) {
132
+ sections.push('*No tools were called during execution.*');
133
+ sections.push('');
134
+ } else {
135
+ sections.push('| Tool | Calls | Success Rate | Avg Duration | Total Time |');
136
+ sections.push('|------|-------|--------------|--------------|------------|');
137
+ const sortedTools = Array.from(toolStats.entries()).sort((a, b)=>b[1].total - a[1].total);
138
+ for (const [toolName, stats] of sortedTools){
139
+ const successRate = (stats.success / stats.total * 100).toFixed(1);
140
+ const avgDuration = (stats.totalDuration / stats.total).toFixed(0);
141
+ const totalTime = stats.totalDuration.toFixed(0);
142
+ sections.push(`| ${toolName} | ${stats.total} | ${successRate}% | ${avgDuration}ms | ${totalTime}ms |`);
143
+ }
144
+ sections.push('');
145
+ }
146
+ // Tool usage insights
147
+ sections.push('## Tool Usage Insights');
148
+ sections.push('');
149
+ if (toolStats.size > 0) {
150
+ const mostUsedTool = Array.from(toolStats.entries()).sort((a, b)=>b[1].total - a[1].total)[0];
151
+ sections.push(`- **Most Used Tool**: \`${mostUsedTool[0]}\` (${mostUsedTool[1].total} calls)`);
152
+ const slowestTool = Array.from(toolStats.entries()).sort((a, b)=>b[1].totalDuration / b[1].total - a[1].totalDuration / a[1].total)[0];
153
+ const slowestAvg = (slowestTool[1].totalDuration / slowestTool[1].total).toFixed(0);
154
+ sections.push(`- **Slowest Tool**: \`${slowestTool[0]}\` (${slowestAvg}ms average)`);
155
+ const failedTools = Array.from(toolStats.entries()).filter(([_, stats])=>stats.failures > 0);
156
+ if (failedTools.length > 0) {
157
+ sections.push(`- **Tools with Failures**: ${failedTools.length} tool(s) had at least one failure`);
158
+ for (const [toolName, stats] of failedTools){
159
+ sections.push(` - \`${toolName}\`: ${stats.failures}/${stats.total} calls failed`);
160
+ }
161
+ } else {
162
+ sections.push('- **Reliability**: All tool calls succeeded ✓');
163
+ }
164
+ }
165
+ sections.push('');
166
+ // Execution patterns
167
+ sections.push('## Execution Patterns');
168
+ sections.push('');
169
+ const iterationsPerToolCall = agenticResult.toolCallsExecuted > 0 ? (agenticResult.iterations / agenticResult.toolCallsExecuted).toFixed(2) : 'N/A';
170
+ sections.push(`- **Iterations per Tool Call**: ${iterationsPerToolCall}`);
171
+ const totalExecutionTime = Array.from(toolStats.values()).reduce((sum, stats)=>sum + stats.totalDuration, 0);
172
+ sections.push(`- **Total Tool Execution Time**: ${totalExecutionTime.toFixed(0)}ms`);
173
+ if (agenticResult.toolCallsExecuted > 0) {
174
+ const avgTimePerCall = (totalExecutionTime / agenticResult.toolCallsExecuted).toFixed(0);
175
+ sections.push(`- **Average Time per Tool Call**: ${avgTimePerCall}ms`);
176
+ }
177
+ sections.push('');
178
+ // Recommendations
179
+ sections.push('## Recommendations');
180
+ sections.push('');
181
+ const recommendations = [];
182
+ const failedTools = Array.from(toolStats.entries()).filter(([_, stats])=>stats.failures > 0);
183
+ if (failedTools.length > 0) {
184
+ recommendations.push('- **Tool Reliability**: Some tools failed during execution. Review error messages and consider improving error handling or tool implementation.');
185
+ }
186
+ const slowTools = Array.from(toolStats.entries()).filter(([_, stats])=>stats.totalDuration / stats.total > 1000);
187
+ if (slowTools.length > 0) {
188
+ recommendations.push('- **Performance**: Consider optimizing slow tools or caching results to improve execution speed.');
189
+ }
190
+ if (agenticResult.iterations >= (agenticResult.maxIterations || 30)) {
191
+ recommendations.push('- **Max Iterations Reached**: The agent reached maximum iterations. Consider increasing the limit or improving tool efficiency to allow the agent to complete naturally.');
192
+ }
193
+ const underutilizedTools = Array.from(toolStats.entries()).filter(([_, stats])=>stats.total === 1);
194
+ if (underutilizedTools.length > 3) {
195
+ recommendations.push('- **Underutilized Tools**: Many tools were called only once. Consider whether all tools are necessary or if the agent needs better guidance on when to use them.');
196
+ }
197
+ if (agenticResult.toolCallsExecuted === 0) {
198
+ recommendations.push('- **No Tools Used**: The agent completed without calling any tools. This might indicate the initial prompt provided sufficient information, or the agent may benefit from more explicit guidance to use tools.');
199
+ }
200
+ if (recommendations.length === 0) {
201
+ sections.push('*No specific recommendations at this time. Execution appears optimal.*');
202
+ } else {
203
+ for (const rec of recommendations){
204
+ sections.push(rec);
205
+ }
206
+ }
207
+ sections.push('');
208
+ // Write the reflection file
209
+ const reflectionContent = sections.join('\n');
210
+ await storage.writeFile(reflectionPath, reflectionContent, 'utf-8');
211
+ logger.info('');
212
+ logger.info('═'.repeat(80));
213
+ logger.info('📊 SELF-REFLECTION REPORT GENERATED');
214
+ logger.info('═'.repeat(80));
215
+ logger.info('');
216
+ logger.info('📁 Location: %s', reflectionPath);
217
+ logger.info('');
218
+ logger.info('📈 Report Summary:');
219
+ logger.info(' • %d iterations completed', agenticResult.iterations);
220
+ logger.info(' • %d tool calls executed', agenticResult.toolCallsExecuted);
221
+ logger.info(' • %d unique tools used', toolStats.size);
222
+ logger.info('');
223
+ logger.info('💡 Use this report to:');
224
+ logger.info(' • Understand which tools were most effective');
225
+ logger.info(' • Identify performance bottlenecks');
226
+ logger.info(' • Optimize tool selection and usage patterns');
227
+ logger.info(' • Improve agentic release notes generation');
228
+ logger.info('');
229
+ logger.info('═'.repeat(80));
230
+ } catch (error) {
231
+ logger.warn('Failed to generate self-reflection report: %s', error.message);
232
+ }
233
+ }
90
234
  // Interactive feedback loop for release notes
91
235
  async function handleInteractiveReleaseFeedback(releaseSummary, runConfig, promptConfig, promptContext, outputDirectory, storage, logContent, diffContent) {
92
236
  const logger = getDryRunLogger(false);
@@ -142,7 +286,7 @@ async function handleInteractiveReleaseFeedback(releaseSummary, runConfig, promp
142
286
  }
143
287
  }
144
288
  const execute = async (runConfig)=>{
145
- var _runConfig_release, _runConfig_release1, _runConfig_release2, _runConfig_release3, _runConfig_release4, _runConfig_release5, _runConfig_release6, _runConfig_release7, _runConfig_release8, _aiConfig_commands_release, _aiConfig_commands, _aiConfig_commands_release1, _aiConfig_commands1, _runConfig_release9;
289
+ var _runConfig_release, _runConfig_release1, _runConfig_release2, _runConfig_release3, _runConfig_release4, _runConfig_release5, _runConfig_release6, _runConfig_release7, _runConfig_release8, _runConfig_release9, _aiConfig_commands_release, _aiConfig_commands, _aiConfig_commands_release1, _aiConfig_commands1, _runConfig_release10;
146
290
  const isDryRun = runConfig.dryRun || false;
147
291
  const logger = getDryRunLogger(isDryRun);
148
292
  // Get current branch to help determine best tag comparison
@@ -233,14 +377,87 @@ const execute = async (runConfig)=>{
233
377
  const aiConfig = toAIConfig(runConfig);
234
378
  const aiStorageAdapter = createStorageAdapter();
235
379
  const aiLogger = createLoggerAdapter(isDryRun);
380
+ // Always ensure output directory exists for request/response files
381
+ const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
382
+ const storage = createStorage();
383
+ await storage.ensureDirectory(outputDirectory);
384
+ // Check if agentic mode is enabled
385
+ if ((_runConfig_release7 = runConfig.release) === null || _runConfig_release7 === void 0 ? void 0 : _runConfig_release7.agentic) {
386
+ var _runConfig_release11, _runConfig_release12, _aiConfig_commands_release2, _aiConfig_commands2, _runConfig_release13, _aiConfig_commands_release3, _aiConfig_commands3, _runConfig_release14, _runConfig_release15;
387
+ logger.info('🤖 Using agentic mode for release notes generation');
388
+ // Run agentic release notes generation
389
+ const agenticResult = await runAgenticRelease({
390
+ fromRef,
391
+ toRef,
392
+ logContent,
393
+ diffContent,
394
+ milestoneIssues: milestoneIssuesContent,
395
+ releaseFocus: (_runConfig_release11 = runConfig.release) === null || _runConfig_release11 === void 0 ? void 0 : _runConfig_release11.focus,
396
+ userContext: (_runConfig_release12 = runConfig.release) === null || _runConfig_release12 === void 0 ? void 0 : _runConfig_release12.context,
397
+ model: ((_aiConfig_commands2 = aiConfig.commands) === null || _aiConfig_commands2 === void 0 ? void 0 : (_aiConfig_commands_release2 = _aiConfig_commands2.release) === null || _aiConfig_commands_release2 === void 0 ? void 0 : _aiConfig_commands_release2.model) || aiConfig.model || 'gpt-4o',
398
+ maxIterations: ((_runConfig_release13 = runConfig.release) === null || _runConfig_release13 === void 0 ? void 0 : _runConfig_release13.maxAgenticIterations) || 30,
399
+ debug: runConfig.debug,
400
+ debugRequestFile: getOutputPath(outputDirectory, getTimestampedRequestFilename('release-agentic')),
401
+ debugResponseFile: getOutputPath(outputDirectory, getTimestampedResponseFilename('release-agentic')),
402
+ storage: aiStorageAdapter,
403
+ logger: aiLogger,
404
+ openaiReasoning: ((_aiConfig_commands3 = aiConfig.commands) === null || _aiConfig_commands3 === void 0 ? void 0 : (_aiConfig_commands_release3 = _aiConfig_commands3.release) === null || _aiConfig_commands_release3 === void 0 ? void 0 : _aiConfig_commands_release3.reasoning) || aiConfig.reasoning
405
+ });
406
+ logger.info('🔍 Agentic analysis complete: %d iterations, %d tool calls', agenticResult.iterations, agenticResult.toolCallsExecuted);
407
+ // Generate self-reflection output if enabled
408
+ if ((_runConfig_release14 = runConfig.release) === null || _runConfig_release14 === void 0 ? void 0 : _runConfig_release14.selfReflection) {
409
+ await generateSelfReflection(agenticResult, outputDirectory, storage, logger);
410
+ }
411
+ // Apply stop-context filtering to release notes
412
+ const titleFilterResult = filterContent(agenticResult.releaseNotes.title, runConfig.stopContext);
413
+ const bodyFilterResult = filterContent(agenticResult.releaseNotes.body, runConfig.stopContext);
414
+ let releaseSummary = {
415
+ title: titleFilterResult.filtered,
416
+ body: bodyFilterResult.filtered
417
+ };
418
+ // Handle interactive mode
419
+ if (((_runConfig_release15 = runConfig.release) === null || _runConfig_release15 === void 0 ? void 0 : _runConfig_release15.interactive) && !isDryRun) {
420
+ var _runConfig_release16;
421
+ requireTTY('Interactive mode requires a terminal. Use --dry-run instead.');
422
+ const interactivePromptContext = {
423
+ context: (_runConfig_release16 = runConfig.release) === null || _runConfig_release16 === void 0 ? void 0 : _runConfig_release16.context,
424
+ directories: runConfig.contextDirectories
425
+ };
426
+ const interactiveResult = await handleInteractiveReleaseFeedback(releaseSummary, runConfig, promptConfig, interactivePromptContext, outputDirectory, storage, logContent, diffContent);
427
+ if (interactiveResult.action === 'skip') {
428
+ logger.info('RELEASE_ABORTED: Release notes generation aborted by user | Reason: User choice | Status: cancelled');
429
+ } else {
430
+ logger.info('RELEASE_FINALIZED: Release notes finalized and accepted | Status: ready | Next: Create release or save');
431
+ }
432
+ releaseSummary = interactiveResult.finalSummary;
433
+ }
434
+ // Save timestamped copy of release notes to output directory
435
+ try {
436
+ const timestampedFilename = getTimestampedReleaseNotesFilename();
437
+ const outputPath = getOutputPath(outputDirectory, timestampedFilename);
438
+ // Format the release notes as markdown
439
+ const releaseNotesContent = `# ${releaseSummary.title}\n\n${releaseSummary.body}`;
440
+ await storage.writeFile(outputPath, releaseNotesContent, 'utf-8');
441
+ logger.debug('Saved timestamped release notes: %s', outputPath);
442
+ } catch (error) {
443
+ logger.warn('RELEASE_SAVE_FAILED: Failed to save timestamped release notes | Error: %s | Impact: Notes not persisted to file', error.message);
444
+ }
445
+ if (isDryRun) {
446
+ logger.info('RELEASE_SUMMARY_COMPLETE: Generated release summary successfully | Status: completed');
447
+ logger.info('RELEASE_SUMMARY_TITLE: %s', releaseSummary.title);
448
+ logger.info('RELEASE_SUMMARY_BODY: %s', releaseSummary.body);
449
+ }
450
+ return releaseSummary;
451
+ }
452
+ // Non-agentic mode: use traditional prompt-based approach
236
453
  const promptContent = {
237
454
  logContent,
238
455
  diffContent,
239
- releaseFocus: (_runConfig_release7 = runConfig.release) === null || _runConfig_release7 === void 0 ? void 0 : _runConfig_release7.focus,
456
+ releaseFocus: (_runConfig_release8 = runConfig.release) === null || _runConfig_release8 === void 0 ? void 0 : _runConfig_release8.focus,
240
457
  milestoneIssues: milestoneIssuesContent
241
458
  };
242
459
  const promptContext = {
243
- context: (_runConfig_release8 = runConfig.release) === null || _runConfig_release8 === void 0 ? void 0 : _runConfig_release8.context,
460
+ context: (_runConfig_release9 = runConfig.release) === null || _runConfig_release9 === void 0 ? void 0 : _runConfig_release9.context,
244
461
  directories: runConfig.contextDirectories
245
462
  };
246
463
  const promptResult = await createReleasePrompt(promptConfig, promptContent, promptContext);
@@ -248,10 +465,6 @@ const execute = async (runConfig)=>{
248
465
  const request = Formatter.create({
249
466
  logger
250
467
  }).formatPrompt(modelToUse, promptResult.prompt);
251
- // Always ensure output directory exists for request/response files
252
- const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
253
- const storage = createStorage();
254
- await storage.ensureDirectory(outputDirectory);
255
468
  logger.debug('Release analysis: isLargeRelease=%s, maxTokens=%d', promptResult.isLargeRelease, promptResult.maxTokens);
256
469
  // Create retry callback that reduces diff size on token limit errors
257
470
  const createRetryCallback = (originalDiffContent, originalLogContent)=>async (attempt)=>{
@@ -294,9 +507,16 @@ const execute = async (runConfig)=>{
294
507
  logger: aiLogger
295
508
  }, createRetryCallback(diffContent, logContent));
296
509
  // Validate and safely cast the response
297
- let releaseSummary = validateReleaseSummary(summary);
510
+ const rawReleaseSummary = validateReleaseSummary(summary);
511
+ // Apply stop-context filtering to release notes
512
+ const titleFilterResult = filterContent(rawReleaseSummary.title, runConfig.stopContext);
513
+ const bodyFilterResult = filterContent(rawReleaseSummary.body, runConfig.stopContext);
514
+ let releaseSummary = {
515
+ title: titleFilterResult.filtered,
516
+ body: bodyFilterResult.filtered
517
+ };
298
518
  // Handle interactive mode
299
- if (((_runConfig_release9 = runConfig.release) === null || _runConfig_release9 === void 0 ? void 0 : _runConfig_release9.interactive) && !isDryRun) {
519
+ if (((_runConfig_release10 = runConfig.release) === null || _runConfig_release10 === void 0 ? void 0 : _runConfig_release10.interactive) && !isDryRun) {
300
520
  requireTTY('Interactive mode requires a terminal. Use --dry-run instead.');
301
521
  const interactiveResult = await handleInteractiveReleaseFeedback(releaseSummary, runConfig, promptConfig, promptContext, outputDirectory, storage, logContent, diffContent);
302
522
  if (interactiveResult.action === 'skip') {