autosnippet 2.1.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +189 -113
  2. package/bin/api-server.js +1 -4
  3. package/bin/cli.js +1 -50
  4. package/config/constitution.yaml +33 -107
  5. package/dashboard/dist/assets/{index-DbkbX1c-.js → index-B9py3ybr.js} +32 -32
  6. package/dashboard/dist/index.html +1 -1
  7. package/lib/bootstrap.js +5 -31
  8. package/lib/cli/SetupService.js +16 -14
  9. package/lib/core/capability/CapabilityProbe.js +8 -6
  10. package/lib/core/constitution/Constitution.js +13 -4
  11. package/lib/core/constitution/ConstitutionValidator.js +106 -211
  12. package/lib/core/gateway/Gateway.js +34 -98
  13. package/lib/core/gateway/GatewayActionRegistry.js +12 -1
  14. package/lib/core/permission/PermissionManager.js +2 -2
  15. package/lib/external/mcp/McpServer.js +4 -7
  16. package/lib/external/mcp/handlers/bootstrap.js +13 -1
  17. package/lib/external/mcp/handlers/browse.js +0 -7
  18. package/lib/external/mcp/handlers/candidate.js +1 -1
  19. package/lib/external/mcp/handlers/guard.js +11 -0
  20. package/lib/external/mcp/handlers/skill.js +186 -18
  21. package/lib/external/mcp/tools.js +40 -1
  22. package/lib/http/middleware/roleResolver.js +1 -1
  23. package/lib/http/routes/auth.js +2 -2
  24. package/lib/http/routes/monitoring.js +4 -4
  25. package/lib/http/routes/search.js +0 -17
  26. package/lib/injection/ServiceContainer.js +21 -40
  27. package/lib/service/candidate/CandidateService.js +12 -1
  28. package/lib/service/chat/ChatAgent.js +139 -18
  29. package/lib/service/chat/Memory.js +104 -0
  30. package/lib/service/chat/tools.js +244 -10
  31. package/lib/service/guard/GuardCheckEngine.js +9 -1
  32. package/lib/service/recipe/RecipeService.js +8 -0
  33. package/lib/service/skills/SkillHooks.js +126 -0
  34. package/package.json +1 -1
  35. package/scripts/init-db.js +1 -2
  36. package/templates/constitution.yaml +29 -85
  37. package/lib/core/session/SessionManager.js +0 -232
  38. package/lib/infrastructure/logging/ReasoningLogger.js +0 -269
  39. package/lib/infrastructure/monitoring/RoleDriftMonitor.js +0 -259
  40. package/lib/infrastructure/quality/ComplianceEvaluator.js +0 -326
@@ -17,6 +17,7 @@ export const TOOL_GATEWAY_MAP = {
17
17
  autosnippet_enrich_candidates: { action: 'candidate:update', resource: 'candidates' },
18
18
  autosnippet_bootstrap_knowledge: { action: 'knowledge:bootstrap', resource: 'knowledge' },
19
19
  autosnippet_bootstrap_refine: { action: 'candidate:update', resource: 'candidates' },
20
+ autosnippet_create_skill: { action: 'create:skills', resource: 'skills' },
20
21
  };
21
22
 
22
23
  export const TOOLS = [
@@ -623,7 +624,45 @@ export const TOOLS = [
623
624
  required: ['skillName'],
624
625
  },
625
626
  },
