autosnippet 2.8.3 → 2.10.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 (110) hide show
  1. package/README.md +5 -5
  2. package/bin/cli.js +5 -33
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
  5. package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
  6. package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/lib/cli/AiScanService.js +13 -11
  9. package/lib/cli/KnowledgeSyncService.js +343 -0
  10. package/lib/cli/SetupService.js +9 -27
  11. package/lib/core/ast/ProjectGraph.js +160 -0
  12. package/lib/core/gateway/GatewayActionRegistry.js +48 -58
  13. package/lib/domain/index.js +16 -11
  14. package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
  15. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  16. package/lib/domain/knowledge/Lifecycle.js +109 -0
  17. package/lib/domain/knowledge/index.js +27 -0
  18. package/lib/domain/knowledge/values/Constraints.js +125 -0
  19. package/lib/domain/knowledge/values/Content.js +86 -0
  20. package/lib/domain/knowledge/values/Quality.js +93 -0
  21. package/lib/domain/knowledge/values/Reasoning.js +69 -0
  22. package/lib/domain/knowledge/values/Relations.js +168 -0
  23. package/lib/domain/knowledge/values/Stats.js +87 -0
  24. package/lib/domain/knowledge/values/index.js +9 -0
  25. package/lib/external/ai/AiProvider.js +48 -0
  26. package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
  27. package/lib/external/mcp/McpServer.js +7 -5
  28. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
  29. package/lib/external/mcp/handlers/bootstrap.js +121 -12
  30. package/lib/external/mcp/handlers/browse.js +77 -73
  31. package/lib/external/mcp/handlers/candidate.js +29 -276
  32. package/lib/external/mcp/handlers/guard.js +2 -0
  33. package/lib/external/mcp/handlers/knowledge.js +205 -0
  34. package/lib/external/mcp/handlers/skill.js +4 -2
  35. package/lib/external/mcp/handlers/structure.js +25 -23
  36. package/lib/external/mcp/handlers/system.js +10 -12
  37. package/lib/external/mcp/tools.js +125 -138
  38. package/lib/http/HttpServer.js +4 -8
  39. package/lib/http/middleware/requestLogger.js +3 -3
  40. package/lib/http/routes/ai.js +17 -1
  41. package/lib/http/routes/extract.js +48 -4
  42. package/lib/http/routes/knowledge.js +246 -0
  43. package/lib/http/routes/search.js +12 -17
  44. package/lib/http/routes/skills.js +44 -1
  45. package/lib/infrastructure/cache/GraphCache.js +143 -0
  46. package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
  47. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  48. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  49. package/lib/infrastructure/realtime/RealtimeService.js +14 -2
  50. package/lib/injection/ServiceContainer.js +164 -63
  51. package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
  52. package/lib/repository/token/TokenUsageStore.js +162 -0
  53. package/lib/service/automation/DirectiveDetector.js +2 -3
  54. package/lib/service/automation/FileWatcher.js +67 -28
  55. package/lib/service/automation/XcodeIntegration.js +931 -156
  56. package/lib/service/automation/handlers/AlinkHandler.js +6 -4
  57. package/lib/service/automation/handlers/CreateHandler.js +53 -18
  58. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  59. package/lib/service/automation/handlers/SearchHandler.js +35 -17
  60. package/lib/service/chat/AnalystAgent.js +25 -14
  61. package/lib/service/chat/CandidateGuardrail.js +1 -1
  62. package/lib/service/chat/ChatAgent.js +280 -48
  63. package/lib/service/chat/ContextWindow.js +92 -8
  64. package/lib/service/chat/HandoffProtocol.js +26 -1
  65. package/lib/service/chat/ProducerAgent.js +11 -9
  66. package/lib/service/chat/tools.js +298 -194
  67. package/lib/service/guard/GuardCheckEngine.js +114 -10
  68. package/lib/service/guard/GuardService.js +59 -48
  69. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  70. package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
  71. package/lib/service/knowledge/KnowledgeService.js +725 -0
  72. package/lib/service/search/SearchEngine.js +92 -19
  73. package/lib/service/skills/SignalCollector.js +15 -9
  74. package/lib/service/skills/SkillAdvisor.js +13 -11
  75. package/lib/service/snippet/SnippetFactory.js +5 -5
  76. package/lib/service/spm/SpmService.js +119 -18
  77. package/package.json +1 -1
  78. package/scripts/install-cursor-skill.js +0 -6
  79. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  80. package/skills/autosnippet-analysis/SKILL.md +15 -7
  81. package/skills/autosnippet-candidates/SKILL.md +6 -6
  82. package/skills/autosnippet-coldstart/SKILL.md +7 -3
  83. package/skills/autosnippet-concepts/SKILL.md +7 -6
  84. package/skills/autosnippet-create/SKILL.md +13 -13
  85. package/skills/autosnippet-intent/SKILL.md +3 -2
  86. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  87. package/skills/autosnippet-recipes/SKILL.md +16 -4
  88. package/templates/constitution.yaml +1 -1
  89. package/templates/copilot-instructions.md +6 -6
  90. package/templates/recipes-setup/README.md +3 -3
  91. package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
  92. package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
  93. package/lib/cli/CandidateSyncService.js +0 -261
  94. package/lib/cli/SyncService.js +0 -356
  95. package/lib/domain/candidate/Candidate.js +0 -196
  96. package/lib/domain/candidate/CandidateRepository.js +0 -107
  97. package/lib/domain/candidate/Reasoning.js +0 -52
  98. package/lib/domain/recipe/Recipe.js +0 -421
  99. package/lib/domain/recipe/RecipeRepository.js +0 -54
  100. package/lib/domain/types/CandidateStatus.js +0 -52
  101. package/lib/http/routes/candidates.js +0 -559
  102. package/lib/http/routes/recipes.js +0 -397
  103. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  104. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  105. package/lib/service/candidate/CandidateAggregator.js +0 -52
  106. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  107. package/lib/service/candidate/CandidateService.js +0 -973
  108. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  109. package/lib/service/recipe/RecipeService.js +0 -786
  110. package/lib/service/recipe/RecipeStatsTracker.js +0 -148
