Spaces:
Sleeping
Sleeping
| 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; | |