context-forge
Version:
AI orchestration platform with autonomous teams, enhancement planning, migration tools, 25+ slash commands, checkpoints & hooks. Multi-IDE: Claude, Cursor, Windsurf, Cline, Copilot
357 lines (355 loc) • 12.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitDisciplineService = void 0;
const events_1 = require("events");
const child_process_1 = require("child_process");
const util_1 = require("util");
const path_1 = __importDefault(require("path"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const chalk_1 = __importDefault(require("chalk"));
const handlebars_1 = __importDefault(require("handlebars"));
const execAsync = (0, util_1.promisify)(child_process_1.exec);
class GitDisciplineService extends events_1.EventEmitter {
constructor(config, projectPath) {
super();
this.stats = {
totalCommits: 0,
commitsByAgent: {},
averageCommitInterval: 0,
branchesCreated: 0,
tagsCreated: 0,
complianceRate: 100,
};
this.lastCommitTimes = new Map();
this.autoCommitIntervals = new Map();
this.config = config;
this.projectPath = projectPath;
// Load commit template
const templatePath = path_1.default.join(__dirname, '../../templates/orchestration/git-commit.hbs');
const templateContent = fs_extra_1.default.readFileSync(templatePath, 'utf-8');
this.commitTemplate = handlebars_1.default.compile(templateContent);
}
/**
* Initialize git discipline for the project
*/
async initialize() {
console.log(chalk_1.default.blue('Initializing git discipline...'));
// Ensure git is initialized
await this.ensureGitRepo();
// Set up git config
await this.configureGit();
// Create hooks directory
await this.setupGitHooks();
console.log(chalk_1.default.green('Git discipline initialized'));
}
/**
* Ensure project is a git repository
*/
async ensureGitRepo() {
const gitPath = path_1.default.join(this.projectPath, '.git');
if (!(await fs_extra_1.default.pathExists(gitPath))) {
console.log(chalk_1.default.yellow('Initializing git repository...'));
await execAsync('git init', { cwd: this.projectPath });
}
}
/**
* Configure git settings
*/
async configureGit() {
try {
// Set up user info if not configured
await execAsync('git config user.name "Context Forge Orchestrator"', {
cwd: this.projectPath,
});
await execAsync('git config user.email "orchestrator@context-forge.ai"', {
cwd: this.projectPath,
});
}
catch {
// Ignore if already configured
}
}
/**
* Set up git hooks
*/
async setupGitHooks() {
const hooksPath = path_1.default.join(this.projectPath, '.git', 'hooks');
await fs_extra_1.default.ensureDir(hooksPath);
if (this.config.requireTests) {
// Create pre-commit hook for test validation
const preCommitHook = `#!/bin/bash
# Pre-commit hook to ensure tests pass
echo "Running tests before commit..."
npm test
if [ $? -ne 0 ]; then
echo "Tests failed! Commit aborted."
exit 1
fi
echo "Tests passed. Proceeding with commit."
exit 0
`;
const hookPath = path_1.default.join(hooksPath, 'pre-commit');
await fs_extra_1.default.writeFile(hookPath, preCommitHook);
await fs_extra_1.default.chmod(hookPath, 0o755);
}
}
/**
* Start auto-commit for an agent
*/
startAutoCommit(agentId, agentRole, sessionId) {
if (!this.config.enabled) {
return;
}
// Clear existing interval
this.stopAutoCommit(agentId);
// Set up new interval
const interval = setInterval(async () => {
await this.performAutoCommit(agentId, agentRole, sessionId);
}, this.config.autoCommitInterval * 60 * 1000);
this.autoCommitIntervals.set(agentId, interval);
console.log(chalk_1.default.green(`Auto-commit enabled for ${agentId} (every ${this.config.autoCommitInterval} minutes)`));
}
/**
* Stop auto-commit for an agent
*/
stopAutoCommit(agentId) {
const interval = this.autoCommitIntervals.get(agentId);
if (interval) {
clearInterval(interval);
this.autoCommitIntervals.delete(agentId);
}
}
/**
* Perform auto-commit
*/
async performAutoCommit(agentId, agentRole, sessionId) {
try {
// Check for changes
const { stdout: statusOutput } = await execAsync('git status --porcelain', {
cwd: this.projectPath,
});
if (!statusOutput.trim()) {
console.log(chalk_1.default.gray(`No changes to commit for ${agentId}`));
return;
}
// Get list of changes
const changes = statusOutput
.trim()
.split('\n')
.map((line) => line.substring(3)); // Remove status prefix
// Stage all changes
await execAsync('git add -A', { cwd: this.projectPath });
// Create commit message
const commitInfo = {
agentId,
agentRole,
sessionId,
changes: changes.slice(0, 5), // Limit to 5 items
};
const commitMessage = this.generateCommitMessage(commitInfo);
// Commit
await execAsync(`git commit -m "${commitMessage}"`, {
cwd: this.projectPath,
});
// Update stats
this.updateCommitStats(agentId);
console.log(chalk_1.default.green(`✓ Auto-commit completed for ${agentId}`));
// Emit event
this.emit('commit', {
agentId,
timestamp: new Date(),
changes: changes.length,
});
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (!errorMessage.includes('nothing to commit')) {
console.error(chalk_1.default.red(`Auto-commit failed for ${agentId}: ${errorMessage}`));
this.emit('commit-failed', { agentId, error: errorMessage });
}
}
}
/**
* Generate commit message from template
*/
generateCommitMessage(info) {
const context = {
commitType: 'Progress',
taskDescription: `Auto-commit by ${info.agentId}`,
agentId: info.agentId,
agentRole: info.agentRole,
sessionId: info.sessionId,
timestamp: new Date().toISOString(),
commitInterval: this.config.autoCommitInterval,
changes: info.changes,
tests: info.tests,
blockers: info.blockers,
nextSteps: info.nextSteps,
};
// Use custom format if provided
if (this.config.commitMessageFormat) {
return this.config.commitMessageFormat
.replace('$TASK', context.taskDescription)
.replace('$DESCRIPTION', info.changes.join(', '))
.replace('$AGENT', info.agentId)
.replace('$TIMESTAMP', context.timestamp);
}
return this.commitTemplate(context);
}
/**
* Create feature branch
*/
async createFeatureBranch(branchName, agentId) {
const sanitizedName = branchName
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-');
const fullBranchName = `feature/${sanitizedName}`;
try {
await execAsync(`git checkout -b ${fullBranchName}`, {
cwd: this.projectPath,
});
this.stats.branchesCreated++;
console.log(chalk_1.default.green(`Created branch: ${fullBranchName}`));
this.emit('branch-created', {
branch: fullBranchName,
agentId,
timestamp: new Date(),
});
return fullBranchName;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('already exists')) {
// Switch to existing branch
await execAsync(`git checkout ${fullBranchName}`, {
cwd: this.projectPath,
});
return fullBranchName;
}
throw error;
}
}
/**
* Tag stable version
*/
async tagStableVersion(tagName, message, agentId) {
if (!this.config.tagStrategy) {
return;
}
const tagPrefix = this.config.tagStrategy === 'stable' ? 'stable' : 'v';
const fullTag = `${tagPrefix}-${tagName}-${Date.now()}`;
try {
await execAsync(`git tag -a ${fullTag} -m "${message}"`, { cwd: this.projectPath });
this.stats.tagsCreated++;
console.log(chalk_1.default.green(`Created tag: ${fullTag}`));
this.emit('tag-created', {
tag: fullTag,
agentId,
timestamp: new Date(),
});
}
catch (error) {
console.error(chalk_1.default.red(`Failed to create tag: ${error}`));
}
}
/**
* Check compliance with commit interval
*/
checkCompliance(agentId) {
const lastCommit = this.lastCommitTimes.get(agentId);
if (!lastCommit) {
return true; // No commits yet
}
const timeSinceLastCommit = Date.now() - lastCommit.getTime();
const maxInterval = this.config.autoCommitInterval * 60 * 1000 * 1.5; // 50% grace period
return timeSinceLastCommit <= maxInterval;
}
/**
* Update commit statistics
*/
updateCommitStats(agentId) {
this.stats.totalCommits++;
this.stats.commitsByAgent[agentId] = (this.stats.commitsByAgent[agentId] || 0) + 1;
const now = new Date();
const lastCommit = this.lastCommitTimes.get(agentId);
if (lastCommit) {
const interval = (now.getTime() - lastCommit.getTime()) / (60 * 1000);
this.updateAverageInterval(interval);
}
this.lastCommitTimes.set(agentId, now);
this.stats.lastCommitTime = now;
}
/**
* Update average commit interval
*/
updateAverageInterval(interval) {
const totalIntervals = this.stats.totalCommits - 1;
if (totalIntervals > 0) {
this.stats.averageCommitInterval =
(this.stats.averageCommitInterval * (totalIntervals - 1) + interval) / totalIntervals;
}
}
/**
* Get git statistics
*/
getStats() {
// Calculate compliance rate
let compliantAgents = 0;
let totalAgents = 0;
for (const [agentId] of this.autoCommitIntervals) {
totalAgents++;
if (this.checkCompliance(agentId)) {
compliantAgents++;
}
}
this.stats.complianceRate = totalAgents > 0 ? (compliantAgents / totalAgents) * 100 : 100;
return { ...this.stats };
}
/**
* Get recent commits
*/
async getRecentCommits(limit = 10) {
try {
const { stdout } = await execAsync(`git log --oneline -${limit} --pretty=format:'%h|%s|%an|%ar'`, { cwd: this.projectPath });
return stdout
.trim()
.split('\n')
.map((line) => {
const [hash, message, author, time] = line.split('|');
return { hash, message, author, time };
});
}
catch {
return [];
}
}
/**
* Get current branch
*/
async getCurrentBranch() {
try {
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
cwd: this.projectPath,
});
return stdout.trim();
}
catch {
return 'main';
}
}
/**
* Clean up resources
*/
cleanup() {
// Stop all auto-commit intervals
for (const [agentId] of this.autoCommitIntervals) {
this.stopAutoCommit(agentId);
}
}
}
exports.GitDisciplineService = GitDisciplineService;
//# sourceMappingURL=gitDiscipline.js.map