import logger from '../utils/logger.js'; import { sample, shuffle } from 'lodash-es'; class SDLCManager { constructor(githubService, aiProvider, codeGenerator, schedule) { this.github = githubService; this.ai = aiProvider; this.codeGen = codeGenerator; this.schedule = schedule; this.state = { currentPhase: 'idle', activeIssues: [], activeBranches: [], activePRs: [], sessionCommits: 0, }; } async runCycle() { if (!this.schedule.isWorkHours()) { logger.info('Outside work hours, skipping cycle'); return; } const energy = this.schedule.getEnergyLevel(); logger.info(`Starting SDLC cycle (energy: ${(energy * 100).toFixed(0)}%)`); await this._syncState(); const action = this._decideNextAction(); logger.info(`Decided action: ${action.type}`); try { switch (action.type) { case 'create_issue': await this._createIssue(); break; case 'work_on_issue': await this._workOnIssue(action.issue); break; case 'review_pr': await this._reviewPR(action.pr); break; case 'merge_pr': await this._mergePR(action.pr); break; case 'fix_review_comment': await this._fixReviewComment(action.pr); break; case 'close_issue': await this._closeIssue(action.issue); break; default: logger.info('No actionable task, waiting'); } } catch (error) { logger.error(`SDLC cycle error: ${error.message}`); } this.state.sessionCommits++; } async _syncState() { const [issues, prs] = await Promise.all([ this.github.listIssues('open'), this.github.listPullRequests('open'), ]); this.state.activeIssues = issues; this.state.activePRs = prs; logger.debug(`Synced state: ${issues.length} open issues, ${prs.length} open PRs`); } _decideNextAction() { const { activeIssues, activePRs, sessionCommits } = this.state; const maxCommits = this.schedule.config?.activity?.maxCommitsPerSession || 8; if (sessionCommits >= maxCommits) { return { type: 'idle', reason: 'max commits reached' }; } const openPRs = activePRs.filter(pr => pr.state === 'open'); if (openPRs.length > 0 && Math.random() < 0.3) { const pr = sample(openPRs); if (Math.random() < 0.4) { return { type: 'review_pr', pr }; } if (Math.random() < 0.3) { return { type: 'merge_pr', pr }; } if (Math.random() < 0.2) { return { type: 'fix_review_comment', pr }; } } const workableIssues = activeIssues.filter( issue => !issue.labels?.some(l => l.name === 'in-progress') ); if (workableIssues.length > 0 && Math.random() < 0.6) { return { type: 'work_on_issue', issue: sample(workableIssues) }; } if (Math.random() < 0.5) { return { type: 'create_issue' }; } const closableIssues = activeIssues.filter( issue => issue.labels?.some(l => l.name === 'done' || l.name === 'resolved') ); if (closableIssues.length > 0) { return { type: 'close_issue', issue: sample(closableIssues) }; } return { type: 'create_issue' }; } async _createIssue() { logger.info('Creating new issue'); const projectContext = await this._getProjectContext(); const title = await this.ai.generateIssueTitle(projectContext); const body = await this.ai.generateIssueBody(title, projectContext); const labels = this._generateIssueLabels(title); const issue = await this.github.createIssue(title, body, labels); this.state.activeIssues.push(issue); logger.info(`Created issue #${issue.number}: ${title}`); } async _workOnIssue(issue) { logger.info(`Working on issue #${issue.number}: ${issue.title}`); await this.github.addLabels(issue.number, ['in-progress']); const branchName = this._generateBranchName(issue.title); try { await this.github.createBranch(branchName); } catch (error) { logger.warn(`Branch ${branchName} exists, using it`); } this.state.activeBranches.push(branchName); const structure = this.codeGen.generateProjectStructure(); const numFiles = Math.min( structure.files.length, (this.schedule.config?.activity?.maxFilesPerCommit || 3) ); const filesToCreate = shuffle(structure.files).slice(0, numFiles); for (const file of filesToCreate) { await this._createFileInBranch(file, branchName, issue); } const prTitle = this._generatePRTitle(issue.title, structure.name); const prBody = await this.ai.generatePRDescription( branchName, `Implemented ${structure.name} functionality for issue #${issue.number}` ); const pr = await this.github.createPullRequest(prTitle, prBody, branchName); this.state.activePRs.push(pr); await this.github.addLabels(issue.number, ['done']); logger.info(`Created PR #${pr.number} for issue #${issue.number}`); } async _createFileInBranch(file, branchName, issue) { const content = this.codeGen.generateFileContent(file.type, { issue: issue.title, project: await this._getProjectContext(), }); const commitMessage = this.codeGen.generateCommitMessageForFile(file.path, file.type); await this.github.createOrUpdateFile( file.path, content, commitMessage, branchName ); logger.info(`Created ${file.path} on ${branchName}`); } async _reviewPR(pr) { logger.info(`Reviewing PR #${pr.number}: ${pr.title}`); const files = await this.github.getPullRequestFiles(pr.number); if (files.length === 0) { return; } const fileToReview = sample(files); try { const content = await this._getFileContent(fileToReview); const comment = await this.ai.generateReviewComment( content || 'Code review pending', fileToReview.filename ); const reviewType = this._decideReviewType(); if (reviewType === 'comment') { await this.github.addPullRequestComment(pr.number, comment); } else { await this.github.addPullRequestReview( pr.number, reviewType, comment, pr.head?.sha ); } logger.info(`Added ${reviewType} review to PR #${pr.number}`); } catch (error) { logger.error(`Review error: ${error.message}`); } } async _mergePR(pr) { logger.info(`Merging PR #${pr.number}`); try { await this.github.mergePullRequest(pr.number); if (pr.head?.ref) { await this.github.deleteBranch(pr.head.ref); } this.state.activePRs = this.state.activePRs.filter(p => p.number !== pr.number); } catch (error) { logger.error(`Merge error: ${error.message}`); } } async _fixReviewComment(pr) { logger.info(`Fixing review comments on PR #${pr.number}`); const files = await this.github.getPullRequestFiles(pr.number); if (files.length === 0) return; const file = sample(files); const content = this.codeGen.generateFileContent(file.type || 'utility', { fix: true, original: file.filename, }); await this.github.createOrUpdateFile( file.filename || file.path, content, 'fix: address review comments', pr.head?.ref || 'main' ); await this.github.addPullRequestComment( pr.number, "Thanks for the review! I've addressed the feedback in this commit." ); } async _closeIssue(issue) { logger.info(`Closing issue #${issue.number}`); await this.github.closeIssue(issue.number); this.state.activeIssues = this.state.activeIssues.filter(i => i.number !== issue.number); } _generateBranchName(issueTitle) { const sanitized = issueTitle .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .slice(0, 50); const prefix = this._getBranchPrefix(issueTitle); return `${prefix}/${sanitized}`; } _getBranchPrefix(title) { const lower = title.toLowerCase(); if (lower.includes('add') || lower.includes('implement') || lower.includes('new')) return 'feat'; if (lower.includes('fix') || lower.includes('bug') || lower.includes('error')) return 'fix'; if (lower.includes('refactor') || lower.includes('clean') || lower.includes('improve')) return 'refactor'; if (lower.includes('test') || lower.includes('coverage')) return 'test'; if (lower.includes('doc') || lower.includes('readme')) return 'docs'; return 'chore'; } _generatePRTitle(issueTitle, featureName) { const prefix = this._getBranchPrefix(issueTitle); return `${prefix}: ${issueTitle.charAt(0).toLowerCase() + issueTitle.slice(1)}`; } _generateIssueLabels(title) { const labels = []; const lower = title.toLowerCase(); if (lower.includes('add') || lower.includes('implement') || lower.includes('new')) { labels.push('enhancement'); } if (lower.includes('fix') || lower.includes('bug') || lower.includes('error')) { labels.push('bug'); } if (lower.includes('performance') || lower.includes('optimize')) { labels.push('performance'); } if (lower.includes('security') || lower.includes('auth')) { labels.push('security'); } if (labels.length === 0) { labels.push(sample(['enhancement', 'maintenance', 'task'])); } return labels; } _decideReviewType() { const rand = Math.random(); if (rand < 0.6) return 'comment'; if (rand < 0.85) return 'approve'; return 'request_changes'; } async _getProjectContext() { try { const commits = await this.github.getCommitHistory('main', 5); return `Recent activity: ${commits.length} commits. Project uses JavaScript/Node.js.`; } catch { return 'JavaScript/Node.js project'; } } async _getFileContent(file) { try { const content = await this.github.getRepositoryContent(file.filename || file.path); if (content?.content) { return Buffer.from(content.content, 'base64').toString(); } } catch { return null; } return null; } } export default SDLCManager;