activity-simulator / src /core /sdlcManager.js
abedelbahnasy55's picture
feat: cloud simulator - Docker, dashboard, auto-start
ccb6b75
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;