UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

257 lines (256 loc) 12.6 kB
/** * AgentInstructionsGenerator — 通用 AI Agent 指令文件生成器 * * Channel F: 为多种 AI 编码工具生成项目指令文件 * - AGENTS.md → OpenAI Codex / 通用 Agent * - CLAUDE.md → Claude Code * - .github/copilot-instructions.md → GitHub Copilot(动态版,替代静态模板) * * 设计原则: * 1. 内容来源统一 — 从 _loadEntries() 已加载的知识条目中提取 * 2. 互补不重复 — .mdc 处理行为规则,Channel F 处理项目知识 * 3. 轻量索引 — 只输出摘要和规则,详细内容引导至 MCP 工具 * 4. 幂等生成 — 每次 deliver 重写全部文件,不做增量 diff */ import fs from 'node:fs'; import path from 'node:path'; import { TEMPLATES_DIR } from '../../shared/package-root.js'; import { mergeSection } from './FileProtection.js'; import { estimateTokens } from './TokenBudget.js'; /** * Agent 指令文件 token 预算 */ const AGENT_BUDGET = Object.freeze({ MAX_RULES: 15, MAX_PATTERNS: 10, MAX_SKILLS: 10, MAX_TOTAL_TOKENS: 3000, }); /** MCP 工具清单 — 精简版(跟随实际 MCP handler 注册名称) */ const MCP_TOOLS_SUMMARY = [ { name: 'autosnippet_task', desc: 'Task & decision management: prime (CALL FIRST every message) / create/claim/close/fail/defer/progress/decompose / record_decision/revise_decision', }, { name: 'autosnippet_search', desc: 'Search knowledge base (mode: auto/context/keyword/semantic)', }, { name: 'autosnippet_knowledge', desc: 'Knowledge CRUD (operation: list/get/insights/confirm_usage)', }, { name: 'autosnippet_submit_knowledge', desc: 'Submit a knowledge candidate (strict validation)', }, { name: 'autosnippet_guard', desc: 'Code compliance check (single file or batch audit)' }, { name: 'autosnippet_structure', desc: 'Project structure discovery (targets/files/metadata)' }, { name: 'autosnippet_graph', desc: 'Knowledge graph query (query/impact/path/stats)' }, { name: 'autosnippet_skill', desc: 'Skill management (list/load/create/update/delete)' }, { name: 'autosnippet_bootstrap', desc: 'Project cold-start & scan' }, { name: 'autosnippet_rescan', desc: 'Incremental rescan: preserves Recipes, cleans caches, re-analyzes project, runs relevance audit', }, { name: 'autosnippet_evolve', desc: 'Batch Recipe evolution decisions (propose_evolution/confirm_deprecation/skip), used per-dimension during rescan or standalone', }, { name: 'autosnippet_panorama', desc: 'Project panorama (operation: overview/module/gaps/health)', }, { name: 'autosnippet_health', desc: 'Service health & KB statistics' }, { name: 'autosnippet_capabilities', desc: 'List all available MCP tools (self-discovery)' }, ]; export class AgentInstructionsGenerator { logger; projectName; projectRoot; constructor(projectRoot, projectName = 'Project', logger = console) { this.projectRoot = projectRoot; this.projectName = projectName; this.logger = logger; } /** * 生成所有 Agent 指令文件 * * @param params.rules kind='rule' 的条目(已排序) * @param params.patterns kind='pattern' 的条目(已排序) * @param params.skills 可用 Skill 名称列表 * @returns } */ generate({ rules = [], patterns = [], skills = [], } = {}) { const startTime = Date.now(); // 构建共享内容块 const sections = this._buildSections({ rules, patterns, skills }); // AGENTS.md 与 CLAUDE.md 互斥(双向): // 有 CLAUDE.md → 走 CLAUDE.md 路线(跳过 AGENTS.md) // 否则 → 走 AGENTS.md 路线(跳过 CLAUDE.md) const claudePath = path.join(this.projectRoot, 'CLAUDE.md'); const agentsPath = path.join(this.projectRoot, 'AGENTS.md'); const useClaudeMode = fs.existsSync(claudePath); const agents = useClaudeMode ? { filePath: agentsPath, tokensUsed: 0, skipped: true } : this._writeAgentsMd(sections); const claude = useClaudeMode ? this._writeClaudeMd(sections) : { filePath: claudePath, tokensUsed: 0, skipped: true }; const copilot = this._writeCopilotInstructions(sections); const duration = Date.now() - startTime; const allResults = [agents, claude, copilot]; const filesWritten = allResults.filter((r) => !r.skipped).length; const skippedFiles = allResults.filter((r) => r.skipped); if (skippedFiles.length > 0) { this.logger.info?.(`[AgentInstructions] Skipped ${skippedFiles.length} file(s): ` + skippedFiles.map((f) => f.filePath).join(', ')); } this.logger.info?.(`[AgentInstructions] Generated ${filesWritten} files in ${duration}ms — ` + `AGENTS.md: ${agents.tokensUsed}t, CLAUDE.md: ${claude.tokensUsed}t, ` + `copilot-instructions: ${copilot.tokensUsed}t`); return { agents, claude, copilot, stats: { filesWritten, filesSkipped: skippedFiles.length, totalTokens: agents.tokensUsed + claude.tokensUsed + copilot.tokensUsed, duration, }, }; } // ─── 内容构建 ────────────────────────────────────── /** * 从知识条目构建共享内容段 */ _buildSections({ rules, patterns, skills, }) { // 编码规则(Channel A 格式,一行一条) const ruleLines = rules .slice(0, AGENT_BUDGET.MAX_RULES) .filter((e) => e.doClause) .map((e) => { const langPrefix = e.language && e.scope !== 'universal' ? `[${e.language}] ` : ''; const doText = e.doClause.replace(/\.+$/, ''); let line = `${langPrefix}${doText}`; if (e.dontClause) { // 有明确否定词的统一为 "Do NOT",否则保留原文(如 "Avoid ...") const hasNegPrefix = /^(Don't|Do not|Never)\s+/i.test(e.dontClause); if (hasNegPrefix) { const stripped = e.dontClause .replace(/^(Don't|Do not|Never)\s+/i, '') .replace(/\.+$/, ''); line += `. Do NOT ${stripped}`; } else { line += `. ${e.dontClause.replace(/\.+$/, '')}`; } } return `- ${line}.`; }); // 架构模式(摘要表格行) const patternRows = patterns .slice(0, AGENT_BUDGET.MAX_PATTERNS) .filter((e) => e.trigger && e.doClause) .map((e) => { const trigger = e.trigger.startsWith('@') ? e.trigger : `@${e.trigger}`; const when = (e.whenClause || '').substring(0, 60); const doText = (e.doClause || '').substring(0, 80); return `| ${trigger} | ${when} | ${doText} |`; }); // Skills 列表 const skillLines = skills.slice(0, AGENT_BUDGET.MAX_SKILLS).map((s) => `- \`${s}\``); // MCP 工具列表 const toolLines = MCP_TOOLS_SUMMARY.map((t) => `- \`${t.name}\` — ${t.desc}`); return { ruleLines, patternRows, skillLines, toolLines }; } // ─── AGENTS.md ───────────────────────────────────── _writeAgentsMd(sections) { // 文件头部(仅用于新建/旧版重写场景) const header = [ `# ${this.projectName} — Agent Instructions`, '', '> Auto-generated by [AutoSnippet](https://github.com/GxFn/AutoSnippet). Do not edit manually.', '', 'This project uses **AutoSnippet** for knowledge management.', 'Access the knowledge base through MCP tools.', '', ].join('\n'); // 动态区段内容(始终在 markers 内管理) const sectionLines = []; if (sections.ruleLines.length > 0) { sectionLines.push('## Coding Standards', '', ...sections.ruleLines, ''); } if (sections.patternRows.length > 0) { sectionLines.push('## Architecture Patterns', '', '| Trigger | When | Do |', '|---------|------|----|', ...sections.patternRows, ''); } sectionLines.push('## MCP Tools', '', ...sections.toolLines, ''); if (sections.skillLines.length > 0) { sectionLines.push('## Skills', '', 'Load with `autosnippet_skill({ operation: "load", name: "<skill>" })`:', '', ...sections.skillLines, ''); } sectionLines.push('## Constraints', '', '1. Do NOT modify knowledge base files directly (`AutoSnippet/recipes/`, `.autosnippet/`).', '2. Prefer Recipes as project standards; source code is supplementary.', '3. Create or update knowledge only through MCP tools.', ''); const sectionContent = sectionLines.join('\n'); const filePath = path.join(this.projectRoot, 'AGENTS.md'); const result = mergeSection(filePath, sectionContent, { header, logger: this.logger }); return { filePath, tokensUsed: estimateTokens(sectionContent), skipped: !result.written }; } // ─── CLAUDE.md ───────────────────────────────────── _writeClaudeMd(sections) { const header = [ `# ${this.projectName} — Claude Code Instructions`, '', '> Auto-generated by AutoSnippet. Regenerated when knowledge base changes.', '', 'This project uses **AutoSnippet** for knowledge management.', 'Access the knowledge base through MCP tools.', '', ].join('\n'); const sectionLines = []; if (sections.ruleLines.length > 0) { sectionLines.push('## Coding Standards', '', ...sections.ruleLines, ''); } if (sections.patternRows.length > 0) { sectionLines.push('## Key Patterns', '', '| Trigger | When | Do |', '|---------|------|----|', ...sections.patternRows, ''); } sectionLines.push('## MCP Tools', '', ...sections.toolLines, ''); if (sections.skillLines.length > 0) { sectionLines.push('## Skills', '', ...sections.skillLines, ''); } sectionLines.push('', '## Constraints', '', '1. Do NOT modify knowledge base files directly (`AutoSnippet/recipes/`, `.autosnippet/`).', '2. Prefer Recipes as project standards; source code is supplementary.', '3. Create or update knowledge only through MCP tools.', ''); const sectionContent = sectionLines.join('\n'); const filePath = path.join(this.projectRoot, 'CLAUDE.md'); const result = mergeSection(filePath, sectionContent, { header, logger: this.logger }); return { filePath, tokensUsed: estimateTokens(sectionContent), skipped: !result.written }; } // ─── copilot-instructions.md ─────────────────────── /** * 动态生成 copilot-instructions.md * 读取 templates/instructions/conventions.md + HTML markers */ _writeCopilotInstructions(_sections) { const body = this._loadConventionsTemplate(); const section = [ '<!-- autosnippet:begin -->', '', '# AutoSnippet Conventions', '', body, '', '<!-- autosnippet:end -->', ].join('\n'); const filePath = path.join(this.projectRoot, '.github', 'copilot-instructions.md'); const result = mergeSection(filePath, section, { logger: this.logger }); return { filePath, tokensUsed: estimateTokens(section), skipped: !result.written }; } // ─── 模板读取 ────────────────────────────────────── /** * 读取 templates/instructions/conventions.md — 唯一内容源 * .mdc、copilot-instructions.md、Channel F 动态版全部从这里读取 */ _loadConventionsTemplate() { const tplPath = path.join(TEMPLATES_DIR, 'instructions/conventions.md'); return fs.readFileSync(tplPath, 'utf8').trimEnd(); } } export default AgentInstructionsGenerator;