UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

1,077 lines (941 loc) 35.9 kB
/** * Factory AI Provider * * Deploys agents as "droids" in Factory AI format. Factory uses a different * frontmatter structure with kebab-case names and mapped tools. * * Deployment paths: * - Agents: .factory/droids/ * - Commands: .factory/commands/ * - Skills: .factory/skills/ * - Rules: .factory/rules/ * * Special features: * - Transforms agent names to kebab-case * - Maps Claude tools to Factory equivalents * - Enables custom droids in ~/.factory/settings.json * - Creates/updates AGENTS.md from template */ import realFs from 'fs'; import { createRequire } from 'module'; const _require = createRequire(import.meta.url); let fs; try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; } import path from 'path'; import { ensureDir, listMdFiles, listMdFilesRecursive, writeFile, deployFiles, toKebabCase, stripJsonComments, createAgentsMdFromTemplate, initializeFrameworkWorkspace, getAddonAgentFiles, getAddonCommandFiles, getAddonRuleFiles, getAddonSkillDirs, listSkillDirs, deploySkillDir, deploySkillsWithKernelRouting, isKernelSkill, pruneStaleAiwgSkills, computeAllKernelNames, normalizeDeploymentMode, collectFrameworkArtifacts, cleanupOldRuleFiles, filterCommandsAgainstSkills, deploySoulCompanions } from './base.mjs'; // ============================================================================ // Provider Configuration // ============================================================================ export const name = 'factory'; export const aliases = []; export const paths = { agents: '.factory/droids/', commands: '.factory/commands/', // Skills sequestered under .factory/.aiwg/skills/ — index-driven discovery (#1212). skills: '.factory/.aiwg/skills/', rules: '.factory/rules/' }; // Kernel skills (always-loaded) deploy to the platform-native dir. export const kernelSkillsPath = '.factory/skills/'; export const support = { agents: 'native', commands: 'native', skills: 'native', rules: 'conventional' }; export const capabilities = { skills: true, rules: true, aggregatedOutput: false, yamlFormat: false }; // ============================================================================ // Tool Mapping // ============================================================================ /** * Map Claude Code tools to Factory AI equivalents */ export function mapToolsToFactory(toolsString, agentName) { // Default comprehensive tool set if no tools specified if (!toolsString) { return ["Read", "LS", "Grep", "Glob", "Edit", "Create", "Execute", "Task", "TodoWrite", "WebSearch", "FetchUrl"]; } // Parse tools (comma-separated or array format) 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); } // Tool mapping: Claude Code → Factory const toolMap = { 'Bash': 'Execute', 'Write': 'Create', // Will add Edit too 'WebFetch': 'FetchUrl', 'Read': 'Read', 'Grep': 'Grep', 'Glob': 'Glob', 'LS': 'LS' }; const factoryTools = new Set(); // Map original tools for (const tool of originalTools) { // MultiEdit maps to Edit + ApplyPatch in Factory if (tool === 'MultiEdit') { factoryTools.add('Edit'); factoryTools.add('ApplyPatch'); continue; } const mapped = toolMap[tool] || tool; factoryTools.add(mapped); // If Write is present, add both Create and Edit if (tool === 'Write') { factoryTools.add('Create'); factoryTools.add('Edit'); } } // Orchestration agents need Task tool for invoking subagents const orchestrationAgents = [ 'executive-orchestrator', 'intake-coordinator', 'documentation-synthesizer', 'project-manager', 'deployment-manager', 'test-architect', 'architecture-designer', 'requirements-analyst', 'security-architect', 'technical-writer' ]; const normalizedName = (agentName || '').toLowerCase().replace(/\s+/g, '-'); if (orchestrationAgents.some(oa => normalizedName.includes(oa))) { factoryTools.add('Task'); factoryTools.add('TodoWrite'); } // Add web tools if WebFetch was present if (originalTools.includes('WebFetch')) { factoryTools.add('FetchUrl'); factoryTools.add('WebSearch'); } // Convert to sorted array for consistency return Array.from(factoryTools).sort(); } // ============================================================================ // Model Mapping // ============================================================================ // Default Factory models (fallback if config not loaded) const DEFAULT_FACTORY_MODELS = { reasoning: 'claude-opus-4-6', coding: 'claude-sonnet-4-6', efficiency: 'claude-haiku-4-5-20251001' }; /** * Sonnet-tier agents that need higher reasoningEffort than the default "medium". * These are deploy-time overrides — source agent files remain provider-agnostic. * * Rationale: these roles perform analysis, review, or verification where * thoroughness is critical despite running on the cost-efficient sonnet tier. */ const REASONING_EFFORT_OVERRIDES = { // Review & analysis — must be thorough 'code-reviewer': 'high', 'security-auditor': 'high', 'test-architect': 'high', 'requirements-reviewer': 'high', 'api-designer': 'high', 'regression-analyst': 'high', 'compliance-checker': 'high', 'privacy-officer': 'high', 'reliability-engineer': 'high', 'migration-planner': 'high', 'incident-responder': 'high', // Grounding agents — verification accuracy is paramount 'security-grounding-agent': 'high', 'compliance-grounding-agent': 'high', 'technology-grounding-agent': 'high', 'performance-grounding-agent': 'high', // Lightweight roles — mechanical, not analytical 'documentation-archivist': 'low', }; /** * Map model tier to Factory reasoningEffort level. * * Priority: agent-specific override > agent frontmatter > models.json per-tier > default by model tier. * Valid levels: off, low, medium, high. */ export function mapReasoningEffort(originalModel, modelsConfig, frontmatterEffort, agentName) { // Explicit frontmatter override wins (source-level) if (frontmatterEffort && ['off', 'low', 'medium', 'high'].includes(frontmatterEffort)) { return frontmatterEffort; } // Agent-specific deploy-time override (Factory provider policy) const normalizedName = (agentName || '').toLowerCase().replace(/\s+/g, '-'); if (REASONING_EFFORT_OVERRIDES[normalizedName]) { return REASONING_EFFORT_OVERRIDES[normalizedName]; } const factoryConfig = modelsConfig?.factory || {}; const clean = (originalModel || 'sonnet').toLowerCase().replace(/['"]/g, ''); // Check models.json for per-tier reasoningEffort if (/opus/i.test(clean) && factoryConfig.reasoning?.reasoningEffort) { return factoryConfig.reasoning.reasoningEffort; } if (/haiku/i.test(clean) && factoryConfig.efficiency?.reasoningEffort) { return factoryConfig.efficiency.reasoningEffort; } if (factoryConfig.coding?.reasoningEffort) { return factoryConfig.coding.reasoningEffort; } // Default mapping by model tier if (/opus/i.test(clean)) return 'high'; if (/haiku/i.test(clean)) return 'low'; return 'medium'; } /** * Map model shorthand to Factory AI format */ export function mapModel(originalModel, modelCfg, modelsConfig) { // Safe access to nested config with fallbacks const factoryConfig = modelsConfig?.factory || {}; const defaultReasoning = factoryConfig.reasoning?.model || DEFAULT_FACTORY_MODELS.reasoning; const defaultCoding = factoryConfig.coding?.model || DEFAULT_FACTORY_MODELS.coding; const defaultEfficiency = factoryConfig.efficiency?.model || DEFAULT_FACTORY_MODELS.efficiency; // 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 || defaultReasoning; if (/haiku/i.test(clean)) return modelCfg.efficiencyModel || defaultEfficiency; return modelCfg.codingModel || defaultCoding; } // Prefer factory-specific shorthand over shared shorthand const factoryModels = modelsConfig?.factory_shorthand || modelsConfig?.shorthand || { 'opus': defaultReasoning, 'sonnet': defaultCoding, 'haiku': defaultEfficiency, 'inherit': 'inherit' }; const clean = (originalModel || 'sonnet').toLowerCase().replace(/['"]/g, ''); // Match to Factory model for (const [key, value] of Object.entries(factoryModels)) { if (clean.includes(key)) return value; } return defaultCoding; // default } // ============================================================================ // Content Transformation // ============================================================================ /** * Transform AIWG agent to Factory droid 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 rawName = 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 effortMatch = frontmatter.match(/reasoningEffort:\s*(.+)/)?.[1]?.trim(); // Convert name to kebab-case for Factory compatibility const name = toKebabCase(rawName); // Map model to Factory format const factoryModel = mapModel(modelMatch, opts, modelsConfig); // Map reasoning effort based on model tier, with agent-specific overrides const reasoningEffort = mapReasoningEffort(modelMatch, modelsConfig, effortMatch, name); // Map tools to Factory equivalents const factoryTools = mapToolsToFactory(toolsMatch, name); // Generate Factory droid frontmatter const factoryFrontmatter = `--- name: ${name} description: ${description || 'AIWG SDLC agent'} model: ${factoryModel} reasoningEffort: ${reasoningEffort} tools: ${JSON.stringify(factoryTools)} ---`; return `${factoryFrontmatter}\n\n${body.trim()}`; } /** * Map a skill commandHint.allowedTools string to Factory equivalents. * * Skill `allowedTools` differs from agent `tools` — it may carry Claude * Code's allowlist syntax (e.g. `Bash(git *, gh *)`, `Bash(npm:*)`) where * the parentheses contain commas that must NOT be treated as token * separators. This tokenizer respects parentheses, then maps each token's * identifier head: * * Bash(git *, gh *) → Execute(git *, gh *) * Bash → Execute * Write → Create (Factory pairs Create+Edit but for hint-only * use we keep the head unique) * MultiEdit → Edit * mcp__gitea__* → mcp__gitea__* (passthrough) * * Returns a comma-separated string suitable for re-insertion into SKILL.md. */ export function mapAllowedToolsString(allowedTools) { if (!allowedTools) return ''; const tokens = []; let buf = ''; let depth = 0; for (const ch of allowedTools) { if (ch === '(') { depth += 1; buf += ch; continue; } if (ch === ')') { depth = Math.max(0, depth - 1); buf += ch; continue; } if (ch === ',' && depth === 0) { if (buf.trim()) tokens.push(buf.trim()); buf = ''; continue; } buf += ch; } if (buf.trim()) tokens.push(buf.trim()); const headMap = { Bash: 'Execute', Write: 'Create', MultiEdit: 'Edit', WebFetch: 'FetchUrl' }; const mapped = tokens.map(tok => { const m = tok.match(/^([A-Za-z_][\w]*)(.*)$/); if (!m) return tok; const [, head, rest] = m; const newHead = headMap[head] || head; return `${newHead}${rest}`; }); // Deduplicate while preserving order const seen = new Set(); const out = []; for (const t of mapped) { if (!seen.has(t)) { seen.add(t); out.push(t); } } return out.join(', '); } /** * Transform SKILL.md frontmatter for Factory. * * Skills declare optional `commandHint:` metadata describing how the skill * should be invoked as a slash command. The hint carries Claude-native tool * names and Claude model shorthand, both of which are wrong on Factory: * * commandHint: * allowedTools: 'WebSearch, Read, Write, Bash' ← Bash/Write/MultiEdit * model: sonnet ← bare shorthand * * This transform rewrites the indented allowedTools and model fields so * the deployed SKILL.md is consistent with how agent droids are transformed * by transformAgent() — Bash → Execute, Write → Create+Edit, MultiEdit → * Edit+ApplyPatch, sonnet → claude-sonnet-4-x, etc. * * Issue: #1056 (upstream #102) */ export function transformSkillFrontmatter(content, opts) { const { modelsConfig = {} } = opts || {}; // Match frontmatter, allowing an optional leading HTML comment. const fmMatch = content.match(/^((?:<!--[\s\S]*?-->\s*)?)---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!fmMatch) return content; const [, prefix, frontmatter, body] = fmMatch; // Step 1: filter top-level keys to the Factory-recognized allowlist (#102). // Anything else is AIWG-internal metadata or Claude Code-specific and would // confuse Factory's skill loader. Block-aware parsing preserves multi-line // values like the `commandHint:` map. const filtered = filterFactorySkillFrontmatter(frontmatter); // Step 2: rewrite the still-Claude-flavored bits inside `commandHint:` — // tool name remap and model shorthand → Factory model id. let updated = filtered; updated = updated.replace( /^(\s+allowedTools:\s*)(['"]?)([^\n'"]+)\2/m, (_match, lead, quote, raw) => { const mapped = mapAllowedToolsString(raw); const q = quote || "'"; return `${lead}${q}${mapped}${q}`; } ); updated = updated.replace( /^(\s+model:\s*)(['"]?)([^\s\n'"]+)\2/m, (_match, lead, quote, raw) => { const mapped = mapModel(raw, opts, modelsConfig); const q = quote; return `${lead}${q}${mapped}${q}`; } ); if (updated === frontmatter) return content; return `${prefix}---\n${updated}\n---\n${body}`; } /** * Top-level frontmatter keys Factory recognizes for skills. * * The list comes from Factory's skill loader contract — anything not in it is * either AIWG-internal (`namespace`, `platforms`, `memory`, `category`, * `orchestration`, `subagent-optimized`, `version`) or a Claude Code-only * concept (`tools`, `model` at the top level — Factory handles those via * `commandHint:` for skills). * * Issue: #102 — Factory provider must strip non-Factory fields, not pass them through. */ const FACTORY_SKILL_TOP_LEVEL_KEYS = new Set([ 'name', 'description', 'user-invocable', 'disable-model-invocation', 'commandHint', ]); /** * Filter a YAML frontmatter string down to Factory-recognized top-level keys. * * Preserves the order and exact line content of kept blocks (no YAML round-trip), * including indented continuation lines. Block boundaries are detected by * unindented `key:` lines. */ export function filterFactorySkillFrontmatter(frontmatter) { const lines = frontmatter.split('\n'); const kept = []; let keepingCurrent = true; for (const line of lines) { const topKeyMatch = line.match(/^([A-Za-z_][\w-]*)\s*:/); if (topKeyMatch) { // Start of a new top-level block — decide whether to keep it. keepingCurrent = FACTORY_SKILL_TOP_LEVEL_KEYS.has(topKeyMatch[1]); if (keepingCurrent) kept.push(line); continue; } // Continuation of the current block (indented value, list item, blank line). if (keepingCurrent) kept.push(line); } return kept.join('\n'); } /** * Transform command for Factory * Commands use similar format to agents */ export function transformCommand(srcPath, content, opts) { // Commands are simpler - just basic frontmatter transformation const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!fmMatch) return content; const [, frontmatter, body] = fmMatch; // Extract metadata const rawName = frontmatter.match(/name:\s*(.+)/)?.[1]?.trim(); const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim(); const args = frontmatter.match(/args:\s*(.+)/)?.[1]?.trim(); const argumentHint = frontmatter.match(/argument-hint:\s*(.+)/)?.[1]?.trim(); // Convert name to kebab-case const name = toKebabCase(rawName) || path.basename(srcPath, '.md'); // Build Factory command frontmatter let factoryFrontmatter = `--- name: ${name} description: ${description || 'AIWG command'} argument-hint: ${argumentHint || '<task-description>'}`; if (args) { factoryFrontmatter += `\nargs: ${args}`; } factoryFrontmatter += '\n---'; // Prepend $ARGUMENTS so Factory passes user input into the prompt body. // Factory silently drops anything typed after the command name if $ARGUMENTS // is not present. This is a deploy-time transform only — source files are unchanged. const bodyWithArgs = `$ARGUMENTS\n\n${body.trim()}`; return `${factoryFrontmatter}\n\n${bodyWithArgs}`; } // ============================================================================ // Deployment Functions // ============================================================================ /** * Deploy agents to .factory/droids/ */ export function deployAgents(agentFiles, targetDir, opts) { const destDir = path.join(targetDir, paths.agents); ensureDir(destDir, opts.dryRun); return deployFiles(agentFiles, destDir, { ...opts, injectPlatform: true }, transformAgent); } /** * Deploy commands to .factory/commands/ */ export function deployCommands(commandFiles, targetDir, opts) { const destDir = path.join(targetDir, paths.commands); ensureDir(destDir, opts.dryRun); return deployFiles(commandFiles, destDir, opts, transformCommand); } /** * Deploy skills with kernel-vs-standard routing (#1212/#1216). * - kernel skills → .factory/skills/ (platform-native, always-loaded) * - standard → .factory/.aiwg/skills/ (index-discoverable) */ export function deploySkills(skillDirs, targetDir, opts) { const standardDestDir = path.join(targetDir, paths.skills); const kernelDestDir = path.join(targetDir, kernelSkillsPath); const skillOpts = { ...opts, transformSkillMd: transformSkillFrontmatter }; deploySkillsWithKernelRouting(skillDirs, standardDestDir, kernelDestDir, skillOpts); } /** * Deploy rules to .factory/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); } // ============================================================================ // AGENTS.md // ============================================================================ /** * Create/update AGENTS.md from Factory template */ export function createAgentsMd(target, srcRoot, dryRun) { createAgentsMdFromTemplate(target, srcRoot, 'factory/AGENTS.md.aiwg-template', dryRun); } // ============================================================================ // Factory Settings // ============================================================================ /** * Enable custom droids in Factory settings.json */ export function enableFactoryCustomDroids(dryRun) { const homeDir = process.env.HOME || process.env.USERPROFILE; if (!homeDir) { console.warn('Could not determine home directory, skipping Factory settings configuration'); return; } const settingsDir = path.join(homeDir, '.factory'); const settingsPath = path.join(settingsDir, 'settings.json'); let settings = {}; let originalContent = ''; let hasExistingFile = false; // Read existing settings if present if (fs.existsSync(settingsPath)) { hasExistingFile = true; try { originalContent = fs.readFileSync(settingsPath, 'utf8'); // Strip JSONC comments before parsing const jsonContent = stripJsonComments(originalContent); settings = JSON.parse(jsonContent); } catch (err) { console.warn(`Warning: Could not parse existing Factory settings.json: ${err.message}`); console.warn('Will add enableCustomDroids setting using text manipulation to preserve file...'); // Try to add the setting via text manipulation if (originalContent.includes('"enableCustomDroids"')) { if (originalContent.includes('"enableCustomDroids": true') || originalContent.includes('"enableCustomDroids":true')) { console.log('Factory Custom Droids already enabled in settings'); return; } // Replace false with true if (!dryRun) { const updatedContent = originalContent.replace( /"enableCustomDroids"\s*:\s*false/, '"enableCustomDroids": true' ); fs.writeFileSync(settingsPath, updatedContent, 'utf8'); console.log(`Enabled Custom Droids in Factory settings: ${settingsPath}`); } else { console.log(`[dry-run] Would enable Custom Droids in ${settingsPath}`); } return; } // Setting doesn't exist, add it after the first { if (!dryRun) { const insertPoint = originalContent.indexOf('{') + 1; const updatedContent = originalContent.slice(0, insertPoint) + '\n "enableCustomDroids": true,' + originalContent.slice(insertPoint); fs.writeFileSync(settingsPath, updatedContent, 'utf8'); console.log(`Enabled Custom Droids in Factory settings: ${settingsPath}`); } else { console.log(`[dry-run] Would enable Custom Droids in ${settingsPath}`); } return; } } // Check if Custom Droids already enabled if (settings.enableCustomDroids === true) { console.log('Factory Custom Droids already enabled in settings'); return; } // Enable Custom Droids settings.enableCustomDroids = true; if (dryRun) { console.log(`[dry-run] Would enable Custom Droids in ${settingsPath}`); console.log(`[dry-run] New setting: enableCustomDroids: true`); } else { // Ensure settings directory exists if (!fs.existsSync(settingsDir)) { fs.mkdirSync(settingsDir, { recursive: true }); } if (hasExistingFile && originalContent.includes('//')) { // File has comments - use text manipulation to preserve them if (originalContent.includes('"enableCustomDroids"')) { const updatedContent = originalContent.replace( /"enableCustomDroids"\s*:\s*false/, '"enableCustomDroids": true' ); fs.writeFileSync(settingsPath, updatedContent, 'utf8'); } else { const insertPoint = originalContent.indexOf('{') + 1; const updatedContent = originalContent.slice(0, insertPoint) + '\n "enableCustomDroids": true,' + originalContent.slice(insertPoint); fs.writeFileSync(settingsPath, updatedContent, 'utf8'); } } else { // No comments or new file - safe to use JSON.stringify fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); } console.log(`Enabled Custom Droids in Factory settings: ${settingsPath}`); console.log('Note: You may need to restart droid for this setting to take effect'); } } // ============================================================================ // Factory Hooks // ============================================================================ /** * Deploy SessionStart hook for AIWG pre-flight checks. * * Writes a SessionStart hook to ~/.factory/settings.json that runs * `aiwg sync --dry-run --quiet` on every new session. Preserves * existing hooks and settings. */ export function deployFactoryHooks(dryRun) { const homeDir = process.env.HOME || process.env.USERPROFILE; if (!homeDir) { console.warn('Could not determine home directory, skipping Factory hooks configuration'); return; } const settingsDir = path.join(homeDir, '.factory'); const settingsPath = path.join(settingsDir, 'settings.json'); let settings = {}; let originalContent = ''; let hasExistingFile = false; // Read existing settings if present if (fs.existsSync(settingsPath)) { hasExistingFile = true; try { originalContent = fs.readFileSync(settingsPath, 'utf8'); const jsonContent = stripJsonComments(originalContent); settings = JSON.parse(jsonContent); } catch (err) { console.warn(`Warning: Could not parse Factory settings.json for hooks: ${err.message}`); console.warn('Skipping hook deployment to avoid corrupting settings file.'); return; } } // Define the AIWG pre-flight hook const aiwgHook = { type: 'command', command: 'aiwg sync --dry-run --quiet' }; const aiwgMatcher = { matcher: '*', hooks: [aiwgHook] }; // Initialize hooks structure if missing if (!settings.hooks) { settings.hooks = {}; } if (!settings.hooks.SessionStart) { settings.hooks.SessionStart = []; } // Check if AIWG hook already exists (by command string) const alreadyInstalled = settings.hooks.SessionStart.some(entry => entry.hooks?.some(h => h.command && h.command.includes('aiwg sync')) ); if (alreadyInstalled) { console.log('Factory SessionStart hook already installed for AIWG pre-flight'); return; } // Add the hook settings.hooks.SessionStart.push(aiwgMatcher); if (dryRun) { console.log(`[dry-run] Would add SessionStart hook to ${settingsPath}`); console.log(`[dry-run] Hook: aiwg sync --dry-run --quiet`); } else { if (!fs.existsSync(settingsDir)) { fs.mkdirSync(settingsDir, { recursive: true }); } // Since we've successfully parsed the JSON, safe to write back fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); console.log(`Deployed SessionStart hook to ${settingsPath}`); console.log('Hook: aiwg sync --dry-run --quiet (runs on every new Factory session)'); } } // ============================================================================ // Post-Deployment // ============================================================================ export async function postDeploy(targetDir, opts) { // Initialize framework workspace structure initializeFrameworkWorkspace(targetDir, opts.mode, opts.dryRun, opts.srcRoot); // Enable custom droids in Factory settings enableFactoryCustomDroids(opts.dryRun); // Deploy SessionStart hook for AIWG pre-flight checks deployFactoryHooks(opts.dryRun); // Create/update AGENTS.md if requested if (opts.createAgentsMd) { createAgentsMd(targetDir, opts.srcRoot, opts.dryRun); } } // ============================================================================ // Factory Plugin Bundle // ============================================================================ /** * Generate a .factory-plugin/ bundle for distributing AIWG as a Factory plugin. * * Factory plugins use the structure: * .factory-plugin/ * plugin.json — manifest (name, version, description, contents) * droids/ — agent definitions * commands/ — command definitions * skills/ — skill definitions * rules/ — rule definitions * hooks.json — hook configuration * * Invoke via: aiwg use sdlc --provider factory --as-plugin */ export function generatePluginBundle(targetDir, opts) { const pluginDir = path.join(targetDir, '.factory-plugin'); const { dryRun, version } = opts; // Read package.json for version info let pkgVersion = version || 'unknown'; if (!version) { try { const pkgPath = path.join(opts.srcRoot, 'package.json'); if (fs.existsSync(pkgPath)) { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); pkgVersion = pkg.version || 'unknown'; } } catch (_) { /* use default */ } } // Count deployed artifacts const droidDir = path.join(targetDir, paths.agents); const cmdDir = path.join(targetDir, paths.commands); const skillDir = path.join(targetDir, paths.skills); const ruleDir = path.join(targetDir, paths.rules); const countFiles = (dir, ext) => { try { return fs.readdirSync(dir).filter(f => f.endsWith(ext || '.md')).length; } catch (_) { return 0; } }; const droidCount = countFiles(droidDir); const commandCount = countFiles(cmdDir); const ruleCount = countFiles(ruleDir); let skillCount = 0; try { skillCount = fs.readdirSync(skillDir).filter(f => fs.statSync(path.join(skillDir, f)).isDirectory() ).length; } catch (_) { /* 0 */ } // Build plugin manifest const manifest = { name: 'aiwg-sdlc', version: pkgVersion, description: `AIWG SDLC Framework v${pkgVersion}${droidCount} droids, ${commandCount} commands, ${skillCount} skills, ${ruleCount} rules. Phase-based SDLC workflows with specialized agents for architecture, security, testing, and deployment.`, author: { name: 'AIWG Contributors', email: 'support@aiwg.io' }, homepage: 'https://aiwg.io', repository: 'https://github.com/jmagly/aiwg', license: 'MIT', contents: { droids: droidCount, commands: commandCount, skills: skillCount, rules: ruleCount }, hooks: { SessionStart: [ { matcher: '*', hooks: [ { type: 'command', command: 'aiwg sync --dry-run --quiet' } ] } ] }, keywords: [ 'sdlc', 'software-development', 'project-management', 'security', 'testing', 'architecture', 'devops', 'aiwg' ] }; if (dryRun) { console.log(`[dry-run] Would create ${pluginDir}/plugin.json`); console.log(`[dry-run] Plugin: aiwg-sdlc v${pkgVersion} (${droidCount} droids, ${commandCount} commands, ${skillCount} skills, ${ruleCount} rules)`); return; } ensureDir(pluginDir, false); fs.writeFileSync( path.join(pluginDir, 'plugin.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8' ); console.log(`Generated Factory plugin manifest: ${pluginDir}/plugin.json`); console.log(`Plugin: aiwg-sdlc v${pkgVersion} (${droidCount} droids, ${commandCount} commands, ${skillCount} skills, ${ruleCount} rules)`); } // ============================================================================ // File Extension // ============================================================================ export function getFileExtension(type) { return '.md'; } // ============================================================================ // Main Deploy Function // ============================================================================ /** * Main deployment function for Factory provider */ export async function deploy(opts) { const { srcRoot, target, mode, deployCommands: shouldDeployCommands, deploySkills: shouldDeploySkills, deployRules: shouldDeployRules, commandsOnly, skillsOnly, rulesOnly, dryRun, createAgentsMd: shouldCreateAgentsMd } = opts; console.log(`\n=== Factory AI Provider ===`); console.log(`Target: ${target}`); console.log(`Mode: ${mode}`); // Collect source files based on mode const agentFiles = []; const commandFiles = []; const ruleFiles = []; const skillDirs = []; 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 (shouldDeployRules || rulesOnly) { ruleFiles.push(...getAddonRuleFiles(srcRoot)); } if (shouldDeploySkills || skillsOnly) { skillDirs.push(...getAddonSkillDirs(srcRoot)); // Holistic post-deploy cleanup of stale AIWG-managed kernel // skills (renamed/removed sources). Uses the global kernel set // (computeAllKernelNames walks all source frameworks/addons), // not just this-call's skillDirs, because aiwg use invokes // deploy-agents.mjs multiple times. { const _kernelDestDir = path.isAbsolute(kernelSkillsPath) ? kernelSkillsPath : path.join(target, kernelSkillsPath); pruneStaleAiwgSkills(_kernelDestDir, computeAllKernelNames(srcRoot), opts); } } } const frameworkArtifacts = collectFrameworkArtifacts(srcRoot, normalizedMode, { includeAgents: true, includeCommands: shouldDeployCommands || commandsOnly, includeSkills: shouldDeploySkills || skillsOnly, includeRules: shouldDeployRules || rulesOnly, recursiveCommands: true, consolidatedSdlcRules: true }); agentFiles.push(...frameworkArtifacts.agents); const soulFiles = [...(frameworkArtifacts.souls || [])]; commandFiles.push(...frameworkArtifacts.commands); skillDirs.push(...frameworkArtifacts.skills); ruleFiles.push(...frameworkArtifacts.rules); // Deploy based on flags if (!commandsOnly && !skillsOnly && !rulesOnly) { console.log(`\nDeploying ${agentFiles.length} agents as droids...`); deployAgents(agentFiles, target, opts); // Deploy soul companion files alongside agents if (soulFiles.length > 0) { const destDir = path.join(target, paths.agents); console.log(`\nDeploying ${soulFiles.length} soul files...`); deploySoulCompanions(soulFiles, destDir, opts); } } // Filter commands that collide with skills (skills take precedence) const filteredCommands = (shouldDeploySkills || skillsOnly) ? filterCommandsAgainstSkills(commandFiles, skillDirs) : commandFiles; if (shouldDeployCommands || commandsOnly) { console.log(`\nDeploying ${filteredCommands.length} commands...`); deployCommands(filteredCommands, 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); } // Post-deployment await postDeploy(target, { ...opts, createAgentsMd: shouldCreateAgentsMd }); // Generate Factory plugin bundle if requested if (opts.asPlugin) { console.log('\nGenerating Factory plugin bundle...'); generatePluginBundle(target, opts); } console.log('\n=== Factory deployment complete ===\n'); } // ============================================================================ // Default Export // ============================================================================ export default { name, aliases, paths, kernelSkillsPath, support, capabilities, transformAgent, transformCommand, transformSkillFrontmatter, filterFactorySkillFrontmatter, mapModel, mapReasoningEffort, mapToolsToFactory, deployAgents, deployCommands, deploySkills, deployRules, createAgentsMd, enableFactoryCustomDroids, deployFactoryHooks, generatePluginBundle, postDeploy, getFileExtension, deploy };