import * as plugins from '../plugins.js'; import { AiDoc } from '../classes.aidoc.js'; import { ProjectContext } from './projectcontext.js'; import { DiffProcessor } from '../classes.diffprocessor.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 // Note: No filesystem tool needed - the diff already contains all change information const commitOrchestrator = new plugins.smartagent.DualAgentOrchestrator({ smartAiInstance: this.aiDocsRef.smartAiInstance, defaultProvider: 'openai', guardianPolicyPrompt: ` You validate commit messages for semantic versioning compliance. APPROVE 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 with specific feedback 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 `, }); await commitOrchestrator.start(); const commitTaskPrompt = ` You create a commit message for a git commit. Project directory: ${this.projectDir} 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. Important: Answer only in valid JSON. Your answer should be parseable with JSON.parse() without modifying anything. Here is the structure of the JSON you should return: interface { recommendedNextVersionLevel: 'fix' | 'feat' | 'BREAKING CHANGE'; // the recommended next version level recommendedNextVersionScope: string; // scope name like "core", "cli", or specific class names recommendedNextVersionMessage: string; // the commit message (don't include fix/feat prefix) recommendedNextVersionDetails: string[]; // detailed bullet points for the changelog recommendedNextVersion: string; // the recommended next version x.x.x } For recommendedNextVersionDetails, only add entries that have obvious value to the reader. Here is the git diff showing what changed: ${processedDiffString} Generate the commit message based on these changes. `; const commitResult = await commitOrchestrator.run(commitTaskPrompt); await commitOrchestrator.stop(); if (!commitResult.success) { throw new Error(`Commit message generation failed: ${commitResult.status}`); } const resultObject: INextCommitObject = JSON.parse( commitResult.result.replace('```json', '').replace('```', '') ); 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', 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; } }