/** * AGENTS.md generation functionality * Core module for generating project documentation for AI agents */ import { loadConfig, loadConfigWithoutCreation, resolveModelForPhase, type HoneConfig, type AgentType, } from './config' import { readFile, writeFile, mkdir } from 'fs/promises' import { extname, join, relative } from 'path' import { existsSync, readFileSync, readdirSync } from 'fs' import { AgentClient } from './agent-client' import { log, logError, logVerbose, logVerboseError } from './logger' /** * Central constant for the agents documentation directory name * This is now configurable via config file using the agentsDocsDir property */ export const AGENTS_DOCS_DIR = '.agents/' /** * Resolve the agents documentation directory from config. * Returns config.agentsDocsDir if set, otherwise falls back to AGENTS_DOCS_DIR constant. * @param config - Optional HoneConfig to read agentsDocsDir from * @returns The resolved agents documentation directory path * @internal */ export function getAgentsDocsDir(config?: HoneConfig): string { if (config?.agentsDocsDir) { return config.agentsDocsDir } return AGENTS_DOCS_DIR } const GENERATED_BLOCK_START = '' const GENERATED_BLOCK_END = '' const GENERATED_BLOCK_REGEX = /[\s\S]*?/m const WORKFLOW_DIR = join('.github', 'workflows') export interface AgentsMdGeneratorOptions { projectPath?: string overwrite?: boolean agent?: AgentType } export interface ProjectAnalysis { languages: string[] buildSystems: string[] testingFrameworks: string[] dependencies: string[] architecture: string[] deployment: string[] } export interface GenerationResult { success: boolean mainFilePath?: string agentsDirPath?: string filesCreated: string[] error?: Error } interface TemplateSection { title: string content: string priority: number detailFile?: string } type MetadataSection = | 'languages' | 'buildSystems' | 'testingFrameworks' | 'architecture' | 'deployment' type MetadataSourceType = 'package.json' | 'workflow' | 'doc' | 'config' | 'agents-docs' /** @internal */ export interface MetadataSignal { section: MetadataSection value: string sourceType: MetadataSourceType sourceTag: string } const METADATA_SOURCE_PRIORITY: Record = { 'package.json': 0, workflow: 1, doc: 2, config: 3, 'agents-docs': 4, } function addMetadataSignal( signals: MetadataSignal[], section: MetadataSection, value: string, sourceType: MetadataSourceType, sourceTag: string ): void { const normalized = value.trim() if (!normalized) return signals.push({ section, value: normalized, sourceType, sourceTag, }) } /** @internal */ export function dedupeMetadataSignals(signals: MetadataSignal[]): MetadataSignal[] { const sorted = [...signals].sort((a, b) => { const sourceCompare = METADATA_SOURCE_PRIORITY[a.sourceType] - METADATA_SOURCE_PRIORITY[b.sourceType] if (sourceCompare !== 0) return sourceCompare const tagCompare = a.sourceTag.localeCompare(b.sourceTag) if (tagCompare !== 0) return tagCompare return a.value.localeCompare(b.value) }) const seen = new Set() const deduped: MetadataSignal[] = [] for (const signal of sorted) { const key = `${signal.section}::${signal.value.toLowerCase()}` if (seen.has(key)) continue seen.add(key) deduped.push(signal) } return deduped } function formatMetadataSection(signals: MetadataSignal[], section: MetadataSection): string[] { const sectionSignals = signals.filter(signal => signal.section === section) if (sectionSignals.length === 0) return [] return sectionSignals.map(signal => `${signal.value} (${signal.sourceTag})`) } /** @internal */ export function isUnavailableAgentResult(content: string): boolean { const normalized = content.trim().toLowerCase() if (!normalized) return true if (normalized.startsWith('not available')) return true if (normalized.startsWith('information not available')) return true if (normalized === 'unknown' || normalized === 'n/a' || normalized === 'na') return true return false } /** * Extract preservable non-generated sections from existing AGENTS.md * This keeps user-authored sections that are outside generated blocks. */ /** @internal */ export function extractPreservableContent( existingContent: string, generatedSectionTitles: string[] ): string | null { const generatedTitles = new Set( generatedSectionTitles.map(title => title.toLowerCase()).concat('agents.md') ) const lines = existingContent.split('\n') const preservedSections: string[] = [] let currentHeader = '' let currentHeaderLine = '' let currentHeaderDepth = 0 let currentSection: string[] = [] let inGeneratedSection = false let generatedSectionDepth = 0 const flushSection = () => { if (!currentHeader) return const normalizedHeader = currentHeader.toLowerCase() if (generatedTitles.has(normalizedHeader)) return const sectionBody = currentSection.join('\n').trim() if (!sectionBody) return const headerLine = currentHeaderLine.startsWith('#') ? currentHeaderLine : `## ${currentHeader}` preservedSections.push(`${headerLine}\n\n${sectionBody}`) } for (const line of lines) { const headerMatch = /^(#{1,6})\s+(.*)$/.exec(line) if (headerMatch) { const hashes = headerMatch[1] ?? '' const rawTitle = headerMatch[2] ?? '' const depth = hashes.length const title = rawTitle.trim() const normalizedTitle = title.toLowerCase() if (inGeneratedSection && depth > generatedSectionDepth) { continue } if (inGeneratedSection && depth <= generatedSectionDepth) { inGeneratedSection = false generatedSectionDepth = 0 } if (normalizedTitle === 'agents.md') { flushSection() currentHeader = '' currentHeaderLine = '' currentHeaderDepth = 0 currentSection = [] continue } if (generatedTitles.has(normalizedTitle)) { flushSection() currentHeader = '' currentHeaderLine = '' currentHeaderDepth = 0 currentSection = [] inGeneratedSection = true generatedSectionDepth = depth continue } if (currentHeader && depth <= currentHeaderDepth) { flushSection() currentHeader = '' currentHeaderLine = '' currentHeaderDepth = 0 currentSection = [] } if (!currentHeader) { currentHeader = title currentHeaderLine = line currentHeaderDepth = depth currentSection = [] continue } currentSection.push(line) continue } if (inGeneratedSection) continue if (currentHeader) { currentSection.push(line) } } flushSection() return preservedSections.length > 0 ? preservedSections.join('\n\n') : null } /** @internal */ export function mergeGeneratedContent( existingContent: string, generatedContent: string ): string | null { if ( !existingContent.includes(GENERATED_BLOCK_START) || !existingContent.includes(GENERATED_BLOCK_END) ) { return null } return existingContent.replace(GENERATED_BLOCK_REGEX, generatedContent.trim()) } /** * Analyze project structure and gather context for AGENTS.md generation * @param projectPath - The root project directory path * @param config - Optional HoneConfig for resolving agentsDocsDir */ async function analyzeProject(projectPath: string, config?: HoneConfig): Promise { const analysis: ProjectAnalysis = { languages: [], buildSystems: [], testingFrameworks: [], dependencies: [], architecture: [], deployment: [], } try { const metadataSignals: MetadataSignal[] = [] collectPackageJsonMetadataSignals(projectPath, metadataSignals) collectConfigMetadataSignals(projectPath, metadataSignals) collectWorkflowMetadataSignals(projectPath, metadataSignals) collectDocsMetadataSignals(projectPath, metadataSignals) collectAgentsDocsMetadataSignals(projectPath, metadataSignals, config) const deduped = dedupeMetadataSignals(metadataSignals) analysis.languages = formatMetadataSection(deduped, 'languages') analysis.buildSystems = formatMetadataSection(deduped, 'buildSystems') analysis.testingFrameworks = formatMetadataSection(deduped, 'testingFrameworks') analysis.architecture = formatMetadataSection(deduped, 'architecture') analysis.deployment = formatMetadataSection(deduped, 'deployment') analysis.dependencies = collectPackageJsonDependencies(projectPath) logVerbose(`[AgentsMd] Project analysis complete: ${JSON.stringify(analysis, null, 2)}`) return analysis } catch (error) { logVerboseError( `[AgentsMd] Error analyzing project: ${error instanceof Error ? error.message : error}` ) return analysis // Return partial analysis on error } } function readPackageJson(projectPath: string): Record | null { const pkgPath = join(projectPath, 'package.json') if (!existsSync(pkgPath)) return null try { return JSON.parse(readFileSync(pkgPath, 'utf-8')) as Record } catch (error) { logVerbose(`Could not parse package.json: ${error}`) return null } } function collectPackageJsonDependencies(projectPath: string): string[] { const pkg = readPackageJson(projectPath) if (!pkg) return [] const deps = { ...(pkg.dependencies as Record | undefined), ...(pkg.devDependencies as Record | undefined), } const detected: string[] = [] if (deps.react) detected.push('React') if (deps.next) detected.push('Next.js') if (deps.vue) detected.push('Vue.js') if (deps.express) detected.push('Express') if (deps.fastify) detected.push('Fastify') if (deps['commander'] || deps['commander.js']) detected.push('Commander.js') return detected } function collectPackageJsonMetadataSignals(projectPath: string, signals: MetadataSignal[]): void { const pkg = readPackageJson(projectPath) if (!pkg) return const sourceTag = 'package.json' const deps = { ...(pkg.dependencies as Record | undefined), ...(pkg.devDependencies as Record | undefined), } const scripts = pkg.scripts as Record | undefined if (scripts && Object.keys(scripts).length > 0) { addMetadataSignal(signals, 'buildSystems', 'npm scripts', 'package.json', sourceTag) } const packageManager = typeof pkg.packageManager === 'string' ? pkg.packageManager : '' if (packageManager.startsWith('bun')) { addMetadataSignal(signals, 'buildSystems', 'Bun', 'package.json', sourceTag) } if (packageManager.startsWith('pnpm')) { addMetadataSignal(signals, 'buildSystems', 'pnpm', 'package.json', sourceTag) } if (packageManager.startsWith('yarn')) { addMetadataSignal(signals, 'buildSystems', 'Yarn', 'package.json', sourceTag) } if (packageManager.startsWith('npm')) { addMetadataSignal(signals, 'buildSystems', 'npm', 'package.json', sourceTag) } if (deps.typescript || deps['ts-node'] || deps.tsx) { addMetadataSignal(signals, 'languages', 'TypeScript', 'package.json', sourceTag) } if (deps.jest) addMetadataSignal(signals, 'testingFrameworks', 'Jest', 'package.json', sourceTag) if (deps.vitest) addMetadataSignal(signals, 'testingFrameworks', 'Vitest', 'package.json', sourceTag) if (deps.mocha) addMetadataSignal(signals, 'testingFrameworks', 'Mocha', 'package.json', sourceTag) if (deps.bun) addMetadataSignal(signals, 'testingFrameworks', 'Bun Test', 'package.json', sourceTag) if (deps['@playwright/test'] || deps.playwright) { addMetadataSignal(signals, 'testingFrameworks', 'Playwright', 'package.json', sourceTag) } if (deps.cypress) addMetadataSignal(signals, 'testingFrameworks', 'Cypress', 'package.json', sourceTag) if (deps.vite) addMetadataSignal(signals, 'buildSystems', 'Vite', 'package.json', sourceTag) if (deps.webpack) addMetadataSignal(signals, 'buildSystems', 'Webpack', 'package.json', sourceTag) if (deps.parcel) addMetadataSignal(signals, 'buildSystems', 'Parcel', 'package.json', sourceTag) if (deps.rollup) addMetadataSignal(signals, 'buildSystems', 'Rollup', 'package.json', sourceTag) if (deps.esbuild) addMetadataSignal(signals, 'buildSystems', 'esbuild', 'package.json', sourceTag) } function listFilesByExtensions( root: string, extensions: Set, ignoreDirs: Set ): string[] { const files: string[] = [] try { const entries = readdirSync(root, { withFileTypes: true }) for (const entry of entries) { const entryPath = join(root, entry.name) if (entry.isDirectory()) { if (ignoreDirs.has(entry.name)) continue files.push(...listFilesByExtensions(entryPath, extensions, ignoreDirs)) } else if (entry.isFile()) { const ext = extname(entry.name).toLowerCase() if (extensions.has(ext)) { files.push(entryPath) } } } } catch (error) { logVerbose('[AgentsMd] Could not read directory during metadata discovery') } return files.sort() } /** @internal */ export function collectConfigMetadataSignals(projectPath: string, signals: MetadataSignal[]): void { const ignoreDirs = new Set([ '.git', 'node_modules', 'dist', 'build', 'coverage', '.next', 'out', '.agents', '.agents-docs', AGENTS_DOCS_DIR, '.plans', ]) const extensionMap: Record = { '.ts': 'TypeScript', '.tsx': 'TypeScript', '.js': 'JavaScript', '.jsx': 'JavaScript', '.mjs': 'JavaScript', '.cjs': 'JavaScript', '.py': 'Python', '.go': 'Go', '.rs': 'Rust', '.java': 'Java', '.kt': 'Kotlin', '.kts': 'Kotlin', '.rb': 'Ruby', '.php': 'PHP', '.cs': 'C#', } const extensions = new Set(Object.keys(extensionMap)) const files = listFilesByExtensions(projectPath, extensions, ignoreDirs) const seenExtensions = new Set() for (const filePath of files) { const ext = extname(filePath).toLowerCase() if (seenExtensions.has(ext)) continue const language = extensionMap[ext] if (!language) continue seenExtensions.add(ext) addMetadataSignal(signals, 'languages', language, 'config', `config:ext:${ext.slice(1)}`) } if (existsSync(join(projectPath, 'tsconfig.json'))) { addMetadataSignal(signals, 'languages', 'TypeScript', 'config', 'config:tsconfig') } if (existsSync(join(projectPath, 'jsconfig.json'))) { addMetadataSignal(signals, 'languages', 'JavaScript', 'config', 'config:jsconfig') } if (existsSync(join(projectPath, 'go.mod'))) { addMetadataSignal(signals, 'languages', 'Go', 'config', 'config:go.mod') addMetadataSignal(signals, 'buildSystems', 'Go modules', 'config', 'config:go.mod') } if (existsSync(join(projectPath, 'Cargo.toml'))) { addMetadataSignal(signals, 'languages', 'Rust', 'config', 'config:cargo.toml') addMetadataSignal(signals, 'buildSystems', 'Cargo', 'config', 'config:cargo.toml') } if (existsSync(join(projectPath, 'pom.xml'))) { addMetadataSignal(signals, 'languages', 'Java', 'config', 'config:pom.xml') addMetadataSignal(signals, 'buildSystems', 'Maven', 'config', 'config:pom.xml') } if ( existsSync(join(projectPath, 'build.gradle')) || existsSync(join(projectPath, 'build.gradle.kts')) ) { addMetadataSignal(signals, 'languages', 'Java/Kotlin', 'config', 'config:gradle') addMetadataSignal(signals, 'buildSystems', 'Gradle', 'config', 'config:gradle') } if ( existsSync(join(projectPath, 'requirements.txt')) || existsSync(join(projectPath, 'pyproject.toml')) || existsSync(join(projectPath, 'setup.py')) ) { addMetadataSignal(signals, 'languages', 'Python', 'config', 'config:python') } const pyprojectPath = join(projectPath, 'pyproject.toml') if (existsSync(pyprojectPath)) { try { const pyproject = readFileSync(pyprojectPath, 'utf-8').toLowerCase() if (pyproject.includes('[tool.poetry]')) { addMetadataSignal(signals, 'buildSystems', 'Poetry', 'config', 'config:pyproject') } else if (pyproject.includes('[tool.hatch]')) { addMetadataSignal(signals, 'buildSystems', 'Hatch', 'config', 'config:pyproject') } else if (pyproject.includes('[tool.flit]')) { addMetadataSignal(signals, 'buildSystems', 'Flit', 'config', 'config:pyproject') } else if (pyproject.includes('[build-system]')) { addMetadataSignal( signals, 'buildSystems', 'PEP 517 build backend', 'config', 'config:pyproject' ) } } catch (error) { logVerbose('[AgentsMd] Could not read pyproject.toml for build detection') } } if (existsSync(join(projectPath, 'bun.lock')) || existsSync(join(projectPath, 'bun.lockb'))) { addMetadataSignal(signals, 'buildSystems', 'Bun', 'config', 'config:bun.lock') } if (existsSync(join(projectPath, 'package-lock.json'))) { addMetadataSignal(signals, 'buildSystems', 'npm', 'config', 'config:package-lock') } if (existsSync(join(projectPath, 'yarn.lock'))) { addMetadataSignal(signals, 'buildSystems', 'Yarn', 'config', 'config:yarn.lock') } if (existsSync(join(projectPath, 'pnpm-lock.yaml'))) { addMetadataSignal(signals, 'buildSystems', 'pnpm', 'config', 'config:pnpm-lock') } if (existsSync(join(projectPath, 'Makefile'))) { addMetadataSignal(signals, 'buildSystems', 'Make', 'config', 'config:makefile') } if ( existsSync(join(projectPath, 'vite.config.ts')) || existsSync(join(projectPath, 'vite.config.js')) ) { addMetadataSignal(signals, 'buildSystems', 'Vite', 'config', 'config:vite') } if ( existsSync(join(projectPath, 'webpack.config.js')) || existsSync(join(projectPath, 'webpack.config.ts')) ) addMetadataSignal(signals, 'buildSystems', 'Webpack', 'config', 'config:webpack') if (existsSync(join(projectPath, '.parcelrc'))) { addMetadataSignal(signals, 'buildSystems', 'Parcel', 'config', 'config:parcel') } if ( existsSync(join(projectPath, 'rollup.config.js')) || existsSync(join(projectPath, 'rollup.config.ts')) ) { addMetadataSignal(signals, 'buildSystems', 'Rollup', 'config', 'config:rollup') } if ( existsSync(join(projectPath, 'jest.config.js')) || existsSync(join(projectPath, 'jest.config.ts')) ) addMetadataSignal(signals, 'testingFrameworks', 'Jest', 'config', 'config:jest') if ( existsSync(join(projectPath, 'vitest.config.js')) || existsSync(join(projectPath, 'vitest.config.ts')) ) { addMetadataSignal(signals, 'testingFrameworks', 'Vitest', 'config', 'config:vitest') } if ( existsSync(join(projectPath, 'playwright.config.ts')) || existsSync(join(projectPath, 'playwright.config.js')) ) { addMetadataSignal(signals, 'testingFrameworks', 'Playwright', 'config', 'config:playwright') } if ( existsSync(join(projectPath, 'cypress.config.ts')) || existsSync(join(projectPath, 'cypress.config.js')) ) { addMetadataSignal(signals, 'testingFrameworks', 'Cypress', 'config', 'config:cypress') } if (existsSync(join(projectPath, 'pytest.ini'))) addMetadataSignal(signals, 'testingFrameworks', 'pytest', 'config', 'config:pytest') if (existsSync(join(projectPath, 'tox.ini'))) addMetadataSignal(signals, 'testingFrameworks', 'tox', 'config', 'config:tox') if (existsSync(join(projectPath, 'src'))) { addMetadataSignal(signals, 'architecture', 'src/ directory structure', 'config', 'config:src') } if (existsSync(join(projectPath, 'apps')) || existsSync(join(projectPath, 'packages'))) { addMetadataSignal( signals, 'architecture', 'monorepo workspace layout', 'config', 'config:workspaces' ) } if (existsSync(join(projectPath, 'docker-compose.yml'))) { addMetadataSignal(signals, 'architecture', 'Docker Compose', 'config', 'config:docker-compose') addMetadataSignal(signals, 'deployment', 'Docker Compose', 'config', 'config:docker-compose') } if (existsSync(join(projectPath, 'Dockerfile'))) { addMetadataSignal( signals, 'architecture', 'Docker containerization', 'config', 'config:dockerfile' ) addMetadataSignal( signals, 'deployment', 'Docker containerization', 'config', 'config:dockerfile' ) } if (existsSync(join(projectPath, 'vercel.json'))) { addMetadataSignal(signals, 'deployment', 'Vercel', 'config', 'config:vercel') } if (existsSync(join(projectPath, 'netlify.toml'))) { addMetadataSignal(signals, 'deployment', 'Netlify', 'config', 'config:netlify') } if (existsSync(join(projectPath, 'fly.toml'))) { addMetadataSignal(signals, 'deployment', 'Fly.io', 'config', 'config:fly') } if (existsSync(join(projectPath, 'render.yaml')) || existsSync(join(projectPath, 'render.yml'))) { addMetadataSignal(signals, 'deployment', 'Render', 'config', 'config:render') } if (existsSync(join(projectPath, 'railway.json'))) { addMetadataSignal(signals, 'deployment', 'Railway', 'config', 'config:railway') } } /** @internal */ export function collectWorkflowMetadataSignals( projectPath: string, signals: MetadataSignal[] ): void { const workflowsPath = join(projectPath, WORKFLOW_DIR) if (!existsSync(workflowsPath)) return try { const workflowFiles = readdirSync(workflowsPath) .filter(file => file.endsWith('.yml') || file.endsWith('.yaml')) .sort() if (workflowFiles.length === 0) return const sourceTag = `workflow:${workflowFiles[0]}` addMetadataSignal(signals, 'architecture', 'GitHub Actions CI/CD', 'workflow', sourceTag) addMetadataSignal(signals, 'deployment', 'GitHub Actions CI/CD', 'workflow', sourceTag) } catch (error) { logVerbose('[AgentsMd] Could not read workflow metadata') } } function extractBracketedList(content: string, label: string): string[] { const regex = new RegExp(`^\\s*${label}\\s*:\\s*\\[([^\\]]*)\\]`, 'im') const match = content.match(regex) if (!match) return [] const list = match[1] ?? '' return list .split(',') .map(item => item.trim()) .filter(Boolean) } function extractLabeledValue(content: string, label: string): string { const regex = new RegExp(`^\\s*${label}\\s*:\\s*(.+)$`, 'im') const match = content.match(regex) return match?.[1]?.trim() ?? '' } /** @internal */ export function collectDocsMetadataSignals(projectPath: string, signals: MetadataSignal[]): void { const files = listMarkdownFiles(projectPath) for (const filePath of files) { let content = '' try { content = readFileSync(filePath, 'utf-8') } catch (error) { logVerbose('[AgentsMd] Could not read markdown for metadata discovery') continue } const relativePath = relative(projectPath, filePath) const sourceTag = `doc:${relativePath}` for (const language of extractBracketedList(content, 'PRIMARY LANGUAGES')) { addMetadataSignal(signals, 'languages', language, 'doc', sourceTag) } for (const system of extractBracketedList(content, 'BUILD SYSTEMS')) { addMetadataSignal(signals, 'buildSystems', system, 'doc', sourceTag) } for (const framework of extractBracketedList(content, 'TESTING FRAMEWORKS')) { addMetadataSignal(signals, 'testingFrameworks', framework, 'doc', sourceTag) } const architectureLabels = [ 'ARCHITECTURE PATTERN', 'DIRECTORY STRUCTURE', 'DESIGN PATTERNS', 'DATABASE', 'API DESIGN', ] for (const label of architectureLabels) { const value = extractLabeledValue(content, label) if (value) addMetadataSignal(signals, 'architecture', value, 'doc', sourceTag) } const deploymentLabels = [ 'DEPLOYMENT STRATEGY', 'CONTAINERIZATION', 'CI/CD', 'HOSTING', 'ENVIRONMENT MANAGEMENT', ] for (const label of deploymentLabels) { const value = extractLabeledValue(content, label) if (value) addMetadataSignal(signals, 'deployment', value, 'doc', sourceTag) } } } /** * Collect metadata signals from existing agents documentation files. * @param projectPath - The root project directory path * @param signals - Array to append discovered metadata signals to * @param config - Optional HoneConfig to resolve agentsDocsDir; uses default if not provided * @internal */ export function collectAgentsDocsMetadataSignals( projectPath: string, signals: MetadataSignal[], config?: HoneConfig ): void { const agentsDocsDir = getAgentsDocsDir(config) const agentsDocsPath = join(projectPath, agentsDocsDir) if (!existsSync(agentsDocsPath)) return const files = listFilesRecursive(agentsDocsPath, filePath => filePath.endsWith('.md')) // Extract directory name without trailing slash for source tag prefix const dirName = agentsDocsDir.replace(/\/$/, '') for (const filePath of files) { let content = '' try { content = readFileSync(filePath, 'utf-8') } catch (error) { logVerbose('[AgentsMd] Could not read agents-docs metadata file') continue } const relativePath = relative(agentsDocsPath, filePath) const sourceTag = `${dirName}:${relativePath}` for (const language of extractBracketedList(content, 'PRIMARY LANGUAGES')) { addMetadataSignal(signals, 'languages', language, 'agents-docs', sourceTag) } for (const system of extractBracketedList(content, 'BUILD SYSTEMS')) { addMetadataSignal(signals, 'buildSystems', system, 'agents-docs', sourceTag) } for (const framework of extractBracketedList(content, 'TESTING FRAMEWORKS')) { addMetadataSignal(signals, 'testingFrameworks', framework, 'agents-docs', sourceTag) } const architectureLabels = [ 'ARCHITECTURE PATTERN', 'DIRECTORY STRUCTURE', 'DESIGN PATTERNS', 'DATABASE', 'API DESIGN', ] for (const label of architectureLabels) { const value = extractLabeledValue(content, label) if (value) addMetadataSignal(signals, 'architecture', value, 'agents-docs', sourceTag) } const deploymentLabels = [ 'DEPLOYMENT STRATEGY', 'CONTAINERIZATION', 'CI/CD', 'HOSTING', 'ENVIRONMENT MANAGEMENT', ] for (const label of deploymentLabels) { const value = extractLabeledValue(content, label) if (value) addMetadataSignal(signals, 'deployment', value, 'agents-docs', sourceTag) } } } /** * Discovery prompts for analyzing different aspects of the project */ const DISCOVERY_PROMPTS = { languages: `Analyze this project's codebase to identify the primary programming languages used and their purposes. IMPORTANT LANGUAGE DETECTION RULES: - Look for source code files (.js, .ts, .py, .java, .go, .rs, .php, .rb, etc.) - Check package.json, requirements.txt, go.mod, Cargo.toml, pom.xml, build.gradle for dependencies - Identify language-specific configuration files (tsconfig.json, .eslintrc, setup.py, etc.) - For TypeScript projects, note if it's primarily TypeScript or mixed JS/TS - For frontend projects, distinguish between client-side and server-side languages CRITICAL: Your response MUST start directly with the structured format below. NO preambles like "Based on my analysis..." or "Here's what I found..." - start IMMEDIATELY with "PRIMARY LANGUAGES:". PRIMARY LANGUAGES: [language 1, language 2, ...] USAGE CONTEXT: [brief explanation of how each language is used in the project]`, buildSystems: `Analyze this project to identify build systems, package managers, and compilation/bundling tools. BUILD SYSTEM DETECTION RULES: - npm/yarn/pnpm: Look for package.json, package-lock.json, yarn.lock, pnpm-lock.yaml - Maven: Look for pom.xml, maven-wrapper files - Gradle: Look for build.gradle, gradlew files - Go modules: Look for go.mod, go.sum - Cargo: Look for Cargo.toml, Cargo.lock - Webpack: Look for webpack.config.js, webpack configurations - Vite: Look for vite.config.ts/js - Parcel: Look for .parcelrc, parcel configurations - Build scripts in package.json (build, bundle, compile commands) - Check .github/workflows/*.yml for build, lint, and format commands - Docker: Look for Dockerfile, docker-compose.yml - Make: Look for Makefile - Custom build scripts in various languages CRITICAL: Your response MUST start directly with the structured format below. NO preambles like "Based on my analysis..." or "Here's what I found..." - start IMMEDIATELY with "BUILD SYSTEMS:". BUILD SYSTEMS: [system 1, system 2, ...] BUILD COMMANDS: [key build commands developers should know] LINT COMMANDS: [lint commands developers should know] FORMAT COMMANDS: [formatting commands developers should know] BUNDLING: [bundling tools if applicable]`, testing: `Identify testing frameworks, test organization patterns, and testing strategies used in this project. TESTING FRAMEWORK DETECTION: - JavaScript/TypeScript: Jest, Vitest, Mocha, Cypress, Playwright, Testing Library - Python: pytest, unittest, nose, tox - Java: JUnit, TestNG, Mockito, Spring Test - Go: built-in testing, Testify, Ginkgo - Rust: built-in testing, proptest, criterion - Ruby: RSpec, minitest - PHP: PHPUnit, Pest Look for: - Test files (*.test.*, *.spec.*, *_test.*, test_*.py) - Test directories (/test, /tests, /__tests__) - Configuration files (jest.config.js, vitest.config.ts, pytest.ini) - CI/CD test configurations - Check .github/workflows/*.yml for test commands - Mock/stub patterns - E2E testing setup CRITICAL: Your response MUST start directly with the structured format below. NO preambles like "Based on my analysis..." or "Here's what I found..." - start IMMEDIATELY with "TESTING FRAMEWORKS:". TESTING FRAMEWORKS: [framework 1, framework 2, ...] TEST COMMANDS: [how to run tests] TEST ORGANIZATION: [how tests are structured and organized] E2E TESTING: [end-to-end testing approach if present]`, architecture: `Analyze the project's architectural patterns, directory structure, and design decisions. ARCHITECTURE ANALYSIS AREAS: - Directory/folder structure and organization - Design patterns (MVC, MVP, MVVM, layered architecture, microservices, etc.) - Code organization (modules, packages, namespaces) - Database integration patterns - API design patterns (REST, GraphQL, RPC) - Configuration management - Dependency injection patterns - Error handling patterns - Logging and monitoring - Security patterns - Performance considerations Examine: - Source code organization in src/, lib/, app/ directories - Configuration files and their patterns - Database schema or ORM usage - API endpoint definitions - Middleware/interceptor patterns - Shared utilities and common code CRITICAL: Your response MUST start directly with the structured format below. NO preambles like "Based on my analysis..." or "Here's what I found..." - start IMMEDIATELY with "ARCHITECTURE PATTERN:". ARCHITECTURE PATTERN: [primary architectural pattern] DIRECTORY STRUCTURE: [key organizational principles] DESIGN PATTERNS: [notable design patterns in use] DATABASE: [data layer architecture if applicable] API DESIGN: [API architectural patterns if applicable]`, deployment: `Analyze deployment strategies, infrastructure patterns, and operational considerations for this project. DEPLOYMENT ANALYSIS: - Containerization (Docker, Podman) - Container orchestration (Kubernetes, Docker Swarm, Docker Compose) - Cloud platforms (AWS, GCP, Azure, Vercel, Netlify, Railway) - CI/CD pipelines (GitHub Actions, GitLab CI, Jenkins, CircleCI) - Infrastructure as Code (Terraform, CloudFormation, Pulumi) - Serverless deployment (Lambda, Cloud Functions, Vercel Functions) - Static site deployment - Database deployment and migrations - Environment configuration management - Monitoring and logging setup Look for: - Dockerfile, docker-compose.yml - .github/workflows/, .gitlab-ci.yml, Jenkinsfile - Cloud provider configuration files - Deployment scripts - Environment variable configurations (.env patterns) - Database migration files - Package.json deploy scripts CRITICAL: Your response MUST start directly with the structured format below. NO preambles like "Based on my analysis..." or "Here's what I found..." - start IMMEDIATELY with "DEPLOYMENT STRATEGY:". DEPLOYMENT STRATEGY: [primary deployment approach] CONTAINERIZATION: [Docker/container usage] CI/CD: [continuous integration/deployment setup] HOSTING: [where the application is designed to be hosted] ENVIRONMENT MANAGEMENT: [how environments are configured]`, } /** * Execute a discovery prompt against the project using agent */ async function executeDiscoveryPrompt( projectPath: string, promptKey: keyof typeof DISCOVERY_PROMPTS, config: HoneConfig, agent?: AgentType ): Promise { const resolvedAgent = agent || config.agent const model = resolveModelForPhase(config, 'agentsMd', resolvedAgent) // Use agentsMd phase model with resolved agent const client = new AgentClient({ agent: resolvedAgent, model, workingDir: projectPath, }) logVerbose(`[AgentsMd] Executing ${promptKey} discovery prompt`) try { const response = await client.messages.create({ max_tokens: 2000, messages: [ { role: 'user', content: 'Analyze the project and provide the requested analysis.', }, ], system: DISCOVERY_PROMPTS[promptKey], }) const content = response.content[0] const result = content && content.type === 'text' ? content.text.trim() : '' logVerbose(`[AgentsMd] Completed ${promptKey} discovery: ${result.substring(0, 100)}...`) return result } catch (error) { logVerboseError( `[AgentsMd] Failed ${promptKey} discovery: ${error instanceof Error ? error.message : error}` ) return `Error analyzing ${promptKey}: ${error instanceof Error ? error.message : error}` } } /** * Execute parallel agent-based project scanning */ async function executeParallelScanning( projectPath: string, config: HoneConfig, agent?: AgentType ): Promise> { const promptKeys = Object.keys(DISCOVERY_PROMPTS) as (keyof typeof DISCOVERY_PROMPTS)[] logVerbose(`[AgentsMd] Starting parallel scanning with ${promptKeys.length} discovery prompts`) // Execute all discovery prompts in parallel to stay within 90-second limit const results = await Promise.all( promptKeys.map(async key => ({ key, result: await executeDiscoveryPrompt(projectPath, key, config, agent), })) ) // Convert results array to object const scanResults = Object.fromEntries(results.map(({ key, result }) => [key, result])) as Record< keyof typeof DISCOVERY_PROMPTS, string > logVerbose(`[AgentsMd] Parallel scanning completed with results for: ${promptKeys.join(', ')}`) return scanResults } function listMarkdownFiles(projectPath: string): string[] { const files: string[] = [] try { const entries = readdirSync(projectPath, { withFileTypes: true }) for (const entry of entries) { if (entry.isFile() && entry.name.endsWith('.md')) { if (entry.name === 'AGENTS.md') continue files.push(join(projectPath, entry.name)) } } } catch (error) { logVerbose('[AgentsMd] Could not read top-level markdown files') } const docsPath = join(projectPath, 'docs') if (existsSync(docsPath)) { files.push(...listFilesRecursive(docsPath, filePath => filePath.endsWith('.md'))) } return files.sort() } function listFilesRecursive(root: string, matcher: (filePath: string) => boolean): string[] { const files: string[] = [] try { const entries = readdirSync(root, { withFileTypes: true }) for (const entry of entries) { const entryPath = join(root, entry.name) if (entry.isDirectory()) { files.push(...listFilesRecursive(entryPath, matcher)) } else if (entry.isFile() && matcher(entryPath)) { files.push(entryPath) } } } catch (error) { logVerbose('[AgentsMd] Could not read directory during file discovery') } return files.sort() } function generateFeedbackContent(testingContent: string, buildContent: string): string { const feedbackLines: string[] = [] const testCommands = extractLabeledValue(testingContent, 'TEST COMMANDS') if (testCommands) { feedbackLines.push(`TEST COMMANDS: ${testCommands}`) } else if (isUnavailableAgentResult(testingContent)) { feedbackLines.push('TEST COMMANDS: Not available.') } const buildCommands = extractLabeledValue(buildContent, 'BUILD COMMANDS') if (buildCommands) { feedbackLines.push(`BUILD COMMANDS: ${buildCommands}`) } else if (isUnavailableAgentResult(buildContent)) { feedbackLines.push('BUILD COMMANDS: Not available.') } const lintCommands = extractLabeledValue(buildContent, 'LINT COMMANDS') if (lintCommands) { feedbackLines.push(`LINT COMMANDS: ${lintCommands}`) } else if (isUnavailableAgentResult(buildContent)) { feedbackLines.push('LINT COMMANDS: Not available.') } const formatCommands = extractLabeledValue(buildContent, 'FORMAT COMMANDS') if (formatCommands) { feedbackLines.push(`FORMAT COMMANDS: ${formatCommands}`) } else if (isUnavailableAgentResult(buildContent)) { feedbackLines.push('FORMAT COMMANDS: Not available.') } if (feedbackLines.length === 0) { return 'Information not available.' } return feedbackLines.join('\n') } /** * Create adaptive template sections based on discovered tech stack */ function createTemplateSections( scanResults: Record, analysis: ProjectAnalysis ): TemplateSection[] { const sections: TemplateSection[] = [] // Build content using agent discovery results with static analysis fallback const getContentWithFallback = (agentResult: string | undefined, fallbackData: string[]) => { if ( agentResult && !agentResult.includes('failed to analyze') && !agentResult.includes('Error:') && !isUnavailableAgentResult(agentResult) ) { return agentResult } return fallbackData.length > 0 ? `Static analysis detected: ${fallbackData.join(', ')}` : 'Not available.' } // High priority sections (always include) sections.push({ title: 'Project Overview', content: getContentWithFallback(scanResults.languages || '', analysis.languages), priority: 1, detailFile: 'languages.md', }) const buildContent = getContentWithFallback(scanResults.buildSystems || '', analysis.buildSystems) sections.push({ title: 'Build System', content: buildContent, priority: 2, detailFile: 'build.md', }) // Medium priority sections (include if they have significant content) const testingContent = getContentWithFallback( scanResults.testing || '', analysis.testingFrameworks ) if (testingContent && !testingContent.includes('Not available')) { sections.push({ title: 'Testing Framework', content: testingContent, priority: 3, detailFile: 'testing.md', }) } const architectureContent = getContentWithFallback( scanResults.architecture || '', analysis.architecture ) if (architectureContent && !architectureContent.includes('Not available')) { sections.push({ title: 'Architecture', content: architectureContent, priority: 4, detailFile: 'architecture.md', }) } // Lower priority sections (include if space allows) const deploymentContent = getContentWithFallback( scanResults.deployment || '', analysis.deployment ) if (deploymentContent && !deploymentContent.toLowerCase().includes('not available')) { sections.push({ title: 'Deployment', content: deploymentContent, priority: 5, detailFile: 'deployment.md', }) } // Add feedback section inline at the top with test/build commands sections.push({ title: 'Feedback Instructions', content: generateFeedbackContent(testingContent, buildContent), priority: 0, // Highest priority to appear at top detailFile: undefined, // Force inline, never create separate file }) // Sort by priority return sections.sort((a, b) => a.priority - b.priority) } /** * Count lines in text content */ function countLines(text: string): number { return text.split('\n').length } /** * Generate compact AGENTS.md content that fits within 100-line limit */ function generateCompactContent( sections: TemplateSection[], useAgentsDir: boolean, agentsDocsDir: string = AGENTS_DOCS_DIR ): string { // Normalize directory to ensure trailing slash for proper markdown link construction const normalizedDir = agentsDocsDir.endsWith('/') ? agentsDocsDir : agentsDocsDir + '/' const header = `# AGENTS.md Learnings and patterns for future agents working on this project. ` if (!useAgentsDir) { // Full content in main file const fullSections = sections .map( section => `## ${section.title} ${section.content} ` ) .join('\n') const content = header + '\n' + fullSections + ` --- *This AGENTS.md was generated using agent-based project discovery.* ` return `${GENERATED_BLOCK_START}\n${content.trimEnd()}\n${GENERATED_BLOCK_END}\n` } // Compact version with references to agents docs directory files, but keep inline sections inline const compactSections = sections .map(section => { // If section has no detailFile, render it inline (like Feedback Instructions) if (!section.detailFile) { return `## ${section.title} ${section.content} ` } // Otherwise, use compact format with reference to detail file return `## ${section.title} ${getFirstSentence(section.content)} See [@${normalizedDir}${section.detailFile}](${normalizedDir}${section.detailFile}) for detailed information. ` }) .join('\n') const content = header + '\n' + compactSections + ` --- *This AGENTS.md was generated using agent-based project discovery.* *Detailed information is available in the ${normalizedDir} directory.* ` return `${GENERATED_BLOCK_START}\n${content.trimEnd()}\n${GENERATED_BLOCK_END}\n` } /** * Extract concise, informative summary from agent-generated content * Skips unhelpful preambles like "Based on my analysis..." */ function getFirstSentence(content: string): string { if (!content) return 'Information not available.' // Skip common unhelpful agent preambles - comprehensive list of patterns const skipPatterns = [ /^Based on (?:my |the )?(?:comprehensive |detailed |thorough )?(?:analysis|exploration|examination|review|investigation).*?[:,]\s*/gi, /^(?:Here's|Here is).*?(?:analysis|overview|summary|breakdown).*?[:,]\s*/gi, /^I(?:'ve|'ll| have| will).*?(?:analyze|explore|examine|review).*?[:,]\s*/gi, /^(?:Looking|Examining|Reviewing|Analyzing) (?:at )?(?:the |this )?(?:project|codebase|code).*?[:,]\s*/gi, /^After (?:analyzing|examining|reviewing|exploring).*?[:,]\s*/gi, /^Let me (?:analyze|explore|examine|review).*?[:,]\s*/gi, /^Upon (?:analysis|examination|review|exploration).*?[:,]\s*/gi, /^(?:The |This )?(?:analysis|exploration|examination) (?:shows|reveals|indicates).*?[:,]\s*/gi, ] let cleanContent = content.trim() // Remove matching preamble patterns (including following whitespace/newlines) for (const pattern of skipPatterns) { cleanContent = cleanContent.replace(pattern, '').trim() } // Look for structured information markers (uppercase patterns) const lines = cleanContent.split('\n').filter(line => line.trim()) // Try to find lines with structured info like "**KEY**: value" or "KEY: value" for (const line of lines) { const trimmed = line.trim() // Match **UPPERCASE**: value or UPPERCASE: value patterns if (trimmed.match(/^\*\*[A-Z][A-Z\s_-]+\*\*\s*:|^[A-Z][A-Z\s_-]+:/)) { if (trimmed.length <= 150) { return trimmed } // If too long, extract just the key and first part of value const colonIndex = trimmed.indexOf(':') if (colonIndex > 0) { const key = trimmed.substring(0, colonIndex + 1) const value = trimmed.substring(colonIndex + 1).trim() const shortValue = value.split(/[,;]/)[0]?.trim() || value.substring(0, 80) return `${key} ${shortValue}` } } } // Try to get the first meaningful line that isn't a preamble for (const line of lines) { const trimmed = line.trim() // Skip lines that look like preambles if ( trimmed.toLowerCase().startsWith('based on') || trimmed.toLowerCase().startsWith("here's") || trimmed.toLowerCase().startsWith('here is') || trimmed.toLowerCase().startsWith('i ') || trimmed.toLowerCase().startsWith("i'") || trimmed.toLowerCase().startsWith('the analysis') || trimmed.toLowerCase().startsWith('looking at') || trimmed.toLowerCase().startsWith('after ') ) { continue } if (trimmed.length > 0 && trimmed.length <= 150) { return trimmed } } // If first line is acceptable, use it const firstLine = lines[0]?.trim() ?? '' if (firstLine.length > 0 && firstLine.length <= 150) { return firstLine } // If first line is too long, try to get first sentence const sentences = cleanContent.split(/[.!?]+/) if (sentences.length > 0 && sentences[0]?.trim().length && sentences[0].trim().length <= 150) { return sentences[0].trim() + '.' } // Fallback: truncate to reasonable length return cleanContent.substring(0, 150).trim() + '...' } /** * Generate AGENTS.md content based on agent-based discovery */ async function generateContent( projectPath: string, analysis: ProjectAnalysis, config: HoneConfig, agent?: AgentType, agentsDocsDir: string = AGENTS_DOCS_DIR ): Promise<{ mainContent: string detailSections?: TemplateSection[] useAgentsDir: boolean sectionTitles: string[] }> { log('\nPhase 2: Agent Discovery') log('-'.repeat(80)) process.stdout.write('Executing agent-based project discovery... ') try { // Execute parallel agent scanning for comprehensive project analysis const scanResults = await executeParallelScanning(projectPath, config, agent) process.stdout.write('✓\n') logVerbose( `[AgentsMd] Discovery completed with ${Object.keys(scanResults).length} analysis areas` ) log('\nPhase 3: Content Generation') log('-'.repeat(80)) // Create adaptive template sections based on discovered tech stack const sections = createTemplateSections(scanResults, analysis) // Generate initial content to check line count const fullContent = generateCompactContent(sections, false, agentsDocsDir) const lineCount = countLines(fullContent) logVerbose(`[AgentsMd] Generated content has ${lineCount} lines (limit: 100)`) // Decide whether to use agents docs subdirectory based on content length and complexity const useAgentsDir = lineCount > 100 || sections.length > 5 if (useAgentsDir) { if (lineCount > 100) { log( `Content exceeds 100-line limit. Creating ${agentsDocsDir} subdirectory for detailed information.` ) } else { log( `Project has complex structure. Creating ${agentsDocsDir} subdirectory for better organization.` ) } } logVerbose(`[AgentsMd] Using ${agentsDocsDir} directory: ${useAgentsDir}`) const mainContent = generateCompactContent(sections, useAgentsDir, agentsDocsDir) return { mainContent, detailSections: useAgentsDir ? sections : undefined, useAgentsDir, sectionTitles: sections.map(section => section.title), } } catch (error) { process.stdout.write('✗\n') throw error } } /** * Main function to generate AGENTS.md documentation */ export async function generateAgentsMd( options: AgentsMdGeneratorOptions = {} ): Promise { const projectPath = options.projectPath || process.cwd() try { log('Phase 1: Project Analysis') log('-'.repeat(80)) process.stdout.write('Loading configuration... ') const config = await loadConfigWithoutCreation() process.stdout.write('✓\n') // Resolve the agents documentation directory from config const agentsDocsDir = getAgentsDocsDir(config) process.stdout.write('Analyzing project structure... ') const analysis = await analyzeProject(projectPath, config) process.stdout.write('✓\n') logVerbose( `[AgentsMd] Found ${analysis.languages.length} languages, ${analysis.buildSystems.length} build systems, ${analysis.testingFrameworks.length} testing frameworks` ) // Check if AGENTS.md already exists and handle accordingly const agentsPath = join(projectPath, 'AGENTS.md') const existingAgentsDirPath = join(projectPath, agentsDocsDir) if (existsSync(agentsPath)) { if (!options.overwrite) { log('\n• AGENTS.md already exists. Use --overwrite to replace it.') return { success: false, filesCreated: [], error: new Error('AGENTS.md already exists'), } } else { log('• AGENTS.md exists. Overwriting with new content.') } } // Also check for existing agents docs directory and inform user if (existsSync(existingAgentsDirPath)) { if (!options.overwrite) { logVerbose( `[AgentsMd] ${agentsDocsDir} directory already exists. Detail files will be skipped unless --overwrite is used.` ) } else { logVerbose( `[AgentsMd] ${agentsDocsDir} directory exists. Detail files will be overwritten.` ) } } const { mainContent, detailSections, useAgentsDir, sectionTitles } = await generateContent( projectPath, analysis, config, options.agent, agentsDocsDir ) log('\nPhase 4: File Generation') log('-'.repeat(80)) process.stdout.write('Writing AGENTS.md file... ') // Preserve existing non-generated content if overwriting let finalContent = mainContent if (options.overwrite && existsSync(agentsPath)) { try { const existingContent = await readFile(agentsPath, 'utf-8') const mergedContent = mergeGeneratedContent(existingContent, mainContent) if (mergedContent) { finalContent = mergedContent logVerbose('[AgentsMd] Preserved existing content outside generated block') } else { const preservedContent = extractPreservableContent(existingContent, sectionTitles) if (preservedContent) { finalContent = `${mainContent}\n\n\n${preservedContent}` logVerbose( '[AgentsMd] Preserved existing non-generated sections from previous AGENTS.md' ) } } } catch (error) { logVerboseError(`[AgentsMd] Could not preserve existing content: ${error}`) } } await writeFile(agentsPath, finalContent, 'utf-8') process.stdout.write('✓\n') const filesCreated = [agentsPath] // Create agents docs directory and detail files if needed let agentsDirPath: string | undefined let detailFilesCreated = 0 if (useAgentsDir && detailSections) { agentsDirPath = join(projectPath, agentsDocsDir) // Handle existing agents docs directory if (existsSync(agentsDirPath)) { if (!options.overwrite) { log( `• ${agentsDocsDir} directory already exists. Use --overwrite to replace existing detail files.` ) } else { log(`• ${agentsDocsDir} directory exists. Overwriting existing detail files.`) } } else { await mkdir(agentsDirPath, { recursive: true }) logVerbose(`[AgentsMd] Created ${agentsDocsDir} directory for detailed information`) } process.stdout.write(`Creating ${detailSections.length} detail files... `) // Write detail files for (const section of detailSections) { if (section.detailFile) { const detailPath = join(agentsDirPath, section.detailFile) // Check if detail file already exists if (existsSync(detailPath) && !options.overwrite) { logVerbose(`[AgentsMd] Skipping existing detail file: ${section.detailFile}`) continue } try { const detailContent = `# ${section.title} ${section.content} --- *This file is part of the AGENTS.md documentation system.* ` await writeFile(detailPath, detailContent, 'utf-8') filesCreated.push(detailPath) detailFilesCreated++ logVerbose( `[AgentsMd] ${existsSync(detailPath) && options.overwrite ? 'Updated' : 'Created'} detail file: ${section.detailFile}` ) } catch (fileError) { logVerboseError( `[AgentsMd] Failed to write detail file ${section.detailFile}: ${fileError instanceof Error ? fileError.message : fileError}` ) // Continue with other files rather than failing completely } } } process.stdout.write('✓\n') } // Success message with details log('') if (useAgentsDir && detailSections) { log(`✓ Generated AGENTS.md with ${detailSections.length} sections`) log(`✓ Created ${detailFilesCreated} detail files in ${agentsDocsDir}`) } else { log( `✓ Generated AGENTS.md with ${analysis.languages.length + analysis.buildSystems.length + analysis.testingFrameworks.length} detected components` ) } // Next steps guidance log('') log('Next steps:') log(' 1. Review the generated AGENTS.md for accuracy') if (useAgentsDir) { log(` 2. Check detailed information in the ${agentsDocsDir} directory`) } log(' 3. Edit and customize the documentation as needed') log(' 4. Commit the changes to your repository') return { success: true, mainFilePath: agentsPath, agentsDirPath, filesCreated, } } catch (error) { const err = error instanceof Error ? error : new Error(String(error)) logError('\n✗ Failed to generate AGENTS.md') logError(`Error: ${err.message}`) return { success: false, filesCreated: [], error: err, } } }