Spaces:
Sleeping
Sleeping
| import logger from '../utils/logger.js'; | |
| class IssueTracker { | |
| constructor(githubService, aiProvider, memory, developer, conversationSimulator) { | |
| this.github = githubService; | |
| this.ai = aiProvider; | |
| this.memory = memory; | |
| this.developer = developer; | |
| this.conversation = conversationSimulator; | |
| } | |
| async processIssues() { | |
| try { | |
| const issues = await this.github.listIssues('open'); | |
| for (const issue of issues) { | |
| await this._processSingleIssue(issue); | |
| } | |
| } catch (error) { | |
| logger.error(`Failed to process issues: ${error.message}`); | |
| } | |
| } | |
| async _processSingleIssue(issue) { | |
| const statusData = this.memory.recallWithMetadata(`issue-status-${issue.number}`); | |
| const status = statusData?.value || null; | |
| try { | |
| if (!status) { | |
| await this._handleNewIssue(issue); | |
| } else if (status === 'acknowledged') { | |
| await this._handleAcknowledgedIssue(issue); | |
| } else if (status === 'in-progress') { | |
| await this._handleInProgressIssue(issue); | |
| } else if (status === 'needs-info') { | |
| await this._handleNeedsInfoIssue(issue); | |
| } else if (status === 'done') { | |
| await this._handleDoneIssue(issue); | |
| } | |
| } catch (error) { | |
| logger.error(`Failed to process issue #${issue.number}: ${error.message}`); | |
| } | |
| } | |
| async _handleNewIssue(issue) { | |
| logger.info(`Handling new issue #${issue.number}: ${issue.title}`); | |
| const comment = await this.conversation.generateIssueComment(issue, []); | |
| await this.github.addIssueComment(issue.number, comment); | |
| this.memory.remember(`issue-status-${issue.number}`, 'acknowledged', { | |
| tags: ['issue'], | |
| title: issue.title, | |
| }); | |
| } | |
| async _handleAcknowledgedIssue(issue) { | |
| if (Math.random() < 0.6) { | |
| await this._startWorkingOnIssue(issue); | |
| } | |
| } | |
| async _startWorkingOnIssue(issue) { | |
| logger.info(`Starting work on issue #${issue.number}`); | |
| await this.github.addLabels(issue.number, ['in-progress']); | |
| const statusUpdate = await this.conversation.generateStatusUpdate(issue, 'in-progress'); | |
| await this.github.addIssueComment(issue.number, statusUpdate); | |
| this.memory.remember(`issue-status-${issue.number}`, 'in-progress', { | |
| tags: ['active-issue'], | |
| title: issue.title, | |
| }); | |
| } | |
| async _handleInProgressIssue(issue) { | |
| const cleanTitle = this._cleanTitle(issue.title); | |
| const action = this.developer.getRoleBasedAction(); | |
| const branchName = this._generateBranchName(cleanTitle); | |
| try { | |
| await this.github.createBranch(branchName); | |
| } catch (error) { | |
| logger.warn(`Branch ${branchName} exists`); | |
| } | |
| const codeContent = await this._generateCodeForIssue(action, issue); | |
| const fileName = this._generateFileName(action); | |
| await this.github.createOrUpdateFile(fileName, codeContent, `feat: ${issue.title}`, branchName); | |
| const prTitle = `${this._getCommitPrefix(cleanTitle)}: ${cleanTitle}`; | |
| const prBody = await this._generatePRBody(issue, branchName); | |
| const pr = await this.github.createPullRequest(prTitle, prBody, branchName); | |
| this.memory.remember(`issue-status-${issue.number}`, 'done', { | |
| tags: ['done', 'open-pr'], | |
| prNumber: pr.number, | |
| }); | |
| this.memory.remember(`pr-for-issue-${issue.number}`, pr.number); | |
| logger.info(`Created PR #${pr.number} for issue #${issue.number}`); | |
| } | |
| async _handleNeedsInfoIssue(issue) { | |
| const clarification = await this.conversation.generateIssueClarification(issue); | |
| await this.github.addIssueComment(issue.number, clarification); | |
| } | |
| async _handleDoneIssue(issue) { | |
| const prNumber = this.memory.recall(`pr-for-issue-${issue.number}`); | |
| if (prNumber) { | |
| try { | |
| const pr = await this.github.getPullRequest(prNumber); | |
| if (pr && pr.merged) { | |
| await this.github.closeIssue(issue.number); | |
| this.memory.remember(`issue-status-${issue.number}`, 'closed'); | |
| logger.info(`Closed issue #${issue.number} after PR merged`); | |
| } | |
| } catch (error) { | |
| logger.error(`Failed to check PR status for issue #${issue.number}: ${error.message}`); | |
| } | |
| } | |
| } | |
| async _generateCodeForIssue(action, issue) { | |
| const roleContext = this.developer.getRoleContext(); | |
| const prompt = `أنت مطور (${roleContext.role}) تكتب كود لـ Issue. | |
| Issue: ${issue.title} | |
| Action: ${action} | |
| اكتب كود JavaScript/TypeScript حقيقي ومنطقي. | |
| يجب أن يكون كود إنتاجي مع error handling و comments. | |
| اكتب الكود فقط.`; | |
| const code = await this.ai.generate(prompt, { | |
| maxTokens: 1500, | |
| temperature: 0.7, | |
| }); | |
| return code.trim(); | |
| } | |
| async _generatePRBody(issue, branchName) { | |
| const roleContext = this.developer.getRoleContext(); | |
| return `## ${issue.title} | |
| **Closes:** #${issue.number} | |
| ### What | |
| Implemented ${issue.title.toLowerCase()}. | |
| ### Why | |
| ${roleContext.phrases?.[0] || 'Improves the project.'} | |
| ### Testing | |
| - [ ] Tests pass | |
| ### Notes | |
| Ready for review.`; | |
| } | |
| _generateBranchName(issueTitle) { | |
| const sanitized = issueTitle | |
| .toLowerCase() | |
| .replace(/[^a-z0-9\s-]/g, '') | |
| .replace(/\s+/g, '-') | |
| .replace(/-+/g, '-') | |
| .slice(0, 50); | |
| const prefix = this._getCommitPrefix(issueTitle); | |
| return `${prefix}/${sanitized}`; | |
| } | |
| _getCommitPrefix(title) { | |
| const cleanTitle = title.replace(/^(feat|fix|docs|refactor|test|chore):\s*/i, '').toLowerCase(); | |
| const lower = cleanTitle.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'; | |
| return 'chore'; | |
| } | |
| _cleanTitle(title) { | |
| let cleaned = title.trim(); | |
| while (/^(feat|fix|docs|refactor|test|chore):\s*/i.test(cleaned)) { | |
| cleaned = cleaned.replace(/^(feat|fix|docs|refactor|test|chore):\s*/i, '').trim(); | |
| } | |
| return cleaned; | |
| } | |
| _generateFileName(action) { | |
| const ext = '.js'; | |
| const nameMap = { | |
| 'create-component': `src/components/${action.replace('create-', '')}${ext}`, | |
| 'add-api-endpoint': `src/api/endpoints${ext}`, | |
| 'fix-bug': `src/fixes/bugFix${ext}`, | |
| 'add-feature': `src/features/${action.replace('add-', '')}${ext}`, | |
| 'add-tests': `tests/test${ext}`, | |
| }; | |
| return nameMap[action] || `src/${action}${ext}`; | |
| } | |
| } | |
| export default IssueTracker; | |