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.

476 lines (402 loc) 14.7 kB
/** * GitHub Copilot Provider * * Deploys agents in GitHub Copilot Custom Agent YAML format. * Commands are converted to agents since Copilot doesn't have separate commands. * * Deployment paths: * - Agents: .github/agents/ * - Commands: .github/agents/ (as agents) * - Skills: .github/skills/ * - Rules: .github/copilot-rules/ * * Special features: * - YAML format output (.yaml extension) * - Temperature and max_tokens per category * - Tool mapping to Copilot equivalents * - Creates copilot-instructions.md */ import fs from 'fs'; import path from 'path'; import { ensureDir, listMdFiles, listMdFilesRecursive, deployFiles, inferAgentCategory, toKebabCase, initializeFrameworkWorkspace, getAddonAgentFiles, getAddonCommandFiles, getAddonRuleFiles, getAddonSkillDirs, listSkillDirs, deploySkillDir, normalizeDeploymentMode, collectFrameworkArtifacts, cleanupOldRuleFiles } from './base.mjs'; // ============================================================================ // Provider Configuration // ============================================================================ export const name = 'copilot'; export const aliases = []; export const paths = { agents: '.github/agents/', commands: '.github/agents/', // Commands become agents skills: '.github/skills/', rules: '.github/copilot-rules/' }; export const support = { agents: 'native', commands: 'conventional', skills: 'conventional', rules: 'conventional' }; export const capabilities = { skills: true, rules: true, aggregatedOutput: false, yamlFormat: true }; // ============================================================================ // Model Mapping // ============================================================================ /** * Map model shorthand to GitHub Copilot format (GPT models) */ export function mapModel(originalModel, modelCfg, modelsConfig) { const copilotModels = { 'opus': 'gpt-4', 'sonnet': 'gpt-4', 'haiku': 'gpt-4o-mini' }; // Handle override models first if (modelCfg.reasoningModel || modelCfg.codingModel || modelCfg.efficiencyModel) { const clean = (originalModel || 'sonnet').toLowerCase().replace(/['"]/g, ''); if (/opus/i.test(clean)) return modelCfg.reasoningModel || copilotModels.opus; if (/haiku/i.test(clean)) return modelCfg.efficiencyModel || copilotModels.haiku; return modelCfg.codingModel || copilotModels.sonnet; } const clean = (originalModel || 'sonnet').toLowerCase().replace(/['"]/g, ''); for (const [key, value] of Object.entries(copilotModels)) { if (clean.includes(key)) return value; } return copilotModels.sonnet; // default } // ============================================================================ // Tool Mapping // ============================================================================ /** * Get Copilot tools based on category and original tools */ export function getTools(category, toolsString) { // Map AIWG tools to GitHub Copilot tools const toolMap = { 'Read': 'search', 'Write': 'createFile', 'MultiEdit': 'editFiles', 'Bash': 'runInTerminal', 'WebFetch': 'fetch', 'Glob': 'search', 'Grep': 'search', 'Task': 'runSubagent' }; // Default tools by category const categoryDefaults = { analysis: ['search', 'fetch', 'githubRepo', 'problems'], documentation: ['search', 'createFile', 'editFiles', 'fetch'], planning: ['search', 'fetch', 'githubRepo', 'todos'], implementation: ['createFile', 'createDirectory', 'editFiles', 'deleteFile', 'search', 'runInTerminal', 'fetch', 'runSubagent', 'todos', 'problems', 'changes'] }; // If tools specified, map them if (toolsString) { let originalTools = []; if (toolsString.startsWith('[')) { try { originalTools = JSON.parse(toolsString); } catch (e) { originalTools = toolsString.replace(/[\[\]"']/g, '').split(/[,\s]+/).filter(Boolean); } } else { originalTools = toolsString.split(/[,\s]+/).filter(Boolean); } const mappedTools = new Set(); for (const tool of originalTools) { const mapped = toolMap[tool]; if (mapped) mappedTools.add(mapped); } if (mappedTools.size > 0) { return Array.from(mappedTools); } } // Fall back to category defaults return categoryDefaults[category] || categoryDefaults.implementation; } // ============================================================================ // Content Transformation // ============================================================================ /** * Transform AIWG agent to GitHub Copilot Custom Agent YAML format */ export function transformAgent(srcPath, content, opts) { const { modelsConfig = {} } = opts; // Parse existing frontmatter const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!fmMatch) return content; const [, frontmatter, body] = fmMatch; // Extract metadata const name = frontmatter.match(/name:\s*(.+)/)?.[1]?.trim(); const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim(); const modelMatch = frontmatter.match(/model:\s*(.+)/)?.[1]?.trim(); const toolsMatch = frontmatter.match(/tools:\s*(.+)/)?.[1]?.trim(); const categoryMatch = frontmatter.match(/category:\s*(.+)/)?.[1]?.trim(); // Map model to Copilot format const copilotModel = mapModel(modelMatch, opts, modelsConfig); // Determine agent category const category = categoryMatch || inferAgentCategory(name, body); // Get Copilot-specific tools const copilotTools = getTools(category, toolsMatch); // Temperature based on category const temperature = category === 'analysis' ? 0.2 : category === 'documentation' ? 0.4 : 0.3; const maxTokens = category === 'implementation' ? 8000 : 4000; // Generate Copilot agent YAML let copilotYaml = `name: ${name || 'aiwg-agent'} description: ${description || 'AIWG SDLC agent'} model: name: ${copilotModel} temperature: ${temperature} max_tokens: ${maxTokens}`; // Add tools if applicable if (copilotTools.length > 0) { copilotYaml += `\ntools: ${JSON.stringify(copilotTools)}`; } // Add instructions from body const cleanBody = body.trim(); if (cleanBody) { const escapedBody = cleanBody.replace(/\\/g, '\\\\'); copilotYaml += `\ninstructions: |\n${escapedBody.split('\n').map(line => ' ' + line).join('\n')}`; } return copilotYaml; } /** * Transform AIWG command to GitHub Copilot agent format */ export function transformCommand(srcPath, content, opts) { // Parse existing frontmatter const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!fmMatch) { // No frontmatter, create simple agent format const firstLine = content.split('\n')[0]; const description = firstLine.replace(/^#\s*/, '').trim() || 'AIWG command'; const name = toKebabCase(description); return `name: ${name} description: ${description} model: name: gpt-4 temperature: 0.3 max_tokens: 4000 instructions: | ${content.split('\n').map(line => ' ' + line).join('\n')}`; } const [, frontmatter, body] = fmMatch; // Extract metadata const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim(); const name = toKebabCase(description || 'aiwg-command'); // Build Copilot command as simple agent return `name: ${name} description: ${description || 'AIWG command'} model: name: gpt-4 temperature: 0.3 max_tokens: 4000 instructions: | ${body.trim().split('\n').map(line => ' ' + line).join('\n')}`; } // ============================================================================ // Deployment Functions // ============================================================================ /** * Deploy agents to .github/agents/ */ export function deployAgents(agentFiles, targetDir, opts) { const destDir = path.join(targetDir, paths.agents); ensureDir(destDir, opts.dryRun); return deployFiles(agentFiles, destDir, { ...opts, fileExtension: '.yaml' }, transformAgent); } /** * Deploy commands to .github/agents/ (as agents) */ export function deployCommands(commandFiles, targetDir, opts) { const destDir = path.join(targetDir, paths.commands); ensureDir(destDir, opts.dryRun); return deployFiles(commandFiles, destDir, { ...opts, fileExtension: '.yaml' }, transformCommand); } /** * Deploy skills to .github/skills/ */ export function deploySkills(skillDirs, targetDir, opts) { const destDir = path.join(targetDir, paths.skills); ensureDir(destDir, opts.dryRun); for (const skillDir of skillDirs) { deploySkillDir(skillDir, destDir, opts); } } /** * Deploy rules to .github/copilot-rules/ */ export function deployRules(ruleFiles, targetDir, opts) { const destDir = path.join(targetDir, paths.rules); ensureDir(destDir, opts.dryRun); cleanupOldRuleFiles(destDir, opts); return deployFiles(ruleFiles, destDir, opts, transformAgent); } // ============================================================================ // copilot-instructions.md // ============================================================================ /** * Create copilot-instructions.md from template */ export function createCopilotInstructions(target, srcRoot, dryRun) { const templatePath = path.join(srcRoot, 'agentic', 'code', 'frameworks', 'sdlc-complete', 'templates', 'copilot', 'copilot-instructions.md.aiwg-template'); const githubDir = path.join(target, '.github'); const destPath = path.join(githubDir, 'copilot-instructions.md'); if (!fs.existsSync(templatePath)) { console.warn(`Copilot instructions template not found at ${templatePath}`); return; } const template = fs.readFileSync(templatePath, 'utf8'); // Ensure .github directory exists if (!dryRun && !fs.existsSync(githubDir)) { fs.mkdirSync(githubDir, { recursive: true }); } if (fs.existsSync(destPath)) { const existing = fs.readFileSync(destPath, 'utf8'); if (existing.includes('<!-- AIWG SDLC Framework Integration -->') || existing.includes('## AIWG SDLC Framework')) { console.log('copilot-instructions.md already contains AIWG section, skipping'); return; } const markerIndex = template.indexOf('<!-- AIWG SDLC Framework Integration -->'); const aiwgSection = markerIndex !== -1 ? template.slice(markerIndex) : template; const combined = existing.trimEnd() + '\n\n---\n\n' + aiwgSection.trim() + '\n'; if (dryRun) { console.log(`[dry-run] Would update existing copilot-instructions.md with AIWG section`); } else { fs.writeFileSync(destPath, combined, 'utf8'); console.log('Updated copilot-instructions.md with AIWG SDLC framework section'); } } else { if (dryRun) { console.log(`[dry-run] Would create copilot-instructions.md from template`); } else { fs.writeFileSync(destPath, template, 'utf8'); console.log('Created copilot-instructions.md from template'); } } } // ============================================================================ // Post-Deployment // ============================================================================ export async function postDeploy(targetDir, opts) { initializeFrameworkWorkspace(targetDir, opts.mode, opts.dryRun, opts.srcRoot); // Create copilot-instructions.md createCopilotInstructions(targetDir, opts.srcRoot, opts.dryRun); } // ============================================================================ // File Extension // ============================================================================ export function getFileExtension(type) { return '.yaml'; } // ============================================================================ // Main Deploy Function // ============================================================================ export async function deploy(opts) { const { srcRoot, target, mode, deployCommands: shouldDeployCommands, deploySkills: shouldDeploySkills, deployRules: shouldDeployRules, commandsOnly, skillsOnly, rulesOnly, dryRun } = opts; console.log(`\n=== GitHub Copilot Provider ===`); console.log(`Target: ${target}`); console.log(`Mode: ${mode}`); const agentFiles = []; const commandFiles = []; const skillDirs = []; const ruleFiles = []; const normalizedMode = normalizeDeploymentMode(mode); // All addons (dynamically discovered) if (normalizedMode === 'general' || normalizedMode === 'sdlc' || normalizedMode === 'both' || normalizedMode === 'all') { agentFiles.push(...getAddonAgentFiles(srcRoot)); if (shouldDeployCommands || commandsOnly) { commandFiles.push(...getAddonCommandFiles(srcRoot)); } if (shouldDeploySkills || skillsOnly) { skillDirs.push(...getAddonSkillDirs(srcRoot)); } if (shouldDeployRules || rulesOnly) { ruleFiles.push(...getAddonRuleFiles(srcRoot)); } } const frameworkArtifacts = collectFrameworkArtifacts(srcRoot, normalizedMode, { includeAgents: true, includeCommands: shouldDeployCommands || commandsOnly, includeSkills: shouldDeploySkills || skillsOnly, includeRules: shouldDeployRules || rulesOnly, recursiveCommands: true, consolidatedSdlcRules: true }); agentFiles.push(...frameworkArtifacts.agents); commandFiles.push(...frameworkArtifacts.commands); skillDirs.push(...frameworkArtifacts.skills); ruleFiles.push(...frameworkArtifacts.rules); // Deploy if (!commandsOnly && !skillsOnly && !rulesOnly) { console.log(`\nDeploying ${agentFiles.length} agents (YAML format)...`); deployAgents(agentFiles, target, opts); } if (shouldDeployCommands || commandsOnly) { console.log(`\nDeploying ${commandFiles.length} commands as agents (YAML format)...`); deployCommands(commandFiles, target, opts); } if (shouldDeploySkills || skillsOnly) { console.log(`\nDeploying ${skillDirs.length} skills...`); deploySkills(skillDirs, target, opts); } if (shouldDeployRules || rulesOnly) { console.log(`\nDeploying ${ruleFiles.length} rules...`); deployRules(ruleFiles, target, opts); } await postDeploy(target, opts); console.log('\n=== Copilot deployment complete ===\n'); } // ============================================================================ // Default Export // ============================================================================ export default { name, aliases, paths, support, capabilities, transformAgent, transformCommand, mapModel, getTools, deployAgents, deployCommands, deploySkills, deployRules, createCopilotInstructions, postDeploy, getFileExtension, deploy };