UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

213 lines (174 loc) 5.72 kB
/** * Gitea Tracker for External Ralph Loop * * Optional integration with Gitea for issue tracking and progress updates. * Uses secure token handling from ~/.config/gitea/token. * * @implements @.aiwg/requirements/design-ralph-external.md */ import { execSync } from 'child_process'; import { existsSync, readFileSync } from 'fs'; import { homedir } from 'os'; import { join } from 'path'; const GITEA_TOKEN_PATH = join(homedir(), '.config', 'gitea', 'token'); const GITEA_API_BASE = 'https://git.integrolabs.net/api/v1'; /** * @typedef {Object} GiteaConfig * @property {string} owner - Repository owner * @property {string} repo - Repository name * @property {string} [tokenPath] - Path to token file */ export class GiteaTracker { /** * @param {GiteaConfig} config */ constructor(config) { this.owner = config.owner; this.repo = config.repo; this.tokenPath = config.tokenPath || GITEA_TOKEN_PATH; this.issueNumber = null; } /** * Check if Gitea token is available * @returns {boolean} */ isAvailable() { return existsSync(this.tokenPath); } /** * Make API call using heredoc pattern for token security * @param {string} method - HTTP method * @param {string} endpoint - API endpoint * @param {Object} [data] - Request body * @returns {Object} */ apiCall(method, endpoint, data = null) { if (!this.isAvailable()) { throw new Error(`Gitea token not found at ${this.tokenPath}`); } const url = `${GITEA_API_BASE}${endpoint}`; const dataArg = data ? `-d '${JSON.stringify(data)}'` : ''; // Use heredoc pattern for secure token handling const script = `bash <<'EOF' TOKEN=$(cat ${this.tokenPath}) curl -s -X ${method} \\ -H "Authorization: token \${TOKEN}" \\ -H "Content-Type: application/json" \\ "${url}" ${dataArg} EOF`; try { const result = execSync(script, { encoding: 'utf8' }); return JSON.parse(result); } catch (error) { throw new Error(`Gitea API call failed: ${error.message}`); } } /** * Create issue for loop tracking * @param {Object} loopState - Loop state * @returns {number} - Issue number */ createIssue(loopState) { const body = `## External Ralph Loop **Loop ID**: \`${loopState.loopId}\` ### Objective ${loopState.objective} ### Completion Criteria ${loopState.completionCriteria} ### Configuration - Max Iterations: ${loopState.maxIterations} - Model: ${loopState.config?.model || 'opus'} - Budget/Iteration: $${loopState.config?.budgetPerIteration || 2.0} --- **Status**: In Progress Progress updates will be posted as comments. `; const response = this.apiCall('POST', `/repos/${this.owner}/${this.repo}/issues`, { title: `[Ralph] ${loopState.objective.slice(0, 50)}...`, body, }); this.issueNumber = response.number; return this.issueNumber; } /** * Post progress comment * @param {number} issueNumber - Issue number * @param {number} iteration - Iteration number * @param {Object} analysis - Analysis result */ postProgressComment(issueNumber, iteration, analysis) { const body = `## Iteration ${iteration} Update **Status**: ${analysis.completed ? (analysis.success ? 'Completed' : 'Failed') : 'In Progress'} **Progress**: ${analysis.completionPercentage || 0}% ### Analysis ${analysis.learnings || 'No learnings recorded'} ### Modified Files ${analysis.artifactsModified?.map(f => `- \`${f}\``).join('\n') || 'None'} ${analysis.blockers?.length > 0 ? `### Blockers\n${analysis.blockers.map(b => `- ${b}`).join('\n')}` : ''} ${analysis.nextApproach ? `### Next Approach\n${analysis.nextApproach}` : ''} `; this.apiCall('POST', `/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments`, { body, }); } /** * Close issue with final status * @param {number} issueNumber - Issue number * @param {boolean} success - Whether loop succeeded * @param {string} summary - Final summary */ closeIssue(issueNumber, success, summary) { // Post final comment const body = `## Loop Completed **Final Status**: ${success ? 'SUCCESS' : 'FAILED'} ### Summary ${summary} --- Loop completed at ${new Date().toISOString()} `; this.apiCall('POST', `/repos/${this.owner}/${this.repo}/issues/${issueNumber}/comments`, { body, }); // Close the issue this.apiCall('PATCH', `/repos/${this.owner}/${this.repo}/issues/${issueNumber}`, { state: 'closed', }); } /** * Update issue title with status * @param {number} issueNumber - Issue number * @param {string} status - Status to append */ updateTitle(issueNumber, status) { this.apiCall('PATCH', `/repos/${this.owner}/${this.repo}/issues/${issueNumber}`, { title: `[Ralph] [${status}] ${this.objective?.slice(0, 40) || 'Task'}...`, }); } } /** * Try to detect repository info from git remote * @param {string} cwd - Working directory * @returns {{owner: string, repo: string}|null} */ export function detectGiteaRepo(cwd) { try { const remote = execSync('git remote get-url origin', { cwd, encoding: 'utf8', }).trim(); // Parse git@git.integrolabs.net:owner/repo.git const sshMatch = remote.match(/git@git\.integrolabs\.net:([^/]+)\/(.+?)(?:\.git)?$/); if (sshMatch) { return { owner: sshMatch[1], repo: sshMatch[2] }; } // Parse https://git.integrolabs.net/owner/repo.git const httpsMatch = remote.match(/https:\/\/git\.integrolabs\.net\/([^/]+)\/(.+?)(?:\.git)?$/); if (httpsMatch) { return { owner: httpsMatch[1], repo: httpsMatch[2] }; } return null; } catch { return null; } } export default GiteaTracker;