import * as plugins from '../plugins.js'; import { AiDoc } from '../classes.aidoc.js'; import { ProjectContext } from './projectcontext.js'; import { DiffProcessor } from '../classes.diffprocessor.js'; import { logger } from '../logging.js'; export interface INextCommitObject { recommendedNextVersionLevel: 'fix' | 'feat' | 'BREAKING CHANGE'; // the recommended next version level of the project recommendedNextVersionScope: string; // the recommended scope name of the next version, like "core" or "cli", or specific class names. recommendedNextVersionMessage: string; // the commit message. Don't put fix() feat() or BREAKING CHANGE in the message. Please just the message itself. recommendedNextVersionDetails: string[]; // detailed bullet points for the changelog recommendedNextVersion: string; // the recommended next version of the project, x.x.x changelog?: string; // the changelog for the next version } export class Commit { private aiDocsRef: AiDoc; private projectDir: string; constructor(aiDocsRef: AiDoc, projectDirArg: string) { this.aiDocsRef = aiDocsRef; this.projectDir = projectDirArg; } public async buildNextCommitObject(): Promise { const smartgitInstance = new plugins.smartgit.Smartgit(); await smartgitInstance.init(); const gitRepo = await plugins.smartgit.GitRepo.fromOpeningRepoDir( smartgitInstance, this.projectDir ); // Define comprehensive exclusion patterns // smartgit@3.3.0+ supports glob patterns natively const excludePatterns = [ // Lock files 'pnpm-lock.yaml', 'package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock', 'deno.lock', 'bun.lockb', // Build artifacts (main culprit for large diffs!) 'dist/**', 'dist_*/**', // dist_ts, dist_web, etc. 'build/**', '.next/**', 'out/**', 'public/dist/**', // Compiled/bundled files '**/*.js.map', '**/*.d.ts.map', '**/*.min.js', '**/*.bundle.js', '**/*.chunk.js', // IDE/Editor directories '.claude/**', '.cursor/**', '.vscode/**', '.idea/**', '**/*.swp', '**/*.swo', // Logs and caches '.nogit/**', '**/*.log', '.cache/**', '.rpt2_cache/**', 'coverage/**', '.nyc_output/**', ]; // Pass glob patterns directly to smartgit - it handles matching internally const diffStringArray = await gitRepo.getUncommittedDiff(excludePatterns); // Process diffs intelligently using DiffProcessor let processedDiffString: string; if (diffStringArray.length > 0) { // Diagnostic logging for raw diff statistics const totalChars = diffStringArray.join('\n\n').length; const estimatedTokens = Math.ceil(totalChars / 4); console.log(`📊 Raw git diff statistics:`); console.log(` Files changed: ${diffStringArray.length}`); console.log(` Total characters: ${totalChars.toLocaleString()}`); console.log(` Estimated tokens: ${estimatedTokens.toLocaleString()}`); console.log(` Exclusion patterns: ${excludePatterns.length}`); // Use DiffProcessor to intelligently handle large diffs const diffProcessor = new DiffProcessor({ maxDiffTokens: 100000, // Reserve 100k tokens for diffs smallFileLines: 300, // Most source files are under 300 lines mediumFileLines: 800, // Only very large files get head/tail treatment sampleHeadLines: 75, // When sampling, show more context sampleTailLines: 75, // When sampling, show more context }); const processedDiff = diffProcessor.processDiffs(diffStringArray); processedDiffString = diffProcessor.formatForContext(processedDiff); console.log(`📝 Processed diff statistics:`); console.log(` Full diffs: ${processedDiff.fullDiffs.length} files`); console.log(` Summarized: ${processedDiff.summarizedDiffs.length} files`); console.log(` Metadata only: ${processedDiff.metadataOnly.length} files`); console.log(` Final tokens: ${processedDiff.totalTokens.toLocaleString()}`); if (estimatedTokens > 50000) { console.log(`✅ DiffProcessor reduced token usage: ${estimatedTokens.toLocaleString()} → ${processedDiff.totalTokens.toLocaleString()}`); } } else { processedDiffString = 'No changes.'; } // Use DualAgentOrchestrator for commit message generation const commitOrchestrator = new plugins.smartagent.DualAgentOrchestrator({ smartAiInstance: this.aiDocsRef.smartAiInstance, defaultProvider: 'openai', logPrefix: '[Commit]', onProgress: (event) => logger.log(event.logLevel, event.logMessage), guardianPolicyPrompt: ` You validate commit messages for semantic versioning compliance. APPROVE tool calls for: - Reading package.json or source files to understand project context - Using tree to see project structure - Listing directory contents REJECT tool calls for: - Reading files outside the project directory - Writing, deleting, or modifying any files - Any destructive operations APPROVE final output if: - Version level (fix/feat/BREAKING CHANGE) matches the scope of changes in the diff - Commit message is clear, professional, and follows conventional commit conventions - No personal information, licensing details, or AI mentions (Claude/Codex) included - JSON structure is valid with all required fields - Scope accurately reflects the changed modules/files REJECT final output if: - Version level doesn't match the scope of changes (e.g., "feat" for a typo fix should be "fix") - Message is vague, unprofessional, or contains sensitive information - JSON is malformed or missing required fields `, }); // Register scoped filesystem tool for agent exploration commitOrchestrator.registerScopedFilesystemTool(this.projectDir, [ '.nogit/**', 'node_modules/**', '.git/**', 'dist/**', 'dist_*/**', ]); await commitOrchestrator.start(); const commitTaskPrompt = ` You create a commit message for a git commit. Project directory: ${this.projectDir} You have access to a filesystem tool to explore the project if needed: - Use tree to see project structure - Use read to read package.json or source files for context Analyze the git diff below to understand what changed and generate a commit message. You should not include any licensing information or personal information. Never mention CLAUDE code, or codex. Your final output (inside the task_complete tags) must be ONLY valid JSON - the raw JSON object, nothing else. No explanations, no summaries, no markdown - just the JSON object that can be parsed with JSON.parse(). Here is the structure of the JSON you must return: { "recommendedNextVersionLevel": "fix" | "feat" | "BREAKING CHANGE", "recommendedNextVersionScope": "string", "recommendedNextVersionMessage": "string", "recommendedNextVersionDetails": ["string"], "recommendedNextVersion": "x.x.x" } For recommendedNextVersionDetails, only add entries that have obvious value to the reader. Here is the git diff showing what changed: ${processedDiffString} Analyze these changes and output the JSON commit message object. `; const commitResult = await commitOrchestrator.run(commitTaskPrompt); await commitOrchestrator.stop(); if (!commitResult.success) { throw new Error(`Commit message generation failed: ${commitResult.status}`); } // Extract JSON from result - handle cases where AI adds text around it let jsonString = commitResult.result .replace(/```json\n?/gi, '') .replace(/```\n?/gi, ''); // Try to find JSON object in the result const jsonMatch = jsonString.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw new Error(`Could not find JSON object in result: ${jsonString.substring(0, 100)}...`); } jsonString = jsonMatch[0]; const resultObject: INextCommitObject = JSON.parse(jsonString); const previousChangelogPath = plugins.path.join(this.projectDir, 'changelog.md'); let previousChangelog: plugins.smartfile.SmartFile; if (await plugins.fsInstance.file(previousChangelogPath).exists()) { previousChangelog = await plugins.smartfileFactory.fromFilePath(previousChangelogPath); } if (!previousChangelog) { // lets build the changelog based on that const commitMessages = await gitRepo.getAllCommitMessages(); console.log(JSON.stringify(commitMessages, null, 2)); // Use DualAgentOrchestrator for changelog generation with Guardian validation const changelogOrchestrator = new plugins.smartagent.DualAgentOrchestrator({ smartAiInstance: this.aiDocsRef.smartAiInstance, defaultProvider: 'openai', logPrefix: '[Changelog]', onProgress: (event) => logger.log(event.logLevel, event.logMessage), guardianPolicyPrompt: ` You validate changelog generation. APPROVE if: - Changelog follows proper markdown format with ## headers for each version - Entries are chronologically ordered (newest first) - Version ranges for trivial commits are properly summarized - No duplicate or empty entries - Format matches: ## yyyy-mm-dd - x.x.x - scope REJECT with feedback if: - Markdown formatting is incorrect - Entries are not meaningful or helpful - Dates or versions are malformed `, }); await changelogOrchestrator.start(); const changelogTaskPrompt = ` You are building a changelog.md file for the project. Omit commits and versions that lack relevant changes, but make sure to mention them as a range with a summarizing message instead. A changelog entry should look like this: ## yyyy-mm-dd - x.x.x - scope here main descriptiom here - detailed bullet points follow You are given: * the commit messages of the project Only return the changelog file content, so it can be written directly to changelog.md. Here are the commit messages: ${JSON.stringify(commitMessages, null, 2)} `; const changelogResult = await changelogOrchestrator.run(changelogTaskPrompt); await changelogOrchestrator.stop(); if (!changelogResult.success) { throw new Error(`Changelog generation failed: ${changelogResult.status}`); } previousChangelog = plugins.smartfileFactory.fromString( previousChangelogPath, changelogResult.result.replaceAll('```markdown', '').replaceAll('```', ''), 'utf8' ); } let oldChangelog = previousChangelog.contents.toString().replace('# Changelog\n\n', ''); if (oldChangelog.startsWith('\n')) { oldChangelog = oldChangelog.replace('\n', ''); } let newDateString = new plugins.smarttime.ExtendedDate().exportToHyphedSortableDate(); let newChangelog = `# Changelog\n\n${`## ${newDateString} - {{nextVersion}} - {{nextVersionScope}} {{nextVersionMessage}} {{nextVersionDetails}}`}\n\n${oldChangelog}`; resultObject.changelog = newChangelog; return resultObject; } }