@@ -218,7 +218,7 @@ export class ContextWindow {
218
218
  for (const m of removed) {
219
219
  if (m.role === 'assistant' && m.toolCalls) {
220
220
  for (const tc of m.toolCalls) {
221
- if (tc.name === 'submit_candidate' || tc.name === 'submit_with_check') {
221
+ if (tc.name === 'submit_knowledge' || tc.name === 'submit_with_check') {
222
222
  this.#compactedSubmits.add(tc.args?.title || tc.args?.category || 'untitled');
223
223
  }
224
224
  }
@@ -339,7 +339,7 @@ export class ContextWindow {
339
339
  const m = this.#messages[i];
340
340
  if (m.role === 'assistant' && m.toolCalls) {
341
341
  for (const tc of m.toolCalls) {
342
- if (tc.name === 'submit_candidate' || tc.name === 'submit_with_check') {
342
+ if (tc.name === 'submit_knowledge' || tc.name === 'submit_with_check') {
343
343
  this.#compactedSubmits.add(tc.args?.title || tc.args?.category || 'untitled');
344
344
  }
345
345
  }
@@ -393,19 +393,33 @@ export class ContextWindow {
393
393
  export function limitToolResult(toolName, result, quota) {
394
394
  const { maxChars = 4000, maxMatches = 10 } = quota;
395
395
 
396
- // submit_candidate / submit_with_check 结果很短,不截断
397
- if (toolName === 'submit_candidate' || toolName === 'submit_with_check') {
396
+ // submit_knowledge / submit_with_check 结果很短,不截断
397
+ if (toolName === 'submit_knowledge' || toolName === 'submit_with_check') {
398
398
  const raw = typeof result === 'string' ? result : JSON.stringify(result);
399
399
  return raw.length > 500 ? raw.substring(0, 500) : raw;
400
400
  }
401
401
 
402
- // search_project_code: 限制匹配数 + 截断上下文
402
+ // search_project_code: 限制匹配数 + 截断上下文(支持批量模式)
403
403
  if (toolName === 'search_project_code') {
404
+ if (result && typeof result === 'object' && result.batchResults) {
405
+ // 批量模式:对每个 pattern 的结果独立限制(直接操作对象,避免 stringify→parse 往返)
406
+ const limited = { ...result };
407
+ const perKeyChars = Math.floor(maxChars / Object.keys(limited.batchResults).length);
408
+ for (const [key, sub] of Object.entries(limited.batchResults)) {
409
+ limited.batchResults[key] = limitSearchResultObj(sub, Math.min(maxMatches, 3), perKeyChars);
410
+ }
411
+ const raw = JSON.stringify(limited);
412
+ return raw.length > maxChars ? raw.substring(0, maxChars) + '\n... [batch truncated]' : raw;
413
+ }
404
414
  return limitSearchResult(result, maxMatches, maxChars);
405
415
  }
406
416
 
407
- // read_project_file: 限制字符数
417
+ // read_project_file: 限制字符数(支持批量模式)
408
418
  if (toolName === 'read_project_file') {
419
+ if (result && typeof result === 'object' && result.batchResults) {
420
+ const raw = JSON.stringify(result);
421
+ return raw.length > maxChars ? raw.substring(0, maxChars) + '\n... [batch truncated]' : raw;
422
+ }
409
423
  return limitFileContent(result, maxChars);
410
424
  }
411
425
 
@@ -465,6 +479,41 @@ function limitSearchResult(result, maxMatches, maxChars) {
465
479
  return str;
466
480
  }
467
481
 
482
+ /**
483
+ * 限制搜索结果(返回对象) — 用于批量模式,避免 JSON.stringify → JSON.parse 往返
484
+ * 当源码含控制字符时,stringify→substring 截断会破坏 JSON 结构导致 parse 失败
485
+ */
486
+ function limitSearchResultObj(result, maxMatches, maxChars) {
487
+ if (!result || typeof result !== 'object') return result || {};
488
+ if (typeof result === 'string') return { _raw: result.substring(0, maxChars) };
489
+
490
+ const limited = { ...result };
491
+ if (Array.isArray(limited.matches)) {
492
+ limited.matches = limited.matches.slice(0, maxMatches).map(m => {
493
+ const copy = { ...m };
494
+ if (copy.context && typeof copy.context === 'string') {
495
+ const contextLines = copy.context.split('\n');
496
+ if (contextLines.length > 7) {
497
+ copy.context = contextLines.slice(0, 7).join('\n') + '\n... [truncated]';
498
+ }
499
+ // 按字符上限截断 context(防止单个代码块过大)
500
+ if (copy.context.length > 500) {
501
+ copy.context = copy.context.substring(0, 500) + '\n... [truncated]';
502
+ }
503
+ }
504
+ if (Array.isArray(copy.lines) && copy.lines.length > 5) {
505
+ copy.lines = copy.lines.slice(0, 5);
506
+ copy._truncated = true;
507
+ }
508
+ return copy;
509
+ });
510
+ if (result.matches.length > maxMatches) {
511
+ limited._note = `Showing ${maxMatches} of ${result.matches.length} matches`;
512
+ }
513
+ }
514
+ return limited;
515
+ }
516
+
468
517
  /**
469
518
  * 限制文件内容 — 截断 content 字段
470
519
  */
@@ -526,6 +575,9 @@ export class PhaseRouter {
526
575
  /** @type {Object} 日志器 */
527
576
  #logger;
528
577
 
578
+ /** @type {boolean} 是否因 maxIterations 强制进入 SUMMARIZE */
579
+ #forcedSummarize = false;
580
+
529
581
  /**
530
582
  * @param {Object} budget — 预算配置
531
583
  * @param {boolean} isSkillOnly — 是否为 skill-only 维度
@@ -561,6 +613,19 @@ export class PhaseRouter {
561
613
  return this.#totalSubmits;
562
614
  }
563
615
 
616
+ /**
617
+ * 是否因 maxIterations 触顶而强制进入 SUMMARIZE(一次性标记)
618
+ * 调用后自动复位
619
+ * @returns {boolean}
620
+ */
621
+ consumeForcedSummarize() {
622
+ if (this.#forcedSummarize) {
623
+ this.#forcedSummarize = false;
624
+ return true;
625
+ }
626
+ return false;
627
+ }
628
+
564
629
  /**
565
630
  * 获取当前阶段的 toolChoice
566
631
  * @returns {'required'|'auto'|'none'}
@@ -585,8 +650,21 @@ export class PhaseRouter {
585
650
  * @returns {boolean}
586
651
  */
587
652
  shouldExit() {
588
- if (this.#totalIterations >= this.#budget.maxIterations) return true;
653
+ // 已在 SUMMARIZE 阶段 给 2 轮输出总结后退出
589
654
  if (this.#phase === 'SUMMARIZE' && this.#phaseRounds >= 2) return true;
655
+
656
+ // 达到 maxIterations → 不硬退出,而是强制转入 SUMMARIZE 让 AI 在完整上下文中收尾
657
+ if (this.#totalIterations >= this.#budget.maxIterations && this.#phase !== 'SUMMARIZE') {
658
+ this.#logger.info(`[PhaseRouter] maxIterations reached (${this.#totalIterations}/${this.#budget.maxIterations}), forcing → SUMMARIZE for graceful exit`);
659
+ this.#forcedSummarize = true;
660
+ this.#transitionTo('SUMMARIZE');
661
+ // 返回 false 让主循环继续运行 SUMMARIZE 阶段的 2 轮收尾
662
+ return false;
663
+ }
664
+
665
+ // SUMMARIZE 阶段超限兜底(maxIterations + 2 轮 grace)
666
+ if (this.#totalIterations >= this.#budget.maxIterations + 2) return true;
667
+
590
668
  return false;
591
669
  }
592
670
 
@@ -688,6 +766,12 @@ export class PhaseRouter {
688
766
  * @returns {string|null}
689
767
  */
690
768
  getPhaseHint() {
769
+ // 接近 maxIterations 上限时,无论处于哪个阶段都警告 AI
770
+ const remaining = this.#budget.maxIterations - this.#totalIterations;
771
+ if (remaining <= 2 && remaining > 0 && this.#phase !== 'SUMMARIZE') {
772
+ return `⚠️ 仅剩 ${remaining} 轮次即达上限,请尽快完成当前工作并准备输出总结。`;
773
+ }
774
+
691
775
  switch (this.#phase) {
692
776
  case 'EXPLORE':
693
777
  if (this.#phaseRounds >= this.#budget.searchBudget - 2) {
@@ -699,7 +783,7 @@ export class PhaseRouter {
699
783
  if (this.#totalSubmits === 0 && this.#phaseRounds >= 1) {
700
784
  return this.#isSkillOnly
701
785
  ? '你已收集足够信息,请在回复中直接输出 dimensionDigest JSON。'
702
- : '⚠️ 探索阶段已结束。你已收集了足够的项目信息,请 **立即** 调用 submit_candidate 提交候选。不要继续搜索,直接提交。';
786
+ : '⚠️ 探索阶段已结束。你已收集了足够的项目信息,请 **立即** 调用 submit_knowledge 提交候选。不要继续搜索,直接提交。';
703
787
  }
704
788
  if (this.#totalSubmits >= this.#budget.softSubmitLimit && this.#budget.softSubmitLimit > 0) {
705
789
  const remaining = this.#budget.maxSubmits - this.#totalSubmits;
@@ -13,6 +13,31 @@
13
13
  // AnalysisReport 构建
14
14
  // ──────────────────────────────────────────────────────────────────
15
15
 
16
+ /**
17
+ * 清理 Analyst 分析文本中可能泄漏的系统 nudge / graceful exit 指令。
18
+ * 这些内容如果传给 Producer,会干扰其正常工作流。
19
+ */
20
+ function sanitizeAnalysisText(text) {
21
+ if (!text) return '';
22
+ // 移除 graceful exit nudge 及 digest 模板指令
23
+ const patterns = [
24
+ /\*{0,2}⚠️?\s*(?:你已使用|轮次即将耗尽|仅剩|请立即停止|必须立即结束)[^\n]*\n?/gi,
25
+ /\*{0,2}请立即停止所有工具调用[^\n]*\*{0,2}\n?/gi,
26
+ /请在回复中直接输出\s*dimensionDigest\s*JSON[^\n]*\n?/gi,
27
+ /> ?(?:remainingTasks|如果所有信号都已覆盖)[^\n]*\n?/gi,
28
+ /> ?⚠️ 严禁输出任何非 JSON 内容[^\n]*\n?/gi,
29
+ // 移除 AI 回显的 dimensionDigest JSON 块(对 Producer 无价值且会干扰)
30
+ /```json\s*\n\s*\{\s*"dimensionDigest"\s*:[\s\S]*?\n```/g,
31
+ ];
32
+ let cleaned = text;
33
+ for (const pat of patterns) {
34
+ cleaned = cleaned.replace(pat, '');
35
+ }
36
+ // 移除可能残留的空行堆积
37
+ cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim();
38
+ return cleaned;
39
+ }
40
+
16
41
  /**
17
42
  * 从 Analyst 的执行结果构建 AnalysisReport
18
43
  *
@@ -73,7 +98,7 @@ export function buildAnalysisReport(analystResult, dimensionId, projectGraph = n
73
98
  }
74
99
 
75
100
  // 从分析文本中提取文件路径
76
- const text = analystResult.reply || '';
101
+ const text = sanitizeAnalysisText(analystResult.reply || '');
77
102
  const textFileRefs = text.match(/[\w/.-]+\.[mhswift]+/g);
78
103
  if (textFileRefs) {
79
104
  for (const f of textFileRefs) {
@@ -2,7 +2,7 @@
2
2
  * ProducerAgent.js — v3.0 生产者 Agent
3
3
  *
4
4
  * 职责:
5
- * - 将 Analyst 的分析文本转换为结构化的 submit_candidate 调用
5
+ * - 将 Analyst 的分析文本转换为结构化的 submit_knowledge 调用
6
6
  * - 遵循 PROJECT_SNAPSHOT_STYLE_GUIDE 格式
7
7
  * - 使用 read_project_file 获取代码片段
8
8
  * - CandidateGuardrail 验证每次提交
@@ -21,7 +21,7 @@ import Logger from '../../infrastructure/logging/Logger.js';
21
21
 
22
22
  const PRODUCER_SYSTEM_PROMPT = `你是知识管理专家。你会收到一段代码分析文本,需要将其中的知识点转化为结构化的知识候选。
23
23
 
24
- 核心原则: 分析文本已经包含了所有发现,你的唯一工作是将它们格式化为 submit_candidate 调用。
24
+ 核心原则: 分析文本已经包含了所有发现,你的唯一工作是将它们格式化为 submit_knowledge 调用。
25
25
 
26
26
  每个候选必须:
27
27
  1. 有清晰的标题 (描述知识点的核心,使用项目真实类名)
@@ -31,12 +31,14 @@ const PRODUCER_SYSTEM_PROMPT = `你是知识管理专家。你会收到一段代
31
31
 
32
32
  工作流程:
33
33
  1. 阅读分析文本,识别每个独立的知识点/发现
34
- 2. 对每个知识点,用 read_project_file 获取关键代码片段(读取 30-80 行,不要只读 5 行头部)
35
- 3. 立刻调用 submit_candidate 提交
34
+ 2. read_project_file 批量获取关键代码片段:
35
+ read_project_file({ filePaths: ["FileA.m", "FileB.m"], maxLines: 80 })
36
+ 3. 立刻调用 submit_knowledge 提交
36
37
  4. 重复直到分析中的所有知识点都已提交
37
38
 
38
39
  关键规则:
39
40
  - 分析中的每个要点/段落都应转化为至少一个候选
41
+ - read_project_file 支持 filePaths 数组批量读取多个文件,一次调用完成
40
42
  - read_project_file 时读取足够多的行数(startLine + maxLines 至少 30 行)
41
43
  - reasoning.sources 必须是非空数组,填写相关文件路径如 ["FileName.m"]
42
44
  - 如果分析提到了 3 个模式,就应该提交 3 个候选,不要合并
@@ -53,7 +55,7 @@ const PRODUCER_SYSTEM_PROMPT = `你是知识管理专家。你会收到一段代
53
55
  // ──────────────────────────────────────────────────────────────────
54
56
 
55
57
  const PRODUCER_TOOLS = [
56
- 'submit_candidate',
58
+ 'submit_knowledge',
57
59
  'submit_with_check',
58
60
  'read_project_file',
59
61
  ];
@@ -63,7 +65,7 @@ const PRODUCER_TOOLS = [
63
65
  // ──────────────────────────────────────────────────────────────────
64
66
 
65
67
  const PRODUCER_BUDGET = {
66
- maxIterations: 15,
68
+ maxIterations: 24, // was 18 — 与 Analyst 统一
67
69
  searchBudget: 4,
68
70
  searchBudgetGrace: 3,
69
71
  maxSubmits: 10,
@@ -77,7 +79,7 @@ const PRODUCER_BUDGET = {
77
79
 
78
80
  const STYLE_GUIDE = `# 「项目特写」写作要求
79
81
 
80
- submit_candidate 的 code 字段必须是「项目特写」。
82
+ submit_knowledge 的 code 字段必须是「项目特写」。
81
83
 
82
84
  ## 什么是「项目特写」
83
85
  将一种技术的**基本用法**与**本项目的具体特征**融合为一体。
@@ -205,7 +207,7 @@ export class ProducerAgent {
205
207
 
206
208
  // 统计提交 (区分成功/失败)
207
209
  const submitCalls = (result.toolCalls || []).filter(
208
- tc => (tc.tool || tc.name) === 'submit_candidate' || (tc.tool || tc.name) === 'submit_with_check'
210
+ tc => (tc.tool || tc.name) === 'submit_knowledge' || (tc.tool || tc.name) === 'submit_with_check'
209
211
  );
210
212
  const successCount = submitCalls.filter(tc => {
211
213
  const res = tc.result;
@@ -236,7 +238,7 @@ export class ProducerAgent {
236
238
  });
237
239
 
238
240
  const retrySubmits = (retryResult.toolCalls || []).filter(
239
- tc => (tc.tool || tc.name) === 'submit_candidate' || (tc.tool || tc.name) === 'submit_with_check'
241
+ tc => (tc.tool || tc.name) === 'submit_knowledge' || (tc.tool || tc.name) === 'submit_with_check'
240
242
  );
241
243
  const retrySuccess = retrySubmits.filter(tc => {
242
244
  const res = tc.result;