626
- // 35. 内容润色:Bootstrap 候选 AI 精炼(Phase 6)
627
+ // 35. 创建项目级 Skill
628
+ {
629
+ name: 'autosnippet_create_skill',
630
+ description:
631
+ '创建一个项目级 Skill 文档,写入 .autosnippet/skills/<name>/SKILL.md。\n' +
632
+ 'Skill 是 Agent 的领域知识增强文档,帮助 Agent 正确执行特定任务。\n' +
633
+ '创建后自动更新编辑器索引(.cursor/rules/autosnippet-skills.mdc),使 Skill 被 AI Agent 被动发现。\n' +
634
+ '\n' +
635
+ '使用场景:\n' +
636
+ ' • 将反复出现的操作指南/架构决策/编码规范固化为 Skill\n' +
637
+ ' • 为特定 Target/模块创建定制化开发指南\n' +
638
+ ' • 记录项目私有的最佳实践(不适合放入通用知识库)\n' +
639
+ '\n' +
640
+ '⚠️ 注意:Skill 名称建议使用 kebab-case,如 my-auth-guide',
641
+ inputSchema: {
642
+ type: 'object',
643
+ properties: {
644
+ name: {
645
+ type: 'string',
646
+ description: 'Skill 名称(kebab-case,如 my-auth-guide)。将作为目录名。',
647
+ },
648
+ description: {
649
+ type: 'string',
650
+ description: 'Skill 一句话描述(写入 SKILL.md frontmatter)',
651
+ },
652
+ content: {
653
+ type: 'string',
654
+ description: 'Skill 正文内容(Markdown 格式,不含 frontmatter)',
655
+ },
656
+ overwrite: {
657
+ type: 'boolean',
658
+ default: false,
659
+ description: '如果同名 Skill 已存在,是否覆盖(默认 false)',
660
+ },
661
+ },
662
+ required: ['name', 'description', 'content'],
663
+ },
664
+ },
665
+ // 36. ② 内容润色:Bootstrap 候选 AI 精炼(Phase 6)
627
666
  {
628
667
  name: 'autosnippet_bootstrap_refine',
629
668
  description:
@@ -96,7 +96,7 @@ export function roleResolverMiddleware(options = {}) {
96
96
  }
97
97
  } else {
98
98
  // 无探针实例 → 本地开发默认 admin(向后兼容)
99
- req.resolvedRole = 'developer_admin';
99
+ req.resolvedRole = 'developer';
100
100
  req.resolvedUser = 'local';
101
101
  }
102
102
 
@@ -44,7 +44,7 @@ if (!process.env.ASD_AUTH_SECRET) {
44
44
  function createToken(username) {
45
45
  const payload = {
46
46
  sub: username,
47
- role: 'developer_admin',
47
+ role: 'developer',
48
48
  iat: Date.now(),
49
49
  exp: Date.now() + TOKEN_TTL,
50
50
  };
@@ -107,7 +107,7 @@ router.post('/login', asyncHandler(async (req, res) => {
107
107
  success: true,
108
108
  data: {
109
109
  token,
110
- user: { username, role: 'developer_admin' },
110
+ user: { username, role: 'developer' },
111
111
  },
112
112
  });
113
113
  }));
