@grunnverk/kilde 0.1.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/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
- package/.github/pull_request_template.md +48 -0
- package/.github/workflows/deploy-docs.yml +59 -0
- package/.github/workflows/npm-publish.yml +48 -0
- package/.github/workflows/test.yml +48 -0
- package/CHANGELOG.md +92 -0
- package/CONTRIBUTING.md +438 -0
- package/LICENSE +190 -0
- package/PROJECT_SUMMARY.md +318 -0
- package/README.md +444 -0
- package/RELEASE_CHECKLIST.md +182 -0
- package/dist/application.js +166 -0
- package/dist/application.js.map +1 -0
- package/dist/commands/release.js +326 -0
- package/dist/commands/release.js.map +1 -0
- package/dist/constants.js +122 -0
- package/dist/constants.js.map +1 -0
- package/dist/logging.js +176 -0
- package/dist/logging.js.map +1 -0
- package/dist/main.js +24 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp-server.js +17467 -0
- package/dist/mcp-server.js.map +7 -0
- package/dist/utils/config.js +89 -0
- package/dist/utils/config.js.map +1 -0
- package/docs/AI_GUIDE.md +618 -0
- package/eslint.config.mjs +85 -0
- package/guide/architecture.md +776 -0
- package/guide/commands.md +580 -0
- package/guide/configuration.md +779 -0
- package/guide/mcp-integration.md +708 -0
- package/guide/overview.md +225 -0
- package/package.json +91 -0
- package/scripts/build-mcp.js +115 -0
- package/scripts/test-mcp-compliance.js +254 -0
- package/src/application.ts +246 -0
- package/src/commands/release.ts +450 -0
- package/src/constants.ts +162 -0
- package/src/logging.ts +210 -0
- package/src/main.ts +25 -0
- package/src/mcp/prompts/index.ts +98 -0
- package/src/mcp/resources.ts +121 -0
- package/src/mcp/server.ts +195 -0
- package/src/mcp/tools.ts +219 -0
- package/src/types.ts +131 -0
- package/src/utils/config.ts +181 -0
- package/tests/application.test.ts +114 -0
- package/tests/commands/commit.test.ts +248 -0
- package/tests/commands/release.test.ts +325 -0
- package/tests/constants.test.ts +118 -0
- package/tests/logging.test.ts +142 -0
- package/tests/mcp/prompts/index.test.ts +202 -0
- package/tests/mcp/resources.test.ts +166 -0
- package/tests/mcp/tools.test.ts +211 -0
- package/tests/utils/config.test.ts +212 -0
- package/tsconfig.json +32 -0
- package/vite.config.ts +107 -0
- package/vitest.config.ts +40 -0
- package/website/index.html +14 -0
- package/website/src/App.css +142 -0
- package/website/src/App.tsx +34 -0
- package/website/src/components/Commands.tsx +182 -0
- package/website/src/components/Configuration.tsx +214 -0
- package/website/src/components/Examples.tsx +234 -0
- package/website/src/components/Footer.css +99 -0
- package/website/src/components/Footer.tsx +93 -0
- package/website/src/components/GettingStarted.tsx +94 -0
- package/website/src/components/Hero.css +95 -0
- package/website/src/components/Hero.tsx +50 -0
- package/website/src/components/Navigation.css +102 -0
- package/website/src/components/Navigation.tsx +57 -0
- package/website/src/index.css +36 -0
- package/website/src/main.tsx +10 -0
- package/website/vite.config.ts +12 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Load .env file if it exists, but NEVER override existing environment variables
|
|
2
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
3
|
+
dotenvConfig({ override: false, debug: false });
|
|
4
|
+
|
|
5
|
+
import { setLogger as setGitLogger } from '@grunnverk/git-tools';
|
|
6
|
+
import { initializeTemplates } from '@grunnverk/ai-service';
|
|
7
|
+
import { Config } from '@grunnverk/core';
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
|
|
10
|
+
// Import commands
|
|
11
|
+
import * as CommandsGit from '@grunnverk/commands-git';
|
|
12
|
+
import * as ReleaseCommand from './commands/release';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
COMMAND_COMMIT,
|
|
16
|
+
COMMAND_RELEASE,
|
|
17
|
+
VERSION,
|
|
18
|
+
BUILD_HOSTNAME,
|
|
19
|
+
BUILD_TIMESTAMP,
|
|
20
|
+
KILDE_DEFAULTS,
|
|
21
|
+
PROGRAM_NAME
|
|
22
|
+
} from './constants';
|
|
23
|
+
import { getLogger, setLogLevel } from './logging';
|
|
24
|
+
import { getEffectiveConfig } from './utils/config';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check Node.js version and exit with clear error message if version is too old.
|
|
28
|
+
*/
|
|
29
|
+
function checkNodeVersion(): void {
|
|
30
|
+
const requiredMajorVersion = 24;
|
|
31
|
+
const currentVersion = process.version;
|
|
32
|
+
const majorVersion = parseInt(currentVersion.slice(1).split('.')[0], 10);
|
|
33
|
+
|
|
34
|
+
if (majorVersion < requiredMajorVersion) {
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
console.error(`\n❌ ERROR: Node.js version ${requiredMajorVersion}.0.0 or higher is required.`);
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
console.error(` Current version: ${currentVersion}`);
|
|
39
|
+
// eslint-disable-next-line no-console
|
|
40
|
+
console.error(` Please upgrade your Node.js version to continue.\n`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get formatted version information including build metadata.
|
|
47
|
+
*/
|
|
48
|
+
export function getVersionInfo(): { version: string; buildHostname: string; buildTimestamp: string; formatted: string } {
|
|
49
|
+
return {
|
|
50
|
+
version: VERSION,
|
|
51
|
+
buildHostname: BUILD_HOSTNAME,
|
|
52
|
+
buildTimestamp: BUILD_TIMESTAMP,
|
|
53
|
+
formatted: `${VERSION}\nBuilt on: ${BUILD_HOSTNAME}\nBuild time: ${BUILD_TIMESTAMP}`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse command line arguments and merge with config
|
|
59
|
+
*/
|
|
60
|
+
async function parseArguments(): Promise<{ commandName: string; config: Config }> {
|
|
61
|
+
const program = new Command();
|
|
62
|
+
|
|
63
|
+
program
|
|
64
|
+
.name(PROGRAM_NAME)
|
|
65
|
+
.description('Universal Git Automation Tool - AI-powered commit and release messages')
|
|
66
|
+
.version(VERSION)
|
|
67
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
68
|
+
.option('-d, --debug', 'Enable debug logging')
|
|
69
|
+
.option('--dry-run', 'Preview without making changes')
|
|
70
|
+
.option('--model <model>', 'AI model to use (default: gpt-4o-mini)')
|
|
71
|
+
.option('--reasoning <level>', 'OpenAI reasoning level: low, medium, high (default: low)');
|
|
72
|
+
|
|
73
|
+
// Commit command
|
|
74
|
+
program
|
|
75
|
+
.command('commit')
|
|
76
|
+
.description('Generate AI-powered commit message')
|
|
77
|
+
.option('--add', 'Stage all changes before committing')
|
|
78
|
+
.option('--cached', 'Only use staged changes')
|
|
79
|
+
.option('--sendit', 'Automatically commit with generated message')
|
|
80
|
+
.option('--interactive', 'Interactive mode for reviewing message')
|
|
81
|
+
.option('--amend', 'Amend the previous commit')
|
|
82
|
+
.option('--push [remote]', 'Push after committing (optionally specify remote)')
|
|
83
|
+
.option('--issue <number>', 'Reference issue number')
|
|
84
|
+
.option('--context <text>', 'Additional context for commit message')
|
|
85
|
+
.option('--context-files <files...>', 'Context files to include')
|
|
86
|
+
.action(async (options) => {
|
|
87
|
+
const config = await buildConfig('commit', options);
|
|
88
|
+
await executeCommand(COMMAND_COMMIT, config);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Release command
|
|
92
|
+
program
|
|
93
|
+
.command('release')
|
|
94
|
+
.description('Generate release notes from git history')
|
|
95
|
+
.option('--from-tag <tag>', 'Start tag for release notes')
|
|
96
|
+
.option('--to-tag <tag>', 'End tag for release notes (default: HEAD)')
|
|
97
|
+
.option('--version <version>', 'Version number for release')
|
|
98
|
+
.option('--output <file>', 'Output file path')
|
|
99
|
+
.option('--interactive', 'Interactive mode for reviewing notes')
|
|
100
|
+
.option('--focus <text>', 'Focus area for release notes')
|
|
101
|
+
.option('--context <text>', 'Additional context for release notes')
|
|
102
|
+
.option('--context-files <files...>', 'Context files to include')
|
|
103
|
+
.action(async (options) => {
|
|
104
|
+
const config = await buildConfig('release', options);
|
|
105
|
+
await executeCommand(COMMAND_RELEASE, config);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
program.parse();
|
|
109
|
+
|
|
110
|
+
// Return empty config if help or version was shown
|
|
111
|
+
return { commandName: '', config: KILDE_DEFAULTS };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build final configuration from defaults, config file, and CLI arguments
|
|
116
|
+
*/
|
|
117
|
+
async function buildConfig(commandName: string, cliOptions: any): Promise<Config> {
|
|
118
|
+
// Load config from file
|
|
119
|
+
const fileConfig = await getEffectiveConfig();
|
|
120
|
+
|
|
121
|
+
// Start with defaults
|
|
122
|
+
const config: Config = { ...KILDE_DEFAULTS };
|
|
123
|
+
|
|
124
|
+
// Merge file config
|
|
125
|
+
Object.assign(config, fileConfig);
|
|
126
|
+
|
|
127
|
+
// Merge CLI global options
|
|
128
|
+
if (cliOptions.parent.verbose !== undefined) config.verbose = cliOptions.parent.verbose;
|
|
129
|
+
if (cliOptions.parent.debug !== undefined) config.debug = cliOptions.parent.debug;
|
|
130
|
+
if (cliOptions.parent.dryRun !== undefined) config.dryRun = cliOptions.parent.dryRun;
|
|
131
|
+
if (cliOptions.parent.model) config.model = cliOptions.parent.model;
|
|
132
|
+
if (cliOptions.parent.reasoning) config.openaiReasoning = cliOptions.parent.reasoning;
|
|
133
|
+
|
|
134
|
+
// Merge command-specific options
|
|
135
|
+
if (commandName === 'commit') {
|
|
136
|
+
config.commit = config.commit || {};
|
|
137
|
+
if (cliOptions.add !== undefined) config.commit.add = cliOptions.add;
|
|
138
|
+
if (cliOptions.cached !== undefined) config.commit.cached = cliOptions.cached;
|
|
139
|
+
if (cliOptions.sendit !== undefined) config.commit.sendit = cliOptions.sendit;
|
|
140
|
+
if (cliOptions.interactive !== undefined) config.commit.interactive = cliOptions.interactive;
|
|
141
|
+
if (cliOptions.amend !== undefined) config.commit.amend = cliOptions.amend;
|
|
142
|
+
if (cliOptions.push !== undefined) config.commit.push = cliOptions.push;
|
|
143
|
+
if (cliOptions.context) config.commit.context = cliOptions.context;
|
|
144
|
+
if (cliOptions.contextFiles) config.commit.contextFiles = cliOptions.contextFiles;
|
|
145
|
+
} else if (commandName === 'release') {
|
|
146
|
+
config.release = config.release || {};
|
|
147
|
+
if (cliOptions.fromTag) config.release.from = cliOptions.fromTag;
|
|
148
|
+
if (cliOptions.toTag) config.release.to = cliOptions.toTag;
|
|
149
|
+
if (cliOptions.interactive !== undefined) config.release.interactive = cliOptions.interactive;
|
|
150
|
+
if (cliOptions.focus) config.release.focus = cliOptions.focus;
|
|
151
|
+
if (cliOptions.context) config.release.context = cliOptions.context;
|
|
152
|
+
if (cliOptions.contextFiles) config.release.contextFiles = cliOptions.contextFiles;
|
|
153
|
+
// Add non-standard fields as any
|
|
154
|
+
if (cliOptions.version) (config.release as any).version = cliOptions.version;
|
|
155
|
+
if (cliOptions.output) (config.release as any).output = cliOptions.output;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Return config (no validation needed - already built from defaults)
|
|
159
|
+
return config as unknown as Config;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Execute the specified command
|
|
164
|
+
*/
|
|
165
|
+
async function executeCommand(commandName: string, runConfig: Config): Promise<void> {
|
|
166
|
+
const logger = getLogger();
|
|
167
|
+
|
|
168
|
+
// Configure logging level
|
|
169
|
+
if (runConfig.verbose) {
|
|
170
|
+
setLogLevel('verbose');
|
|
171
|
+
}
|
|
172
|
+
if (runConfig.debug) {
|
|
173
|
+
setLogLevel('debug');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Configure external packages to use our logger
|
|
177
|
+
setGitLogger(logger);
|
|
178
|
+
|
|
179
|
+
logger.info('APPLICATION_STARTING: Kilde application initializing | Version: %s | BuildHost: %s | BuildTime: %s | Status: starting',
|
|
180
|
+
VERSION, BUILD_HOSTNAME, BUILD_TIMESTAMP);
|
|
181
|
+
|
|
182
|
+
logger.debug(`Executing command: ${commandName}`);
|
|
183
|
+
|
|
184
|
+
let summary: string = '';
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
if (commandName === COMMAND_COMMIT) {
|
|
188
|
+
summary = await CommandsGit.commit(runConfig);
|
|
189
|
+
} else if (commandName === COMMAND_RELEASE) {
|
|
190
|
+
const releaseSummary = await ReleaseCommand.execute(runConfig);
|
|
191
|
+
summary = `Release notes generated:\nTitle: ${releaseSummary.title}\n\n${releaseSummary.body}`;
|
|
192
|
+
} else {
|
|
193
|
+
throw new Error(`Unknown command: ${commandName}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (summary) {
|
|
197
|
+
logger.info('COMMAND_COMPLETE: Command executed successfully | Status: success');
|
|
198
|
+
if (runConfig.verbose || runConfig.debug) {
|
|
199
|
+
logger.info('COMMAND_SUMMARY: %s', summary);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
} catch (error: any) {
|
|
204
|
+
// Handle user cancellation gracefully
|
|
205
|
+
if (error.name === 'UserCancellationError' || error.message?.includes('cancelled')) {
|
|
206
|
+
logger.info('COMMAND_CANCELLED: Command cancelled by user | Status: cancelled');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Log and re-throw other errors
|
|
211
|
+
logger.error('COMMAND_FAILED: Command execution failed | Error: %s | Status: error', error.message);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Configure early logging based on command line flags.
|
|
218
|
+
*/
|
|
219
|
+
export function configureEarlyLogging(): void {
|
|
220
|
+
const hasVerbose = process.argv.includes('--verbose') || process.argv.includes('-v');
|
|
221
|
+
const hasDebug = process.argv.includes('--debug') || process.argv.includes('-d');
|
|
222
|
+
|
|
223
|
+
// Set log level based on early flag detection
|
|
224
|
+
if (hasDebug) {
|
|
225
|
+
setLogLevel('debug');
|
|
226
|
+
} else if (hasVerbose) {
|
|
227
|
+
setLogLevel('verbose');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Main application entry point
|
|
233
|
+
*/
|
|
234
|
+
export async function runApplication(): Promise<void> {
|
|
235
|
+
// Check Node.js version first
|
|
236
|
+
checkNodeVersion();
|
|
237
|
+
|
|
238
|
+
// Configure logging early
|
|
239
|
+
configureEarlyLogging();
|
|
240
|
+
|
|
241
|
+
// Initialize RiotPrompt templates for ai-service
|
|
242
|
+
initializeTemplates();
|
|
243
|
+
|
|
244
|
+
// Parse arguments and execute command
|
|
245
|
+
await parseArguments();
|
|
246
|
+
}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Pure Git-Only Release Notes Generator
|
|
4
|
+
*
|
|
5
|
+
* This is a simplified version of the release command that works with pure git
|
|
6
|
+
* operations only - no GitHub API dependencies, making it work with any git host
|
|
7
|
+
* (GitHub, GitLab, Bitbucket, self-hosted, etc.)
|
|
8
|
+
*/
|
|
9
|
+
import 'dotenv/config';
|
|
10
|
+
import type { ChatCompletionMessageParam } from 'openai/resources';
|
|
11
|
+
import { getDefaultFromRef, getCurrentBranch } from '@grunnverk/git-tools';
|
|
12
|
+
import { Formatter, Model } from '@riotprompt/riotprompt';
|
|
13
|
+
import {
|
|
14
|
+
Config,
|
|
15
|
+
Log,
|
|
16
|
+
Diff,
|
|
17
|
+
DEFAULT_EXCLUDED_PATTERNS,
|
|
18
|
+
DEFAULT_TO_COMMIT_ALIAS,
|
|
19
|
+
DEFAULT_OUTPUT_DIRECTORY,
|
|
20
|
+
DEFAULT_MAX_DIFF_BYTES,
|
|
21
|
+
improveContentWithLLM,
|
|
22
|
+
toAIConfig,
|
|
23
|
+
createStorageAdapter,
|
|
24
|
+
createLoggerAdapter,
|
|
25
|
+
getDryRunLogger,
|
|
26
|
+
getOutputPath,
|
|
27
|
+
getTimestampedRequestFilename,
|
|
28
|
+
getTimestampedResponseFilename,
|
|
29
|
+
getTimestampedReleaseNotesFilename,
|
|
30
|
+
validateReleaseSummary,
|
|
31
|
+
ReleaseSummary,
|
|
32
|
+
filterContent,
|
|
33
|
+
type LLMImprovementConfig,
|
|
34
|
+
} from '@grunnverk/core';
|
|
35
|
+
import {
|
|
36
|
+
createCompletionWithRetry,
|
|
37
|
+
getUserChoice,
|
|
38
|
+
editContentInEditor,
|
|
39
|
+
getLLMFeedbackInEditor,
|
|
40
|
+
requireTTY,
|
|
41
|
+
STANDARD_CHOICES,
|
|
42
|
+
ReleaseContext,
|
|
43
|
+
runAgenticRelease,
|
|
44
|
+
generateReflectionReport,
|
|
45
|
+
createReleasePrompt,
|
|
46
|
+
} from '@grunnverk/ai-service';
|
|
47
|
+
import { createStorage } from '@grunnverk/shared';
|
|
48
|
+
|
|
49
|
+
// Helper function to read context files
|
|
50
|
+
async function readContextFiles(contextFiles: string[] | undefined, logger: any): Promise<string> {
|
|
51
|
+
if (!contextFiles || contextFiles.length === 0) {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const storage = createStorage();
|
|
56
|
+
const contextParts: string[] = [];
|
|
57
|
+
|
|
58
|
+
for (const filePath of contextFiles) {
|
|
59
|
+
try {
|
|
60
|
+
const content = await storage.readFile(filePath, 'utf8');
|
|
61
|
+
contextParts.push(`## Context from ${filePath}\n\n${content}\n`);
|
|
62
|
+
logger.debug(`Read context from file: ${filePath}`);
|
|
63
|
+
} catch (error: any) {
|
|
64
|
+
logger.warn(`Failed to read context file ${filePath}: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return contextParts.join('\n---\n\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Helper function to edit release notes using editor
|
|
72
|
+
async function editReleaseNotesInteractively(releaseSummary: ReleaseSummary): Promise<ReleaseSummary> {
|
|
73
|
+
const templateLines = [
|
|
74
|
+
'# Edit your release notes below. Lines starting with "#" will be ignored.',
|
|
75
|
+
'# The first line is the title, everything else is the body.',
|
|
76
|
+
'# Save and close the editor when you are done.'
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const content = `${releaseSummary.title}\n\n${releaseSummary.body}`;
|
|
80
|
+
const result = await editContentInEditor(content, templateLines, '.md');
|
|
81
|
+
|
|
82
|
+
const lines = result.content.split('\n');
|
|
83
|
+
const title = lines[0].trim();
|
|
84
|
+
const body = lines.slice(1).join('\n').trim();
|
|
85
|
+
|
|
86
|
+
return { title, body };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Helper function to improve release notes using LLM
|
|
90
|
+
async function improveReleaseNotesWithLLM(
|
|
91
|
+
releaseSummary: ReleaseSummary,
|
|
92
|
+
runConfig: Config,
|
|
93
|
+
promptConfig: any,
|
|
94
|
+
promptContext: any,
|
|
95
|
+
outputDirectory: string,
|
|
96
|
+
logContent: string,
|
|
97
|
+
diffContent: string
|
|
98
|
+
): Promise<ReleaseSummary> {
|
|
99
|
+
// Get user feedback on what to improve using the editor
|
|
100
|
+
const releaseNotesContent = `${releaseSummary.title}\n\n${releaseSummary.body}`;
|
|
101
|
+
const userFeedback = await getLLMFeedbackInEditor('release notes', releaseNotesContent);
|
|
102
|
+
|
|
103
|
+
const improvementConfig: LLMImprovementConfig = {
|
|
104
|
+
contentType: 'release notes',
|
|
105
|
+
createImprovedPrompt: async (promptConfig, currentSummary, promptContext) => {
|
|
106
|
+
const improvementPromptContent = {
|
|
107
|
+
logContent: logContent,
|
|
108
|
+
diffContent: diffContent,
|
|
109
|
+
releaseFocus: `Please improve these release notes based on the user's feedback: "${userFeedback}".
|
|
110
|
+
|
|
111
|
+
Current release notes:
|
|
112
|
+
Title: "${currentSummary.title}"
|
|
113
|
+
Body: "${currentSummary.body}"
|
|
114
|
+
|
|
115
|
+
Please revise the release notes according to the user's feedback while maintaining accuracy and following good release note practices.`,
|
|
116
|
+
};
|
|
117
|
+
const promptResult = await createReleasePrompt(promptConfig, improvementPromptContent, promptContext);
|
|
118
|
+
// Format the prompt into a proper request with messages
|
|
119
|
+
const aiConfig = toAIConfig(runConfig);
|
|
120
|
+
const modelToUse = aiConfig.commands?.release?.model || aiConfig.model || 'gpt-4o-mini';
|
|
121
|
+
return Formatter.create({ logger: getDryRunLogger(false) }).formatPrompt(modelToUse as Model, promptResult.prompt);
|
|
122
|
+
},
|
|
123
|
+
callLLM: async (request, runConfig, outputDirectory) => {
|
|
124
|
+
const aiConfig = toAIConfig(runConfig);
|
|
125
|
+
const aiStorageAdapter = createStorageAdapter(outputDirectory);
|
|
126
|
+
const aiLogger = createLoggerAdapter(false);
|
|
127
|
+
const modelToUse = aiConfig.commands?.release?.model || aiConfig.model || 'gpt-4o-mini';
|
|
128
|
+
const openaiReasoning = aiConfig.commands?.release?.reasoning || aiConfig.reasoning;
|
|
129
|
+
return await createCompletionWithRetry(
|
|
130
|
+
request.messages as ChatCompletionMessageParam[],
|
|
131
|
+
{
|
|
132
|
+
model: modelToUse,
|
|
133
|
+
openaiReasoning,
|
|
134
|
+
responseFormat: { type: 'json_object' },
|
|
135
|
+
debug: runConfig.debug,
|
|
136
|
+
debugRequestFile: getOutputPath(outputDirectory, getTimestampedRequestFilename('release-improve')),
|
|
137
|
+
debugResponseFile: getOutputPath(outputDirectory, getTimestampedResponseFilename('release-improve')),
|
|
138
|
+
storage: aiStorageAdapter,
|
|
139
|
+
logger: aiLogger,
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
processResponse: (response: any) => {
|
|
144
|
+
return validateReleaseSummary(response);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return await improveContentWithLLM(
|
|
149
|
+
releaseSummary,
|
|
150
|
+
runConfig,
|
|
151
|
+
promptConfig,
|
|
152
|
+
promptContext,
|
|
153
|
+
outputDirectory,
|
|
154
|
+
improvementConfig
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Helper function to generate self-reflection output for release notes using observability module
|
|
159
|
+
async function generateSelfReflection(
|
|
160
|
+
agenticResult: any,
|
|
161
|
+
outputDirectory: string,
|
|
162
|
+
storage: any,
|
|
163
|
+
logger: any
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
try {
|
|
166
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('.')[0];
|
|
167
|
+
const reflectionPath = getOutputPath(outputDirectory, `agentic-reflection-release-${timestamp}.md`);
|
|
168
|
+
|
|
169
|
+
// Use new observability reflection generator
|
|
170
|
+
const report = await generateReflectionReport({
|
|
171
|
+
iterations: agenticResult.iterations || 0,
|
|
172
|
+
toolCallsExecuted: agenticResult.toolCallsExecuted || 0,
|
|
173
|
+
maxIterations: agenticResult.maxIterations || 30,
|
|
174
|
+
toolMetrics: agenticResult.toolMetrics || [],
|
|
175
|
+
conversationHistory: agenticResult.conversationHistory || [],
|
|
176
|
+
releaseNotes: agenticResult.releaseNotes,
|
|
177
|
+
logger
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Save the report to output directory
|
|
181
|
+
await storage.writeFile(reflectionPath, report, 'utf8');
|
|
182
|
+
|
|
183
|
+
logger.info('');
|
|
184
|
+
logger.info('═'.repeat(80));
|
|
185
|
+
logger.info('📊 SELF-REFLECTION REPORT GENERATED');
|
|
186
|
+
logger.info('═'.repeat(80));
|
|
187
|
+
logger.info('');
|
|
188
|
+
logger.info('📁 Location: %s', reflectionPath);
|
|
189
|
+
logger.info('');
|
|
190
|
+
logger.info('📈 Report Summary:');
|
|
191
|
+
const iterations = agenticResult.iterations || 0;
|
|
192
|
+
const toolCalls = agenticResult.toolCallsExecuted || 0;
|
|
193
|
+
const uniqueTools = new Set((agenticResult.toolMetrics || []).map((m: any) => m.name)).size;
|
|
194
|
+
logger.info(` • ${iterations} iterations completed`);
|
|
195
|
+
logger.info(` • ${toolCalls} tool calls executed`);
|
|
196
|
+
logger.info(` • ${uniqueTools} unique tools used`);
|
|
197
|
+
logger.info('');
|
|
198
|
+
logger.info('💡 Use this report to:');
|
|
199
|
+
logger.info(' • Understand which tools were most effective');
|
|
200
|
+
logger.info(' • Identify performance bottlenecks');
|
|
201
|
+
logger.info(' • Optimize tool selection and usage patterns');
|
|
202
|
+
logger.info(' • Improve agentic release notes generation');
|
|
203
|
+
logger.info('');
|
|
204
|
+
logger.info('═'.repeat(80));
|
|
205
|
+
} catch (error: any) {
|
|
206
|
+
logger.warn('Failed to generate self-reflection report: %s', error.message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Interactive feedback loop for release notes
|
|
211
|
+
async function handleInteractiveReleaseFeedback(
|
|
212
|
+
releaseSummary: ReleaseSummary,
|
|
213
|
+
runConfig: Config,
|
|
214
|
+
promptConfig: any,
|
|
215
|
+
promptContext: any,
|
|
216
|
+
outputDirectory: string,
|
|
217
|
+
storage: any,
|
|
218
|
+
logContent: string,
|
|
219
|
+
diffContent: string
|
|
220
|
+
): Promise<{ action: 'confirm' | 'skip', finalSummary: ReleaseSummary }> {
|
|
221
|
+
const logger = getDryRunLogger(false);
|
|
222
|
+
let currentSummary = releaseSummary;
|
|
223
|
+
|
|
224
|
+
while (true) {
|
|
225
|
+
// Display the current release notes
|
|
226
|
+
logger.info('\nRELEASE_NOTES_GENERATED: Generated release notes from AI | Title Length: ' + currentSummary.title.length + ' | Body Length: ' + currentSummary.body.length);
|
|
227
|
+
logger.info('─'.repeat(50));
|
|
228
|
+
logger.info('RELEASE_NOTES_TITLE: %s', currentSummary.title);
|
|
229
|
+
logger.info('');
|
|
230
|
+
logger.info('RELEASE_NOTES_BODY: Release notes content:');
|
|
231
|
+
logger.info(currentSummary.body);
|
|
232
|
+
logger.info('─'.repeat(50));
|
|
233
|
+
|
|
234
|
+
// Get user choice
|
|
235
|
+
const userChoice = await getUserChoice(
|
|
236
|
+
'\nWhat would you like to do with these release notes?',
|
|
237
|
+
[
|
|
238
|
+
STANDARD_CHOICES.CONFIRM,
|
|
239
|
+
STANDARD_CHOICES.EDIT,
|
|
240
|
+
STANDARD_CHOICES.SKIP,
|
|
241
|
+
STANDARD_CHOICES.IMPROVE
|
|
242
|
+
],
|
|
243
|
+
{
|
|
244
|
+
nonTtyErrorSuggestions: ['Use --dry-run to see the generated content without interaction']
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
switch (userChoice) {
|
|
249
|
+
case 'c':
|
|
250
|
+
return { action: 'confirm', finalSummary: currentSummary };
|
|
251
|
+
|
|
252
|
+
case 'e':
|
|
253
|
+
try {
|
|
254
|
+
currentSummary = await editReleaseNotesInteractively(currentSummary);
|
|
255
|
+
} catch (error: any) {
|
|
256
|
+
logger.error(`RELEASE_NOTES_EDIT_FAILED: Unable to edit release notes | Error: ${error.message} | Impact: Using original notes`);
|
|
257
|
+
// Continue the loop to show options again
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
|
|
261
|
+
case 's':
|
|
262
|
+
return { action: 'skip', finalSummary: currentSummary };
|
|
263
|
+
|
|
264
|
+
case 'i':
|
|
265
|
+
try {
|
|
266
|
+
currentSummary = await improveReleaseNotesWithLLM(
|
|
267
|
+
currentSummary,
|
|
268
|
+
runConfig,
|
|
269
|
+
promptConfig,
|
|
270
|
+
promptContext,
|
|
271
|
+
outputDirectory,
|
|
272
|
+
logContent,
|
|
273
|
+
diffContent
|
|
274
|
+
);
|
|
275
|
+
} catch (error: any) {
|
|
276
|
+
logger.error(`RELEASE_NOTES_IMPROVE_FAILED: Unable to improve release notes | Error: ${error.message} | Impact: Using current version`);
|
|
277
|
+
// Continue the loop to show options again
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
default:
|
|
282
|
+
// This shouldn't happen, but continue the loop
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Execute release notes generation (pure git version)
|
|
290
|
+
*
|
|
291
|
+
* This implementation uses ONLY git operations - no GitHub API calls.
|
|
292
|
+
* Works with any git repository regardless of host or language.
|
|
293
|
+
*/
|
|
294
|
+
export const execute = async (runConfig: Config): Promise<ReleaseSummary> => {
|
|
295
|
+
const isDryRun = runConfig.dryRun || false;
|
|
296
|
+
const logger = getDryRunLogger(isDryRun);
|
|
297
|
+
|
|
298
|
+
// Get current branch to help determine best tag comparison
|
|
299
|
+
const currentBranch = await getCurrentBranch();
|
|
300
|
+
|
|
301
|
+
// Resolve the from reference with fallback logic if not explicitly provided
|
|
302
|
+
const fromRef = runConfig.release?.from ?? await getDefaultFromRef(false, currentBranch);
|
|
303
|
+
const toRef = runConfig.release?.to ?? DEFAULT_TO_COMMIT_ALIAS;
|
|
304
|
+
|
|
305
|
+
logger.debug(`Using git references: from=${fromRef}, to=${toRef}`);
|
|
306
|
+
|
|
307
|
+
const log = await Log.create({
|
|
308
|
+
from: fromRef,
|
|
309
|
+
to: toRef,
|
|
310
|
+
limit: runConfig.release?.messageLimit
|
|
311
|
+
});
|
|
312
|
+
let logContent = '';
|
|
313
|
+
|
|
314
|
+
const maxDiffBytes = runConfig.release?.maxDiffBytes ?? DEFAULT_MAX_DIFF_BYTES;
|
|
315
|
+
const diff = await Diff.create({
|
|
316
|
+
from: fromRef,
|
|
317
|
+
to: toRef,
|
|
318
|
+
excludedPatterns: runConfig.excludedPatterns ?? DEFAULT_EXCLUDED_PATTERNS,
|
|
319
|
+
maxDiffBytes
|
|
320
|
+
});
|
|
321
|
+
let diffContent = '';
|
|
322
|
+
|
|
323
|
+
diffContent = await diff.get();
|
|
324
|
+
logContent = await log.get();
|
|
325
|
+
|
|
326
|
+
const promptConfig = {
|
|
327
|
+
overridePaths: runConfig.discoveredConfigDirs || [],
|
|
328
|
+
overrides: runConfig.overrides || false,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Always ensure output directory exists for request/response files
|
|
332
|
+
const outputDirectory = runConfig.outputDirectory || DEFAULT_OUTPUT_DIRECTORY;
|
|
333
|
+
const storage = createStorage();
|
|
334
|
+
await storage.ensureDirectory(outputDirectory);
|
|
335
|
+
|
|
336
|
+
// Create adapters for ai-service
|
|
337
|
+
const aiConfig = toAIConfig(runConfig);
|
|
338
|
+
const aiStorageAdapter = createStorageAdapter(outputDirectory);
|
|
339
|
+
const aiLogger = createLoggerAdapter(isDryRun);
|
|
340
|
+
|
|
341
|
+
// Read context from files if provided
|
|
342
|
+
const contextFromFiles = await readContextFiles(runConfig.release?.contextFiles, logger);
|
|
343
|
+
|
|
344
|
+
// Combine file context with existing context
|
|
345
|
+
const combinedContext = [
|
|
346
|
+
runConfig.release?.context,
|
|
347
|
+
contextFromFiles
|
|
348
|
+
].filter(Boolean).join('\n\n---\n\n');
|
|
349
|
+
|
|
350
|
+
// Run agentic release notes generation
|
|
351
|
+
// NOTE: No milestone/GitHub integration - pure git only
|
|
352
|
+
const agenticResult = await runAgenticRelease({
|
|
353
|
+
fromRef,
|
|
354
|
+
toRef,
|
|
355
|
+
logContent,
|
|
356
|
+
diffContent,
|
|
357
|
+
milestoneIssues: '', // Empty - no GitHub API integration
|
|
358
|
+
releaseFocus: runConfig.release?.focus,
|
|
359
|
+
userContext: combinedContext || undefined,
|
|
360
|
+
targetVersion: (runConfig.release as any)?.version,
|
|
361
|
+
model: aiConfig.commands?.release?.model || aiConfig.model || 'gpt-4o',
|
|
362
|
+
maxIterations: runConfig.release?.maxAgenticIterations || 30,
|
|
363
|
+
debug: runConfig.debug,
|
|
364
|
+
debugRequestFile: getOutputPath(outputDirectory, getTimestampedRequestFilename('release')),
|
|
365
|
+
debugResponseFile: getOutputPath(outputDirectory, getTimestampedResponseFilename('release')),
|
|
366
|
+
storage: aiStorageAdapter,
|
|
367
|
+
logger: aiLogger,
|
|
368
|
+
openaiReasoning: aiConfig.commands?.release?.reasoning || aiConfig.reasoning,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const iterations = agenticResult.iterations || 0;
|
|
372
|
+
const toolCalls = agenticResult.toolCallsExecuted || 0;
|
|
373
|
+
logger.info(`🔍 Analysis complete: ${iterations} iterations, ${toolCalls} tool calls`);
|
|
374
|
+
|
|
375
|
+
// Generate self-reflection output if enabled
|
|
376
|
+
if (runConfig.release?.selfReflection) {
|
|
377
|
+
await generateSelfReflection(agenticResult, outputDirectory, storage, logger);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Apply stop-context filtering to release notes
|
|
381
|
+
const titleFilterResult = filterContent(agenticResult.releaseNotes.title, runConfig.stopContext);
|
|
382
|
+
const bodyFilterResult = filterContent(agenticResult.releaseNotes.body, runConfig.stopContext);
|
|
383
|
+
let releaseSummary: ReleaseSummary = {
|
|
384
|
+
title: titleFilterResult.filtered,
|
|
385
|
+
body: bodyFilterResult.filtered,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// Handle interactive mode
|
|
389
|
+
if (runConfig.release?.interactive && !isDryRun) {
|
|
390
|
+
requireTTY('Interactive mode requires a terminal. Use --dry-run instead.');
|
|
391
|
+
|
|
392
|
+
const interactivePromptContext: ReleaseContext = {
|
|
393
|
+
context: combinedContext || undefined,
|
|
394
|
+
directories: runConfig.contextDirectories,
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const interactiveResult = await handleInteractiveReleaseFeedback(
|
|
398
|
+
releaseSummary,
|
|
399
|
+
runConfig,
|
|
400
|
+
promptConfig,
|
|
401
|
+
interactivePromptContext,
|
|
402
|
+
outputDirectory,
|
|
403
|
+
storage,
|
|
404
|
+
logContent,
|
|
405
|
+
diffContent
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
if (interactiveResult.action === 'skip') {
|
|
409
|
+
logger.info('RELEASE_ABORTED: Release notes generation aborted by user | Reason: User choice | Status: cancelled');
|
|
410
|
+
} else {
|
|
411
|
+
logger.info('RELEASE_FINALIZED: Release notes finalized and accepted | Status: ready | Next: Create release or save');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
releaseSummary = interactiveResult.finalSummary;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Save timestamped copy of release notes to output directory
|
|
418
|
+
try {
|
|
419
|
+
const timestampedFilename = getTimestampedReleaseNotesFilename();
|
|
420
|
+
const outputPath = getOutputPath(outputDirectory, timestampedFilename);
|
|
421
|
+
|
|
422
|
+
// Format the release notes as markdown
|
|
423
|
+
const releaseNotesContent = `# ${releaseSummary.title}\n\n${releaseSummary.body}`;
|
|
424
|
+
|
|
425
|
+
await storage.writeFile(outputPath, releaseNotesContent, 'utf-8');
|
|
426
|
+
logger.debug('Saved timestamped release notes: %s', outputPath);
|
|
427
|
+
} catch (error: any) {
|
|
428
|
+
logger.warn('RELEASE_SAVE_FAILED: Failed to save timestamped release notes | Error: %s | Impact: Notes not persisted to file', error.message);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Save to specified output file if provided
|
|
432
|
+
const outputFile = (runConfig.release as any)?.output;
|
|
433
|
+
if (outputFile) {
|
|
434
|
+
try {
|
|
435
|
+
const releaseNotesContent = `# ${releaseSummary.title}\n\n${releaseSummary.body}`;
|
|
436
|
+
await storage.writeFile(outputFile, releaseNotesContent, 'utf-8');
|
|
437
|
+
logger.info(`Saved release notes to: ${outputFile}`);
|
|
438
|
+
} catch (error: any) {
|
|
439
|
+
logger.error(`Failed to save release notes to ${outputFile}: ${error.message}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (isDryRun) {
|
|
444
|
+
logger.info('RELEASE_SUMMARY_COMPLETE: Generated release summary successfully | Status: completed');
|
|
445
|
+
logger.info('RELEASE_SUMMARY_TITLE: %s', releaseSummary.title);
|
|
446
|
+
logger.info('RELEASE_SUMMARY_BODY: %s', releaseSummary.body);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return releaseSummary;
|
|
450
|
+
}
|