@@ -160,12 +160,12 @@ router.get('/cache', (req, res) => {
160
160
 
161
161
  /**
162
162
  * POST /api/v1/monitoring/cache/clear
163
- * 清空缓存(仅限 developer_admin
163
+ * 清空缓存(仅限 developer
164
164
  */
165
165
  router.post('/cache/clear', async (req, res) => {
166
166
  // 角色检查:仅 admin 可操作
167
167
  const role = req.resolvedRole || 'visitor';
168
- if (role !== 'developer_admin') {
168
+ if (role !== 'developer') {
169
169
  return res.status(403).json({
170
170
  success: false,
171
171
  error: { message: '仅管理员可清空缓存' },
@@ -285,7 +285,7 @@ router.get('/dashboard', async (req, res) => {
285
285
 
286
286
  /**
287
287
  * POST /api/v1/monitoring/reset
288
- * 重置监控统计(仅限开发环境 + developer_admin
288
+ * 重置监控统计(仅限开发环境 + developer
289
289
  */
290
290
  router.post('/reset', (req, res) => {
291
291
  if (process.env.NODE_ENV === 'production') {
@@ -297,7 +297,7 @@ router.post('/reset', (req, res) => {
297
297
 
298
298
  // 角色检查:仅 admin 可操作
299
299
  const role = req.resolvedRole || 'visitor';
300
- if (role !== 'developer_admin') {
300
+ if (role !== 'developer') {
301
301
  return res.status(403).json({
302
302
  success: false,
303
303
  error: { message: '仅管理员可重置监控统计' },
@@ -214,23 +214,6 @@ router.get('/graph/stats', asyncHandler(async (req, res) => {
214
214
  res.json({ success: true, data: stats });
215
215
  }));
216
216
 
217
- /**
218
- * GET /api/v1/search/compliance
219
- * 合规评估报告
220
- */
221
- router.get('/compliance', asyncHandler(async (req, res) => {
222
- const { period } = req.query;
223
- const container = getServiceContainer();
224
- const evaluator = container.get('complianceEvaluator');
225
-
226
- if (!evaluator) {
227
- return res.json({ success: true, data: { overallScore: 0, message: 'ComplianceEvaluator not available' } });
228
- }
229
-
230
- const report = evaluator.evaluate({ period });
231
- res.json({ success: true, data: report });
232
- }));
233
-
234
217
  /**
235
218
  * POST /api/v1/search/trigger-from-code
236
219
  * Xcode trigger 搜索模拟 (stub — 功能未完整实现)
@@ -49,10 +49,10 @@ import { AutomationOrchestrator } from '../service/automation/AutomationOrchestr
49
49
  import { ToolRegistry } from '../service/chat/ToolRegistry.js';
50
50
  import { ChatAgent } from '../service/chat/ChatAgent.js';
51
51
  import { ALL_TOOLS } from '../service/chat/tools.js';
52
+ import { SkillHooks } from '../service/skills/SkillHooks.js';
52
53
 
53
54
  // ─── P3: Infrastructure ──────────────────────────────
54
- import { EventBus } from '../infrastructure/event/EventBus.js';
55
- import { PluginManager } from '../infrastructure/plugin/PluginManager.js';
55
+ // EventBus / PluginManager imports removed — source files retained for future use
56
56
 
57
57
  /**
58
58
  * DependencyInjection 容器
@@ -88,18 +88,10 @@ export class ServiceContainer {
88
88
  if (bootstrapComponents.gateway) {
89
89
  this.singletons.gateway = bootstrapComponents.gateway;
90
90
  }
91
- if (bootstrapComponents.reasoningLogger) {
92
- this.singletons.reasoningLogger = bootstrapComponents.reasoningLogger;
93
- }
94
- if (bootstrapComponents.roleDriftMonitor) {
95
- this.singletons.roleDriftMonitor = bootstrapComponents.roleDriftMonitor;
96
- }
97
- if (bootstrapComponents.complianceEvaluator) {
98
- this.singletons.complianceEvaluator = bootstrapComponents.complianceEvaluator;
99
- }
100
- if (bootstrapComponents.sessionManager) {
101
- this.singletons.sessionManager = bootstrapComponents.sessionManager;
91
+ if (bootstrapComponents.constitution) {
92
+ this.singletons.constitution = bootstrapComponents.constitution;
102
93
  }
94
+
103
95
  if (bootstrapComponents.projectRoot) {
104
96
  this.singletons._projectRoot = bootstrapComponents.projectRoot;
105
97
  }
@@ -209,17 +201,7 @@ export class ServiceContainer {
209
201
  return this.singletons.gateway;
210
202
  });
211
203
 
212
- // ReasoningLogger
213
- this.register('reasoningLogger', () => this.singletons.reasoningLogger || null);
214
204
 
215
- // RoleDriftMonitor
216
- this.register('roleDriftMonitor', () => this.singletons.roleDriftMonitor || null);
217
-
218
- // ComplianceEvaluator
219
- this.register('complianceEvaluator', () => this.singletons.complianceEvaluator || null);
220
-
221
- // SessionManager
222
- this.register('sessionManager', () => this.singletons.sessionManager || null);
223
205
  }
224
206
 
225
207
  /**
@@ -257,11 +239,12 @@ export class ServiceContainer {
257
239
  const gateway = this.get('gateway');
258
240
  const projectRoot = this.singletons._projectRoot || process.cwd();
259
241
  const candidateFileWriter = new CandidateFileWriter(projectRoot);
242
+ const skillHooks = this.get('skillHooks');
260
243
  this.singletons.candidateService = new CandidateService(
261
244
  candidateRepository,
262
245
  auditLogger,
263
246
  gateway,
264
- { fileWriter: candidateFileWriter }
247
+ { fileWriter: candidateFileWriter, skillHooks }
265
248
  );
266
249
  }
267
250
  return this.singletons.candidateService;
@@ -276,12 +259,13 @@ export class ServiceContainer {
276
259
  const knowledgeGraphService = this.get('knowledgeGraphService');
277
260
  const projectRoot = this.singletons._projectRoot || process.cwd();
278
261
  const fileWriter = new RecipeFileWriter(projectRoot);
262
+ const skillHooks = this.get('skillHooks');
279
263
  this.singletons.recipeService = new RecipeService(
280
264
  recipeRepository,
281
265
  auditLogger,
282
266
  gateway,
283
267
  knowledgeGraphService,
284
- { fileWriter }
268
+ { fileWriter, skillHooks }
285
269
  );
286
270
  }
287
271
  return this.singletons.recipeService;
@@ -332,23 +316,12 @@ export class ServiceContainer {
332
316
  return this.singletons.guardCheckEngine;
333
317
  });
334
318
 
335
- // ─── 新迁移的服务 ────────────────────────────────────
319
+ // ─── Constitution ────────────────────────────────────
320
+ this.register('constitution', () => this.singletons.constitution || null);
336
321
 
337
- // EventBus
338
- this.register('eventBus', () => {
339
- if (!this.singletons.eventBus) {
340
- this.singletons.eventBus = new EventBus();
341
- }
342
- return this.singletons.eventBus;
343
- });
322
+ // ─── 新迁移的服务 ────────────────────────────────────
344
323
 
345
- // PluginManager
346
- this.register('pluginManager', () => {
347
- if (!this.singletons.pluginManager) {
348
- this.singletons.pluginManager = new PluginManager();
349
- }
350
- return this.singletons.pluginManager;
351
- });
324
+ // EventBus / PluginManager — 已移除注册(源文件保留,未来可恢复)
352
325
 
353
326
  // RetrievalFunnel (Advanced Search)
354
327
  this.register('retrievalFunnel', () => {
@@ -518,6 +491,14 @@ export class ServiceContainer {
518
491
  }
519
492
  return this.singletons.chatAgent;
520
493
  });
494
+
495
+ // SkillHooks (Skill 生命周期钩子 — 加载 skills/*/hooks.js)
496
+ this.register('skillHooks', () => {
497
+ if (!this.singletons.skillHooks) {
498
+ this.singletons.skillHooks = new SkillHooks();
499
+ }
500
+ return this.singletons.skillHooks;
501
+ });
521
502
  }
522
503
 
523
504
  /**
@@ -11,11 +11,12 @@ import { v4 as uuidv4 } from 'uuid';
11
11
  * 包括创建、批准、驳回和应用到 Recipe 的业务逻辑
12
12
  */
13
13
  export class CandidateService {
14
- constructor(candidateRepository, auditLogger, gateway, { fileWriter } = {}) {
14
+ constructor(candidateRepository, auditLogger, gateway, { fileWriter, skillHooks } = {}) {
15
15
  this.candidateRepository = candidateRepository;
16
16
  this.auditLogger = auditLogger;
17
17
  this.gateway = gateway;
18
18
  this.fileWriter = fileWriter || null;
19
+ this.skillHooks = skillHooks || null;
19
20
  this.logger = Logger.getInstance();
20
21
  }
21
22
 
@@ -52,6 +53,16 @@ export class CandidateService {
52
53
  throw new ValidationError('Invalid candidate data');
53
54
  }
54
55
 
56
+ // ── SkillHooks: onCandidateSubmit ──
57
+ if (this.skillHooks) {
58
+ const hookResult = await this.skillHooks.run('onCandidateSubmit', candidate, {
59
+ userId: context.userId,
60
+ });
61
+ if (hookResult?.block) {
62
+ throw new ValidationError(`SkillHook blocked: ${hookResult.reason || 'unknown'}`);
63
+ }
64
+ }
65
+
55
66
  // 保存到数据库
56
67
  const created = await this.candidateRepository.create(candidate);
57
68
 
@@ -27,9 +27,12 @@ import path from 'node:path';
27
27
  import { fileURLToPath } from 'node:url';
28
28
  import Logger from '../../infrastructure/logging/Logger.js';
29
29
  import { TaskPipeline } from './TaskPipeline.js';
30
+ import { Memory } from './Memory.js';
30
31
 
31
32
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
- const SKILLS_DIR = path.resolve(__dirname, '../../../skills');
33
+ const PROJECT_ROOT = path.resolve(__dirname, '../../..');
34
+ const SKILLS_DIR = path.resolve(PROJECT_ROOT, 'skills');
35
+ const SOUL_PATH = path.resolve(PROJECT_ROOT, 'SOUL.md');
33
36
  const MAX_ITERATIONS = 6;
34
37
 
35
38
  export class ChatAgent {
@@ -39,6 +42,10 @@ export class ChatAgent {
39
42
  #logger;
40
43
  /** @type {Map<string, TaskPipeline>} */
41
44
  #pipelines = new Map();
45
+ /** @type {string} 缓存的项目概况(每次 execute 刷新一次) */
46
+ #projectBriefingCache = '';
47
+ /** @type {Memory|null} 跨对话轻量记忆 */
48
+ #memory = null;
42
49
 
43
50
  /**
44
51
  * @param {object} opts
@@ -55,6 +62,12 @@ export class ChatAgent {
55
62
  /** 是否有 AI Provider(只读) */
56
63
  this.hasAI = !!aiProvider;
57
64
 
65
+ // 初始化跨对话记忆
66
+ try {
67
+ const projectRoot = container?.singletons?._projectRoot || process.cwd();
68
+ this.#memory = new Memory(projectRoot);
69
+ } catch { /* Memory init failed, degrade silently */ }
70
+
58
71
  // 注册内置 DAG 管线
59
72
  this.#registerBuiltinPipelines();
60
73
  }
@@ -71,6 +84,9 @@ export class ChatAgent {
71
84
  * @returns {Promise<{reply: string, toolCalls: Array, hasContext: boolean}>}
72
85
  */
73
86
  async execute(prompt, { history = [] } = {}) {
87
+ // 每次对话刷新项目概况(不是每轮 ReAct)
88
+ this.#projectBriefingCache = await this.#buildProjectBriefing();
89
+
74
90
  const toolSchemas = this.#toolRegistry.getToolSchemas();
75
91
  const systemPrompt = this.#buildSystemPrompt(toolSchemas);
76
92
 
@@ -97,11 +113,9 @@ export class ChatAgent {
97
113
 
98
114
  if (!action) {
99
115
  // 没有 Action → 最终回答
100
- return {
101
- reply: this.#cleanFinalAnswer(response),
102
- toolCalls,
103
- hasContext: toolCalls.length > 0,
104
- };
116
+ const reply = this.#cleanFinalAnswer(response);
117
+ this.#extractMemory(prompt, reply);
118
+ return { reply, toolCalls, hasContext: toolCalls.length > 0 };
105
119
  }
106
120
 
107
121
  // 执行工具
@@ -141,8 +155,11 @@ export class ChatAgent {
141
155
  systemPrompt: '直接回答用户问题,不要再调用工具。',
142
156
  });
143
157
 
158
+ const finalReply = this.#cleanFinalAnswer(finalResponse);
159
+ this.#extractMemory(prompt, finalReply);
160
+
144
161
  return {
145
- reply: this.#cleanFinalAnswer(finalResponse),
162
+ reply: finalReply,
146
163
  toolCalls,
147
164
  hasContext: toolCalls.length > 0,
148
165
  };
@@ -574,9 +591,18 @@ ${code.substring(0, 3000)}
574
591
  ? `\n## 可用 Skills\n通过 load_skill 工具按需加载领域知识文档,获取操作指南和最佳实践参考。\n\n| Skill | 说明 |\n|---|---|\n${skillList.map(s => `| ${s.name} | ${s.summary || '-'} |`).join('\n')}\n\n**场景 → Skill 推荐**:\n- 冷启动、初始化 → autosnippet-coldstart\n- 深度项目分析 → autosnippet-analysis\n- 候选生成 → autosnippet-candidates + autosnippet-create\n- 代码规范审计 → autosnippet-guard\n- Snippet 概念解释 → autosnippet-concepts\n- 生命周期管理 → autosnippet-lifecycle\n- Swift/ObjC/JS·TS 语言参考 → autosnippet-reference-{swift,objc,jsts}\n- 项目结构分析 → autosnippet-structure\n- 不确定该用哪个 → autosnippet-intent\n`
575
592
  : '';
576
593
 
577
- return `你是 AutoSnippet 项目的统一 AI 中心。项目内所有 AI 推理和分析都通过你执行。
578
- 你拥有 ${toolSchemas.length} 个工具覆盖知识库管理全链路:搜索、提交、审核、质量评估、Guard 检查、知识图谱、冷启动等。
594
+ // SOUL AI 人格注入(如果 SOUL.md 存在)
595
+ let soulSection = '';
596
+ try {
597
+ if (fs.existsSync(SOUL_PATH)) {
598
+ soulSection = '\n' + fs.readFileSync(SOUL_PATH, 'utf-8').trim() + '\n';
599
+ }
600
+ } catch { /* SOUL.md not available */ }
579
601
 
602
+ return `${soulSection}
603
+ 你是 AutoSnippet 项目的统一 AI 中心。项目内所有 AI 推理和分析都通过你执行。
604
+ 你拥有 ${toolSchemas.length} 个工具覆盖知识库管理全链路:搜索、提交、审核、质量评估、Guard 检查、知识图谱、冷启动等。
605
+ ${this.#projectBriefingCache}${this.#memory?.toPromptSection() || ''}
580
606
  可用工具:
581
607
 
582
608
  ${toolDescriptions}
@@ -599,7 +625,9 @@ ${skillSection}
599
625
  - 候选创建/提交 → load_skill("autosnippet-candidates")
600
626
  - 代码规范/Guard → load_skill("autosnippet-guard")
601
627
  - 不确定做什么 → load_skill("autosnippet-intent")
602
- 8. 你可以组合多个工具完成复杂任务(如:查重 → 提交 → 质量评分 → 知识图谱关联)。`;
628
+ 8. 你可以组合多个工具完成复杂任务(如:查重 → 提交 → 质量评分 → 知识图谱关联)。
629
+ 9. 当工具返回 _meta.confidence = "none" 时,告知用户无匹配并建议下一步,不要凭空编造。当 _meta.confidence = "low" 时,明确标注结果不确定性。
630
+ 10. 优先使用组合工具(analyze_code, knowledge_overview, submit_with_check)减少调用轮次。`;
603
631
  }
604
632
 
605
633
  /**
@@ -660,24 +688,39 @@ ${skillSection}
660
688
 
661
689
  /**
662
690
  * 列出可用的 Skills 及其摘要(用于系统提示词)
691
+ * 加载顺序: 内置 skills/ → 项目级 .autosnippet/skills/(同名覆盖)
663
692
  * @returns {{ name: string, summary: string }[]}
664
693
  */
665
694
  #listAvailableSkills() {
695
+ const skillMap = new Map();
696
+
697
+ // 1. 内置 Skills
698
+ this.#loadSkillsFromDir(SKILLS_DIR, skillMap);
699
+
700
+ // 2. 项目级 Skills(覆盖同名内置 Skill)
701
+ const projectSkillsDir = path.resolve(PROJECT_ROOT, '.autosnippet', 'skills');
702
+ this.#loadSkillsFromDir(projectSkillsDir, skillMap);
703
+
704
+ return Array.from(skillMap.values());
705
+ }
706
+
707
+ /**
708
+ * 从目录加载 Skills 到 Map
709
+ */
710
+ #loadSkillsFromDir(dir, skillMap) {
666
711
  try {
667
- const dirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
712
+ const dirs = fs.readdirSync(dir, { withFileTypes: true })
668
713
  .filter(d => d.isDirectory())
669
714
  .map(d => d.name);
670
- return dirs.map(name => {
671
- const skillPath = path.join(SKILLS_DIR, name, 'SKILL.md');
715
+ for (const name of dirs) {
716
+ const skillPath = path.join(dir, name, 'SKILL.md');
672
717
  let summary = '';
673
718
  try {
674
719
  const raw = fs.readFileSync(skillPath, 'utf-8');
675
- // 从 frontmatter description 或首行 # 后提取摘要
676
720
  const fmMatch = raw.match(/^---[\s\S]*?description:\s*["']?(.+?)["']?\s*$/m);
677
721
  if (fmMatch) {
678
722
  summary = fmMatch[1];
679
723
  } else {
680
- // fallback: 取首个非空非标题行
681
724
  const lines = raw.split('\n');
682
725
  for (const line of lines) {
683
726
  const trimmed = line.trim();
@@ -688,10 +731,88 @@ ${skillSection}
688
731
  }
689
732
  }
690
733
  } catch { /* SKILL.md not found */ }
691
- return { name, summary };
692
- });
734
+ skillMap.set(name, { name, summary });
735
+ }
736
+ } catch { /* directory not found */ }
737
+ }
738
+
739
+ /**
740
+ * 构建项目概况注入到系统提示词(每次 execute 刷新一次)
741
+ * 单次 SQL 聚合 < 5ms,静默降级
742
+ */
743
+ async #buildProjectBriefing() {
744
+ try {
745
+ const db = this.#container?.get('database');
746
+ if (!db) return '';
747
+ // knowledge_type → kind 映射:
748
+ // rule: code-standard, code-style, best-practice, boundary-constraint
749
+ // pattern: code-pattern, architecture, solution
750
+ // fact: code-relation, inheritance, call-chain, data-flow, module-dependency
751
+ const stats = db.prepare(`
752
+ SELECT
753
+ (SELECT COUNT(*) FROM recipes) as recipeCount,
754
+ (SELECT COUNT(*) FROM recipes WHERE knowledge_type IN ('code-standard','code-style','best-practice','boundary-constraint')) as ruleCount,
755
+ (SELECT COUNT(*) FROM recipes WHERE knowledge_type IN ('code-pattern','architecture','solution')) as patternCount,
756
+ (SELECT COUNT(*) FROM recipes WHERE knowledge_type IN ('code-relation','inheritance','call-chain','data-flow','module-dependency')) as factCount,
757
+ (SELECT COUNT(*) FROM recipes WHERE knowledge_type = 'boundary-constraint') as guardRuleCount,
758
+ (SELECT COUNT(*) FROM candidates WHERE status='pending') as pendingCandidates,
759
+ (SELECT COUNT(*) FROM candidates) as totalCandidates
760
+ `).get();
761
+ if (!stats || stats.recipeCount === 0) {
762
+ return '\n## 项目状态\n⚠️ 知识库为空。建议先执行冷启动(bootstrap_knowledge)。\n';
763
+ }
764
+ let section = `\n## 项目状态\n- 知识库: ${stats.recipeCount} 条 Recipe(${stats.ruleCount || 0} rule / ${stats.patternCount || 0} pattern / ${stats.factCount || 0} fact)\n- Guard 规则: ${stats.guardRuleCount || 0} 条\n- 候选: ${stats.pendingCandidates} 条待审 / ${stats.totalCandidates} 条总计\n`;
765
+ if (stats.pendingCandidates > 10) {
766
+ section += `\n⚠️ 有 ${stats.pendingCandidates} 条候选积压,建议执行批量审核。\n`;
767
+ }
768
+ return section;
693
769
  } catch {
694
- return [];
770
+ return ''; // DB 不可用时静默降级
771
+ }
772
+ }
773
+
774
+ /**
775
+ * 从用户消息中提取偏好/决策写入 Memory
776
+ * 使用正则匹配,不调 AI — 零延迟
777
+ */
778
+ #extractMemory(prompt, _reply) {
779
+ if (!this.#memory) return;
780
+ try {
781
+ const prefPatterns = [
782
+ /我们(项目|团队)?(不用|不使用|禁止|避免|偏好|习惯|规范是)/,
783
+ /以后(都|请|要)/,
784
+ /记住/,
785
+ ];
786
+ if (prefPatterns.some(p => p.test(prompt))) {
787
+ this.#memory.append({
788
+ type: 'preference',
789
+ content: prompt.substring(0, 200),
790
+ ttl: 30,
791
+ });
792
+ }
793
+ } catch { /* memory write failure is non-critical */ }
794
+ }
795
+
796
+ /**
797
+ * 事件驱动入口(P2 预留接口)
798
+ * @param {{ type: string, payload: object, source?: string }} event
799
+ */
800
+ async executeEvent(event) {
801
+ const { type, payload } = event;
802
+ const prompt = this.#eventToPrompt(type, payload);
803
+ return this.execute(prompt, { history: [] });
804
+ }
805
+
806
+ #eventToPrompt(type, payload) {
807
+ switch (type) {
808
+ case 'file_saved':
809
+ return `文件 ${payload.filePath} 刚被保存,变更了 ${payload.changedLines} 行。请分析是否有值得提取为 Recipe 的代码模式。如果有,说明原因;没有就说"无需操作"。`;
810
+ case 'candidate_backlog':
811
+ return `当前有 ${payload.count} 条候选积压(最早 ${payload.oldest})。请按质量分类:哪些值得审核、哪些可以直接拒绝、哪些需要补充信息。`;
812
+ case 'scheduled_health':
813
+ return `请执行知识库健康检查:Recipe 覆盖率、过时标记、Guard 规则有效性。给出简要报告。`;
814
+ default:
815
+ return `事件: ${type}\n${JSON.stringify(payload)}`;
695
816
  }
696
817
  }
697
818
 
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Memory — 跨对话轻量记忆
3
+ *
4
+ * 设计:
5
+ * - JSONL 文件存储,每行一条记忆
6
+ * - 支持 TTL 自动过期
7
+ * - 上限 maxEntries,超出时截断旧条目
8
+ * - 读写均做静默降级(Memory 是增强,不是核心路径)
9
+ *
10
+ * 记忆类型:
11
+ * - preference: 用户偏好("我们不用 singleton"、"以后用 DI")
12
+ * - decision: 关键决策("Network 模块审核通过")
13
+ * - context: 项目上下文("主语言是 Swift,使用 SPM")
14
+ *
15
+ * 文件路径: .autosnippet/memory.jsonl
16
+ */
17
+
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+
21
+ export class Memory {
22
+ #filePath;
23
+ #maxEntries;
24
+
25
+ /**
26
+ * @param {string} projectRoot — 用户项目根目录
27
+ * @param {object} [opts]
28
+ * @param {number} [opts.maxEntries=50] — 最大记忆条数
29
+ */
30
+ constructor(projectRoot, { maxEntries = 50 } = {}) {
31
+ this.#filePath = path.join(projectRoot, '.autosnippet', 'memory.jsonl');
32
+ this.#maxEntries = maxEntries;
33
+ }
34
+
35
+ /**
36
+ * 读取最近 N 条记忆,过滤过期项
37
+ * @param {number} [limit=20]
38
+ * @returns {{ ts: string, type: string, content: string, ttl?: number }[]}
39
+ */
40
+ load(limit = 20) {
41
+ try {
42
+ if (!fs.existsSync(this.#filePath)) return [];
43
+ const raw = fs.readFileSync(this.#filePath, 'utf-8').trim();
44
+ if (!raw) return [];
45
+ const lines = raw.split('\n').filter(Boolean);
46
+ const now = Date.now();
47
+ return lines
48
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
49
+ .filter(Boolean)
50
+ .filter(m => !m.ttl || (now - new Date(m.ts).getTime()) < m.ttl * 86400000)
51
+ .slice(-limit);
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 追加一条记忆
59
+ * @param {{ type: string, content: string, ttl?: number }} entry
60
+ */
61
+ append(entry) {
62
+ try {
63
+ const dir = path.dirname(this.#filePath);
64
+ fs.mkdirSync(dir, { recursive: true });
65
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...entry });
66
+ fs.appendFileSync(this.#filePath, line + '\n', 'utf-8');
67
+ this.#compact();
68
+ } catch { /* write failure non-critical */ }
69
+ }
70
+
71
+ /**
72
+ * 生成供系统提示词的记忆摘要
73
+ * @returns {string}
74
+ */
75
+ toPromptSection() {
76
+ const memories = this.load();
77
+ if (memories.length === 0) return '';
78
+ const lines = memories.map(m => `- [${m.type}] ${m.content}`).join('\n');
79
+ return `\n## 历史记忆\n以下是之前对话中积累的项目偏好和决策,请参考:\n${lines}\n`;
80
+ }
81
+
82
+ /**
83
+ * 当前记忆条数
84
+ */
85
+ get size() {
86
+ return this.load(this.#maxEntries).length;
87
+ }
88
+
89
+ /**
90
+ * 超过 maxEntries 时截断旧条目
91
+ */
92
+ #compact() {
93
+ try {
94
+ const raw = fs.readFileSync(this.#filePath, 'utf-8').trim();
95
+ if (!raw) return;
96
+ const lines = raw.split('\n').filter(Boolean);
97
+ if (lines.length > this.#maxEntries) {
98
+ fs.writeFileSync(this.#filePath, lines.slice(-this.#maxEntries).join('\n') + '\n', 'utf-8');
99
+ }
100
+ } catch { /* ignore */ }
101
+ }
102
+ }
103
+
104
+ export default Memory;