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
@@ -36,7 +36,7 @@ const PROJECT_ROOT = path.resolve(__dirname, '../../..');
36
36
  const SKILLS_DIR = path.resolve(PROJECT_ROOT, 'skills');
37
37
  const SOUL_PATH = path.resolve(PROJECT_ROOT, 'SOUL.md');
38
38
  const MAX_ITERATIONS = 6;
39
- /** 系统调用 (如 bootstrap) 允许更多迭代,因为每维度需要多次 submit_candidate */
39
+ /** 系统调用 (如 bootstrap) 允许更多迭代,因为每维度需要多次 submit_knowledge */
40
40
  const MAX_ITERATIONS_SYSTEM = 30;
41
41
  /** 原生函数调用模式下,已提交 ≥ MIN_SUBMITS_FOR_EARLY_EXIT 个候选后,连续 N 轮无新提交则提前退出 */
42
42
  const MIN_SUBMITS_FOR_EARLY_EXIT = 1;
@@ -69,23 +69,23 @@ const SYSTEM_CONTINUATION_PROMPT = `你的分析计划很好。但你需要 **
69
69
  请现在开始执行:
70
70
  1. 用 \`search_project_code\` 搜索项目代码获取真实示例
71
71
  2. 用 \`read_project_file\` 查看完整文件内容
72
- 3. 对每个值得保留的信号,用 \`submit_candidate\` 提交候选
72
+ 3. 对每个值得保留的信号,用 \`submit_knowledge\` 提交候选
73
73
 
74
74
  ⚡ 推荐使用 batch_actions 一次提交多条候选:
75
75
  \`\`\`batch_actions
76
76
  [
77
- {"tool": "submit_candidate", "params": {"title": "[Bootstrap] xxx/子主题", "code": "# 标题 — 项目特写\\n\\n> 摘要...\\n\\n描述和代码交织...", "language": "objectivec", "category": "Service", "summary": "...", "tags": ["bootstrap"], "source": "bootstrap", "reasoning": {"whyStandard": "...", "sources": ["file1"], "confidence": 0.7}}},
78
- {"tool": "submit_candidate", "params": {"title": "...", "code": "...", ...}}
77
+ {"tool": "submit_knowledge", "params": {"title": "[Bootstrap] xxx/子主题", "code": "# 标题 — 项目特写\\n\\n> 摘要...\\n\\n描述和代码交织...", "language": "objectivec", "category": "Service", "summary": "...", "tags": ["bootstrap"], "source": "bootstrap", "reasoning": {"whyStandard": "...", "sources": ["file1"], "confidence": 0.7}}},
78
+ {"tool": "submit_knowledge", "params": {"title": "...", "code": "...", ...}}
79
79
  ]
80
80
  \`\`\`
81
81
 
82
82
  请立即开始执行,不要再输出分析文字。`;
83
83
 
84
84
  /**
85
- * 系统调用提交提示 — 当 AI 做了工具调用(search/read)、写了分析文本,但没调 submit_candidate 时注入
86
- * 引导 AI 将已有分析转化为实际的 submit_candidate 调用
85
+ * 系统调用提交提示 — 当 AI 做了工具调用(search/read)、写了分析文本,但没调 submit_knowledge 时注入
86
+ * 引导 AI 将已有分析转化为实际的 submit_knowledge 调用
87
87
  */
88
- const SYSTEM_SUBMIT_PROMPT = `你的分析很好,已经获取了足够的项目信息。但你还没有调用 \`submit_candidate\` 提交任何候选。
88
+ const SYSTEM_SUBMIT_PROMPT = `你的分析很好,已经获取了足够的项目信息。但你还没有调用 \`submit_knowledge\` 提交任何候选。
89
89
 
90
90
  **你的分析不能只停留在文字描述层面** — 必须通过工具调用将分析结果持久化。
91
91
 
@@ -93,7 +93,7 @@ const SYSTEM_SUBMIT_PROMPT = `你的分析很好,已经获取了足够的项
93
93
 
94
94
  \`\`\`batch_actions
95
95
  [
96
- {"tool": "submit_candidate", "params": {
96
+ {"tool": "submit_knowledge", "params": {
97
97
  "title": "[Bootstrap] 维度/子主题",
98
98
  "code": "# 标题 — 项目特写\\n\\n> 本项目使用 XX 模式, N 个文件采用此写法\\n\\n描述...\\n\\n\`\`\`objc\\n// 真实代码示例\\n\`\`\`\\n\\n要点说明...",
99
99
  "language": "objectivec",
@@ -103,11 +103,11 @@ const SYSTEM_SUBMIT_PROMPT = `你的分析很好,已经获取了足够的项
103
103
  "source": "bootstrap",
104
104
  "reasoning": {"whyStandard": "为什么值得保留", "sources": ["真实文件名"], "confidence": 0.7}
105
105
  }},
106
- {"tool": "submit_candidate", "params": {...}}
106
+ {"tool": "submit_knowledge", "params": {...}}
107
107
  ]
108
108
  \`\`\`
109
109
 
110
- 将你上面分析出的每个有价值的发现都转化为一条 submit_candidate 调用。code 字段写「项目特写」风格: 描述和代码交织,用项目真实类名和代码。`;
110
+ 将你上面分析出的每个有价值的发现都转化为一条 submit_knowledge 调用。code 字段写「项目特写」风格: 描述和代码交织,用项目真实类名和代码。`;
111
111
 
112
112
  export class ChatAgent {
113
113
  #toolRegistry;
@@ -253,6 +253,31 @@ export class ChatAgent {
253
253
  // 附加 token 用量统计
254
254
  result.tokenUsage = { ...this.#currentTokenUsage };
255
255
 
256
+ // 持久化 token 消耗到数据库(fire-and-forget)
257
+ try {
258
+ const tokenStore = this.#container?.get?.('tokenUsageStore');
259
+ if (tokenStore) {
260
+ const aiProvider = this.#aiProvider;
261
+ tokenStore.record({
262
+ source: source || 'unknown',
263
+ dimension: dimensionId || dimensionMeta?.id || null,
264
+ provider: aiProvider?.name || null,
265
+ model: aiProvider?.model || null,
266
+ inputTokens: this.#currentTokenUsage.input,
267
+ outputTokens: this.#currentTokenUsage.output,
268
+ durationMs: Date.now() - execStartTime,
269
+ toolCalls: result.toolCalls?.length || 0,
270
+ sessionId: conversationId || null,
271
+ });
272
+
273
+ // 通知前端 token 用量变化
274
+ try {
275
+ const realtime = this.#container?.get?.('realtimeService');
276
+ realtime?.broadcastTokenUsageUpdated?.();
277
+ } catch { /* optional */ }
278
+ }
279
+ } catch { /* token logging should never break execution */ }
280
+
256
281
  return { ...result, conversationId };
257
282
  }
258
283
 
@@ -306,12 +331,18 @@ export class ChatAgent {
306
331
  const phaseRouter = (isSystem && !disablePhaseRouter) ? new PhaseRouter(budget, isSkillOnly) : null;
307
332
 
308
333
  // ── 系统提示词 (支持外部覆盖) ──
309
- const baseSystemPrompt = systemPromptOverride || this.#buildNativeToolSystemPrompt(budget);
334
+ let baseSystemPrompt = systemPromptOverride || this.#buildNativeToolSystemPrompt(budget);
335
+ // 统一注入轮次预算,确保 AI 始终知道具体数字和节奏
336
+ if (isSystem && !baseSystemPrompt.includes('轮次预算')) {
337
+ const exploreEnd = Math.floor(budget.maxIterations * 0.6);
338
+ const verifyEnd = Math.floor(budget.maxIterations * 0.8);
339
+ baseSystemPrompt += `\n\n## 轮次预算\n- 总轮次: **${budget.maxIterations} 轮**\n- 探索阶段: 第 1-${exploreEnd} 轮(搜索和结构化查询)\n- 验证阶段: 第 ${exploreEnd + 1}-${verifyEnd} 轮(读取关键文件确认细节)\n- 总结阶段: 第 ${verifyEnd + 1}-${budget.maxIterations} 轮(**停止工具调用,输出分析文本**)\n\n到达第 ${verifyEnd} 轮时你必须开始输出总结,不要继续搜索。`;
340
+ }
310
341
 
311
342
  // Bootstrap 场景限制可用工具集 (支持外部覆盖)
312
343
  const effectiveAllowedTools = allowedTools || (isSystem ? [
313
344
  'search_project_code', 'read_project_file',
314
- 'submit_candidate', 'submit_with_check',
345
+ 'submit_knowledge', 'submit_with_check',
315
346
  'list_project_structure', 'get_file_summary', 'semantic_search_code',
316
347
  // AST 结构化分析工具
317
348
  'get_project_overview', 'get_class_hierarchy', 'get_class_info',
@@ -328,6 +359,20 @@ export class ChatAgent {
328
359
  const submittedTitles = new Set(this.#globalSubmittedTitles);
329
360
  const sharedState = {}; // P2.2: 跨工具调用共享状态(搜索计数器等)
330
361
 
362
+ // ── 进度感知收敛 (Analyst 专用: disablePhaseRouter=true) ──
363
+ // 追踪搜索/读取新信息的效率,当探索饱和时提前引导 AI 收敛
364
+ const explorationMetrics = {
365
+ uniqueFiles: new Set(), // 已读取的唯一文件
366
+ uniquePatterns: new Set(), // 已搜索的唯一 pattern
367
+ uniqueQueries: new Set(), // 已查询的唯一类名/目录(AST + list_project_structure)
368
+ totalToolCalls: 0, // 总工具调用数
369
+ newInfoRounds: 0, // 最近 N 轮中获取到新信息的轮次
370
+ staleRounds: 0, // 连续未获取新信息的轮次
371
+ convergenceNudged: false, // 是否已注入收敛 nudge
372
+ };
373
+ const MIN_EXPLORE_ITERS = 16; // 最少探索轮次(冷启动质量保障)
374
+ const STALE_THRESHOLD = 3; // 连续无新信息轮次触发收敛
375
+
331
376
  // ── 主循环 ──
332
377
  while (true) {
333
378
  // PhaseRouter tick + 退出检查
@@ -337,9 +382,53 @@ export class ChatAgent {
337
382
  this.#logger.info(`[ChatAgent] PhaseRouter exit: phase=${phaseRouter.phase}, iter=${phaseRouter.totalIterations}, submits=${phaseRouter.totalSubmits}`);
338
383
  break;
339
384
  }
340
- } else if (isSystem && iterationCount >= maxIter) {
341
- // v3: system 模式无 PhaseRouter 时用独立迭代计数器硬限制
342
- this.#logger.info(`[ChatAgent] Iteration hard cap reached: ${iterationCount}/${maxIter}`);
385
+ // PhaseRouter maxIterations 强制转入 SUMMARIZE 注入收尾 nudge
386
+ if (phaseRouter.consumeForcedSummarize()) {
387
+ const submitCount = toolCalls.filter(tc => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check').length;
388
+ ctx.appendUserNudge(
389
+ `⚠️ 轮次即将耗尽 (${phaseRouter.totalIterations}/${budget.maxIterations}),**必须立即结束**。请在回复中直接输出 dimensionDigest JSON 总结(用 \`\`\`json 包裹),不要再调用任何工具。\n` +
390
+ `\`\`\`json\n{"dimensionDigest":{"summary":"分析总结","candidateCount":${submitCount},"keyFindings":["发现"],"crossRefs":{},"gaps":["缺口"],"remainingTasks":[{"signal":"未处理信号","reason":"轮次耗尽","priority":"high","searchHints":["搜索词"]}]}}\n\`\`\`\n> remainingTasks: 列出未来得及处理的信号。已覆盖则留空 \`[]\`。`
391
+ );
392
+ this.#logger.info('[ChatAgent] 📝 injected forced SUMMARIZE nudge (maxIterations reached)');
393
+ }
394
+ } else if (isSystem && !phaseRouter && !ctx.__gracefulExitInjected && !explorationMetrics.convergenceNudged
395
+ && iterationCount >= MIN_EXPLORE_ITERS && explorationMetrics.staleRounds >= STALE_THRESHOLD) {
396
+ // ── 进度感知收敛:探索已饱和,提前引导 AI 总结 ──
397
+ this.#logger.info(
398
+ `[ChatAgent] 📊 Exploration saturated at iter ${iterationCount}/${maxIter} — ` +
399
+ `files=${explorationMetrics.uniqueFiles.size}, patterns=${explorationMetrics.uniquePatterns.size}, ` +
400
+ `queries=${explorationMetrics.uniqueQueries.size}, staleRounds=${explorationMetrics.staleRounds} — nudging convergence`
401
+ );
402
+ ctx.appendUserNudge(
403
+ `你已经充分探索了项目代码(${explorationMetrics.uniqueFiles.size} 个文件,${explorationMetrics.uniquePatterns.size} 次不同搜索,${explorationMetrics.uniqueQueries.size} 次结构化查询)。` +
404
+ `最近 ${explorationMetrics.staleRounds} 轮没有发现新信息,建议开始撰写分析总结。\n` +
405
+ `如果你确信还有重要方面未覆盖,可以继续探索(剩余 ${maxIter - iterationCount} 轮);否则请直接输出你的分析发现。`
406
+ );
407
+ explorationMetrics.convergenceNudged = true;
408
+ // 不 break,不设 toolChoice=none — 这是软 nudge,AI 仍可继续探索
409
+ } else if (isSystem && !phaseRouter && !ctx.__budgetWarningInjected
410
+ && iterationCount >= Math.floor(maxIter * 0.75)) {
411
+ // ── 预算感知提醒:75% 预算消耗时轻量提示 ──
412
+ ctx.appendUserNudge(
413
+ `📌 进度提醒:你已使用 ${iterationCount}/${maxIter} 轮次(${Math.round(iterationCount / maxIter * 100)}%)。` +
414
+ `请确保核心方面已覆盖,开始准备总结。剩余 ${maxIter - iterationCount} 轮,优先填补最重要的分析空白。`
415
+ );
416
+ ctx.__budgetWarningInjected = true;
417
+ this.#logger.info(`[ChatAgent] 📌 Budget warning at ${iterationCount}/${maxIter} (${Math.round(iterationCount / maxIter * 100)}%)`);
418
+ } else if (isSystem && iterationCount >= maxIter && !ctx.__gracefulExitInjected) {
419
+ // 达到上限 → 注入收尾消息让 AI 快速总结(而非硬中断)
420
+ this.#logger.info(`[ChatAgent] Iteration cap reached (${iterationCount}/${maxIter}) — injecting graceful exit nudge`);
421
+ const submitCount = toolCalls.filter(tc => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check').length;
422
+ ctx.appendUserNudge(
423
+ `⚠️ 你已使用 ${iterationCount}/${maxIter} 轮次,**必须立即结束**。请在回复中直接输出 dimensionDigest JSON 总结(用 \`\`\`json 包裹),不要再调用任何工具。\n` +
424
+ `\`\`\`json\n{"dimensionDigest":{"summary":"分析总结","candidateCount":${submitCount},"keyFindings":["发现"],"crossRefs":{},"gaps":["缺口"],"remainingTasks":[{"signal":"未处理信号","reason":"轮次耗尽","priority":"high","searchHints":["搜索词"]}]}}\n\`\`\`\n> remainingTasks: 列出未来得及处理的信号。已覆盖则留空 \`[]\`。`
425
+ );
426
+ ctx.__gracefulExitInjected = true;
427
+ // 不 break,给 AI 2 轮 grace 来产出总结
428
+ // 继续 while 循环
429
+ } else if (isSystem && iterationCount >= maxIter + 2) {
430
+ // 硬上限兜底:grace 轮次也耗尽
431
+ this.#logger.info(`[ChatAgent] Hard cap reached: ${iterationCount}/${maxIter + 2}`);
343
432
  break;
344
433
  } else if (!isSystem && ctx.length > maxIter * 2 + 2) {
345
434
  // 用户对话模式: 简单的消息数限制
@@ -352,7 +441,10 @@ export class ChatAgent {
352
441
 
353
442
  // ── 动态 toolChoice (由 PhaseRouter 决定) ──
354
443
  let currentChoice;
355
- if (phaseRouter) {
444
+ if (ctx.__gracefulExitInjected) {
445
+ // graceful exit: 禁止工具调用,强制 AI 输出文本总结
446
+ currentChoice = 'none';
447
+ } else if (phaseRouter) {
356
448
  currentChoice = phaseRouter.getToolChoice();
357
449
  } else {
358
450
  currentChoice = 'auto';
@@ -371,6 +463,19 @@ export class ChatAgent {
371
463
  if (hint) {
372
464
  systemPrompt += `\n\n## 当前状态\n${hint}`;
373
465
  }
466
+ } else if (isSystem) {
467
+ // 非 PhaseRouter 路径:注入实时轮次计数器,让 AI 始终感知进度
468
+ const verifyStart = Math.floor(maxIter * 0.8);
469
+ const remaining = maxIter - iterationCount;
470
+ let phaseLabel;
471
+ if (iterationCount <= Math.floor(maxIter * 0.6)) {
472
+ phaseLabel = '探索阶段';
473
+ } else if (iterationCount <= verifyStart) {
474
+ phaseLabel = '验证阶段';
475
+ } else {
476
+ phaseLabel = '⚠ 总结阶段 — 请停止工具调用,直接输出分析文本';
477
+ }
478
+ systemPrompt += `\n\n## 当前进度\n第 ${iterationCount}/${maxIter} 轮 | ${phaseLabel} | 剩余 ${remaining} 轮`;
374
479
  }
375
480
 
376
481
  // ── AI 调用 ──
@@ -438,6 +543,20 @@ export class ChatAgent {
438
543
 
439
544
  // ── 处理 functionCalls ──
440
545
  if (aiResult.functionCalls && aiResult.functionCalls.length > 0) {
546
+ // ── Graceful exit 保护: Gemini 有时会无视 toolChoice='none' 继续返回工具调用
547
+ // 强制忽略工具调用,将附带文本视为最终回复
548
+ if (ctx.__gracefulExitInjected) {
549
+ this.#logger.warn(`[ChatAgent] ⚠ AI returned ${aiResult.functionCalls.length} tool calls despite toolChoice=none (graceful exit) — ignoring tools, treating as text`);
550
+ if (aiResult.text) {
551
+ const reply = this.#cleanFinalAnswer(aiResult.text);
552
+ const totalDuration = Date.now() - execStartTime;
553
+ this.#logger.info(`[ChatAgent] ✅ final answer (graceful exit, forced) — ${reply.length} chars, ${toolCalls.length} tool calls, ${totalDuration}ms`);
554
+ return { reply, toolCalls, hasContext: toolCalls.length > 0 };
555
+ }
556
+ // 无文本时继续循环,下一轮硬上限兜底
557
+ continue;
558
+ }
559
+
441
560
  // 限制单次工具调用数量(防上下文溢出)
442
561
  const MAX_TOOL_CALLS_PER_ITER = 8;
443
562
  let activeCalls = aiResult.functionCalls;
@@ -450,6 +569,7 @@ export class ChatAgent {
450
569
  ctx.appendAssistantWithToolCalls(aiResult.text || null, activeCalls);
451
570
 
452
571
  let roundSubmitCount = 0;
572
+ let roundHasNewInfo = false; // 本轮是否获取到新信息
453
573
 
454
574
  for (const fc of activeCalls) {
455
575
  const toolStartTime = Date.now();
@@ -474,12 +594,105 @@ export class ChatAgent {
474
594
  const summarized = this.#summarizeResult(toolResult);
475
595
  toolCalls.push({ tool: fc.name, params: fc.args, result: summarized });
476
596
 
597
+ // ── 探索进度追踪 (非 PhaseRouter 路径) ──
598
+ if (!phaseRouter && isSystem) {
599
+ explorationMetrics.totalToolCalls++;
600
+ let foundNewInfo = false;
601
+
602
+ if (fc.name === 'search_project_code') {
603
+ const pattern = fc.args?.pattern || '';
604
+ const patterns = fc.args?.patterns || [];
605
+ // 单模式
606
+ if (pattern && !explorationMetrics.uniquePatterns.has(pattern)) {
607
+ explorationMetrics.uniquePatterns.add(pattern);
608
+ foundNewInfo = true;
609
+ }
610
+ // 批量模式
611
+ for (const p of patterns) {
612
+ if (!explorationMetrics.uniquePatterns.has(p)) {
613
+ explorationMetrics.uniquePatterns.add(p);
614
+ foundNewInfo = true;
615
+ }
616
+ }
617
+ // 检查搜索结果是否有新文件
618
+ if (toolResult && typeof toolResult === 'object') {
619
+ const matches = toolResult.matches || [];
620
+ const batchResults = toolResult.batchResults || {};
621
+ for (const m of matches) {
622
+ if (m.file && !explorationMetrics.uniqueFiles.has(m.file)) {
623
+ explorationMetrics.uniqueFiles.add(m.file);
624
+ foundNewInfo = true;
625
+ }
626
+ }
627
+ for (const sub of Object.values(batchResults)) {
628
+ for (const m of (sub.matches || [])) {
629
+ if (m.file && !explorationMetrics.uniqueFiles.has(m.file)) {
630
+ explorationMetrics.uniqueFiles.add(m.file);
631
+ foundNewInfo = true;
632
+ }
633
+ }
634
+ }
635
+ }
636
+ } else if (fc.name === 'read_project_file') {
637
+ const fp = fc.args?.filePath || '';
638
+ const fps = fc.args?.filePaths || [];
639
+ if (fp && !explorationMetrics.uniqueFiles.has(fp)) {
640
+ explorationMetrics.uniqueFiles.add(fp);
641
+ foundNewInfo = true;
642
+ }
643
+ for (const f of fps) {
644
+ if (!explorationMetrics.uniqueFiles.has(f)) {
645
+ explorationMetrics.uniqueFiles.add(f);
646
+ foundNewInfo = true;
647
+ }
648
+ }
649
+ } else if (fc.name === 'list_project_structure') {
650
+ // 目录结构:同一目录只算一次新信息
651
+ const dir = fc.args?.directory || '/';
652
+ const qKey = `list:${dir}`;
653
+ if (!explorationMetrics.uniqueQueries.has(qKey)) {
654
+ explorationMetrics.uniqueQueries.add(qKey);
655
+ foundNewInfo = true;
656
+ }
657
+ } else if (fc.name === 'get_class_info' || fc.name === 'get_class_hierarchy'
658
+ || fc.name === 'get_protocol_info' || fc.name === 'get_method_overrides'
659
+ || fc.name === 'get_category_map') {
660
+ // AST 结构化查询:同一类名/协议名只算一次新信息
661
+ const queryTarget = fc.args?.className || fc.args?.protocolName || fc.args?.name || '';
662
+ const qKey = `${fc.name}:${queryTarget}`;
663
+ if (!explorationMetrics.uniqueQueries.has(qKey)) {
664
+ explorationMetrics.uniqueQueries.add(qKey);
665
+ foundNewInfo = true;
666
+ }
667
+ } else if (fc.name === 'get_project_overview') {
668
+ // 项目概览只需调一次
669
+ const qKey = 'overview';
670
+ if (!explorationMetrics.uniqueQueries.has(qKey)) {
671
+ explorationMetrics.uniqueQueries.add(qKey);
672
+ foundNewInfo = true;
673
+ }
674
+ } else if (fc.name !== 'submit_knowledge' && fc.name !== 'submit_with_check') {
675
+ // 其他未分类工具 — 首次算新信息,之后同工具名+同参数去重
676
+ const qKey = `${fc.name}:${JSON.stringify(fc.args || {}).substring(0, 80)}`;
677
+ if (!explorationMetrics.uniqueQueries.has(qKey)) {
678
+ explorationMetrics.uniqueQueries.add(qKey);
679
+ foundNewInfo = true;
680
+ }
681
+ }
682
+
683
+ if (foundNewInfo) {
684
+ explorationMetrics.staleRounds = 0;
685
+ explorationMetrics.newInfoRounds++;
686
+ roundHasNewInfo = true;
687
+ }
688
+ }
689
+
477
690
  // ── Layer 3: ToolResultLimiter — 动态配额压缩 ──
478
691
  const quota = ctx.getToolResultQuota();
479
692
  let resultStr = limitToolResult(fc.name, toolResult, quota);
480
693
 
481
694
  // ── 重复提交 / 维度范围校验 ──
482
- if (fc.name === 'submit_candidate' || fc.name === 'submit_with_check') {
695
+ if (fc.name === 'submit_knowledge' || fc.name === 'submit_with_check') {
483
696
  const title = fc.args?.title || fc.args?.category || '';
484
697
  const isRejected = typeof toolResult === 'object' && toolResult?.status === 'rejected';
485
698
  const isError = typeof toolResult === 'object' && (toolResult?.error || toolResult?.status === 'error');
@@ -503,6 +716,11 @@ export class ChatAgent {
503
716
  ctx.appendToolResult(fc.id, fc.name, resultStr);
504
717
  }
505
718
 
719
+ // ── 探索饱和度更新 (非 PhaseRouter 路径) ──
720
+ if (!phaseRouter && isSystem && !roundHasNewInfo) {
721
+ explorationMetrics.staleRounds++;
722
+ }
723
+
506
724
  // ── PhaseRouter 更新 ──
507
725
  if (phaseRouter) {
508
726
  const transition = phaseRouter.update({
@@ -516,7 +734,7 @@ export class ChatAgent {
516
734
  // 需要一条 user 消息明确告知 AI 切换到提交模式
517
735
  if (transition.transitioned && transition.newPhase === 'PRODUCE') {
518
736
  ctx.appendUserNudge(
519
- '你已充分探索了项目代码,现在请开始调用 submit_candidate 工具来提交你发现的知识候选。不要再搜索,直接提交。'
737
+ '你已充分探索了项目代码,现在请开始调用 submit_knowledge 工具来提交你发现的知识候选。不要再搜索,直接提交。'
520
738
  );
521
739
  this.#logger.info('[ChatAgent] 📝 injected PRODUCE transition nudge');
522
740
  }
@@ -525,7 +743,7 @@ export class ChatAgent {
525
743
  // skill-only 维度从 EXPLORE 直接进入 SUMMARIZE (跳过 PRODUCE),
526
744
  // 需要明确告知 AI 输出 dimensionDigest JSON
527
745
  if (transition.transitioned && transition.newPhase === 'SUMMARIZE') {
528
- const submitCount = toolCalls.filter(tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check').length;
746
+ const submitCount = toolCalls.filter(tc => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check').length;
529
747
  ctx.appendUserNudge(
530
748
  `你已完成分析探索。请在回复中直接输出 dimensionDigest JSON(用 \`\`\`json 包裹),包含以下字段:\n\`\`\`json\n{"dimensionDigest":{"summary":"分析总结(100-200字)","candidateCount":${submitCount},"keyFindings":["关键发现"],"crossRefs":{},"gaps":["未覆盖方面"],"remainingTasks":[{"signal":"未处理的信号/主题","reason":"未完成原因(如:提交上限已达)","priority":"high|medium|low","searchHints":["建议搜索词"]}]}}\n\`\`\`\n> 如果所有信号都已覆盖,remainingTasks 留空数组 \`[]\`。如果有未来得及处理的信号,请在此标记,系统会在下次运行时续传。`
531
749
  );
@@ -563,7 +781,7 @@ export class ChatAgent {
563
781
  // 注入 nudge 让 AI 再输出一次 digest JSON
564
782
  if (transition.transitioned) {
565
783
  ctx.appendAssistantText(aiResult.text || '');
566
- const submitCount = toolCalls.filter(tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check').length;
784
+ const submitCount = toolCalls.filter(tc => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check').length;
567
785
  ctx.appendUserNudge(
568
786
  `请在回复中直接输出 dimensionDigest JSON 总结(用 \`\`\`json 包裹):\n\`\`\`json\n{"dimensionDigest":{"summary":"分析总结","candidateCount":${submitCount},"keyFindings":["发现"],"crossRefs":{},"gaps":["缺口"],"remainingTasks":[{"signal":"未处理的信号","reason":"原因","priority":"high","searchHints":["搜索词"]}]}}\n\`\`\`\n> remainingTasks: 记录未来得及处理的信号。已全部覆盖则留空 \`[]\`。`
569
787
  );
@@ -584,7 +802,7 @@ export class ChatAgent {
584
802
  ctx.appendAssistantText(aiResult.text || '');
585
803
  if (phaseRouter.phase === 'PRODUCE') {
586
804
  ctx.appendUserNudge(
587
- '你的分析很好。请继续调用 submit_candidate 提交你发现的知识候选,每个值得记录的模式/实践都应该提交。'
805
+ '你的分析很好。请继续调用 submit_knowledge 提交你发现的知识候选,每个值得记录的模式/实践都应该提交。'
588
806
  );
589
807
  this.#logger.info('[ChatAgent] 📝 injected submit nudge (text in PRODUCE, not transitioning)');
590
808
  }
@@ -602,7 +820,15 @@ export class ChatAgent {
602
820
  continue;
603
821
  }
604
822
 
605
- // 用户对话: 文字回答即最终回答
823
+ // 用户对话 / graceful exit: 文字回答即最终回答
824
+ if (ctx.__gracefulExitInjected || !isSystem) {
825
+ const reply = this.#cleanFinalAnswer(aiResult.text || '');
826
+ const totalDuration = Date.now() - execStartTime;
827
+ this.#logger.info(`[ChatAgent] ✅ final answer (${ctx.__gracefulExitInjected ? 'graceful exit' : 'user'}) — ${reply.length} chars, ${toolCalls.length} tool calls, ${totalDuration}ms`);
828
+ return { reply, toolCalls, hasContext: toolCalls.length > 0 };
829
+ }
830
+
831
+ // system 模式非 graceful exit 的文字回答(理论上不应到这里)
606
832
  const reply = this.#cleanFinalAnswer(aiResult.text || '');
607
833
  const totalDuration = Date.now() - execStartTime;
608
834
  this.#logger.info(`[ChatAgent] ✅ final answer — ${reply.length} chars, ${toolCalls.length} tool calls, ${totalDuration}ms`);
@@ -624,7 +850,7 @@ export class ChatAgent {
624
850
  this.#logger.info(`[ChatAgent] ⚠ producing forced summary (${iterations} iters, ${toolCalls.length} calls)`);
625
851
 
626
852
  const candidateCount = toolCalls.filter(tc =>
627
- tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check'
853
+ tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check'
628
854
  ).length;
629
855
 
630
856
  let finalReply;
@@ -639,7 +865,7 @@ export class ChatAgent {
639
865
  if (isCircuitOpen) throw new Error('circuit open — skip to synthetic digest');
640
866
 
641
867
  const submitSummary = toolCalls
642
- .filter(tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check')
868
+ .filter(tc => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check')
643
869
  .map((tc, i) => `${i + 1}. ${tc.params?.title || tc.params?.category || 'untitled'}`)
644
870
  .join('\n');
645
871
 
@@ -686,7 +912,7 @@ ${submitSummary ? `已提交候选:\n${submitSummary}\n` : ''}
686
912
  this.#logger.warn(`[ChatAgent] forced summary AI call failed: ${err.message}`);
687
913
  // 合成 digest 兜底
688
914
  const titles = toolCalls
689
- .filter(tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check')
915
+ .filter(tc => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check')
690
916
  .map(tc => tc.params?.title || 'untitled');
691
917
  finalReply = `\`\`\`json
692
918
  {
@@ -771,7 +997,7 @@ ${submitSummary ? `已提交候选:\n${submitSummary}\n` : ''}
771
997
 
772
998
  if (!actions) {
773
999
  // ── 系统调用自动续跑 ──
774
- const hasSubmits = toolCalls.some(tc => tc.tool === 'submit_candidate' || tc.tool === 'submit_with_check');
1000
+ const hasSubmits = toolCalls.some(tc => tc.tool === 'submit_knowledge' || tc.tool === 'submit_with_check');
775
1001
  if (source === 'system' && iterations < maxIter && !hasSubmits) {
776
1002
  if (this.#looksLikeIncompleteStep(response)) {
777
1003
  this.#logger.info(`[ChatAgent] 🔄 detected planning-only response at iteration ${iterations}, injecting continuation prompt`);
@@ -1046,13 +1272,13 @@ ${highSim.map(s => `- ${s.title} (相似度: ${s.similarity})`).join('\n')}
1046
1272
  */
1047
1273
  async #taskDiscoverAllRelations({ batchSize = 20 } = {}) {
1048
1274
  const ctx = this.#getToolContext();
1049
- const recipeService = ctx.container.get('recipeService');
1050
- if (!recipeService) throw new Error('RecipeService 不可用');
1275
+ const knowledgeService = ctx.container.get('knowledgeService');
1276
+ if (!knowledgeService) throw new Error('KnowledgeService 不可用');
1051
1277
 
1052
1278
  if (!ctx.aiProvider) throw new Error('AI Provider 未配置,请先设置 API Key');
1053
1279
 
1054
- // 获取所有 recipe
1055
- const { items = [], data = [] } = await recipeService.listRecipes({}, { page: 1, pageSize: 500 });
1280
+ // 获取所有活跃知识条目
1281
+ const { items = [], data = [] } = await knowledgeService.list({ lifecycle: 'active' }, { page: 1, pageSize: 500 });
1056
1282
  const recipes = items.length > 0 ? items : data;
1057
1283
  if (recipes.length < 2) return { discovered: 0, totalPairs: 0, message: `只有 ${recipes.length} 条 Recipe,至少需要 2 条` };
1058
1284
 
@@ -1108,10 +1334,10 @@ ${highSim.map(s => `- ${s.title} (相似度: ${s.similarity})`).join('\n')}
1108
1334
  */
1109
1335
  async #taskFullEnrich({ status = 'pending', maxCount = 50 } = {}) {
1110
1336
  const ctx = this.#getToolContext();
1111
- const candidateService = ctx.container.get('candidateService');
1337
+ const knowledgeService = ctx.container.get('knowledgeService');
1112
1338
 
1113
- const { items = [], data = [] } = await candidateService.listCandidates(
1114
- { status }, { page: 1, pageSize: maxCount }
1339
+ const { items = [], data = [] } = await knowledgeService.list(
1340
+ { lifecycle: status }, { page: 1, pageSize: maxCount }
1115
1341
  );
1116
1342
  const candidates = items.length > 0 ? items : data;
1117
1343
  if (candidates.length === 0) return { enriched: 0, message: 'No candidates to enrich' };
@@ -1137,10 +1363,10 @@ ${highSim.map(s => `- ${s.title} (相似度: ${s.similarity})`).join('\n')}
1137
1363
  */
1138
1364
  async #taskQualityAudit({ threshold = 0.6, maxCount = 100 } = {}) {
1139
1365
  const ctx = this.#getToolContext();
1140
- const recipeService = ctx.container.get('recipeService');
1366
+ const knowledgeService = ctx.container.get('knowledgeService');
1141
1367
 
1142
- const { items = [], data = [] } = await recipeService.listRecipes(
1143
- { status: 'active' }, { page: 1, pageSize: maxCount }
1368
+ const { items = [], data = [] } = await knowledgeService.list(
1369
+ { lifecycle: 'active' }, { page: 1, pageSize: maxCount }
1144
1370
  );
1145
1371
  const recipes = items.length > 0 ? items : data;
1146
1372
  if (recipes.length === 0) return { total: 0, lowQuality: [], message: 'No active recipes' };
@@ -1267,7 +1493,7 @@ ${code.substring(0, 3000)}
1267
1493
  // 核心工具 — 使用最频繁,直接展示完整 schema
1268
1494
  const coreTools = new Set([
1269
1495
  'search_project_code', 'read_project_file',
1270
- 'search_knowledge', 'submit_candidate', 'submit_with_check', 'analyze_code',
1496
+ 'search_knowledge', 'submit_knowledge', 'submit_with_check', 'analyze_code',
1271
1497
  'bootstrap_knowledge', 'load_skill', 'suggest_skills',
1272
1498
  'create_skill', 'knowledge_overview', 'get_tool_details',
1273
1499
  'plan_task', 'review_my_output',
@@ -1323,8 +1549,8 @@ ${skillSection}
1323
1549
 
1324
1550
  \`\`\`batch_actions
1325
1551
  [
1326
- {"tool": "submit_candidate", "params": {"title": "...", "code": "..."}},
1327
- {"tool": "submit_candidate", "params": {"title": "...", "code": "..."}}
1552
+ {"tool": "submit_knowledge", "params": {"title": "...", "code": "..."}},
1553
+ {"tool": "submit_knowledge", "params": {"title": "...", "code": "..."}}
1328
1554
  ]
1329
1555
  \`\`\`
1330
1556
 
@@ -1390,10 +1616,15 @@ ${this.#projectBriefingCache}
1390
1616
  2. **定向探索** → get_file_summary 快速了解文件角色
1391
1617
  3. **深入研读** → search_project_code / read_project_file 获取真实代码
1392
1618
  4. **语义发现** → semantic_search_code 在知识库查找相关知识
1393
- 5. **知识产出** → submit_candidate 提交有价值的发现
1619
+ 5. **知识产出** → submit_knowledge 提交有价值的发现
1620
+
1621
+ ## 高效使用工具(节省轮次)
1622
+ - **批量搜索**: search_project_code({ patterns: ["keywordA", "keywordB", "keywordC"] })
1623
+ - **批量读文件**: read_project_file({ filePaths: ["path/a.m", "path/b.m"] })
1624
+ - 合并同类请求为一次调用,避免逐个搜索/读取浪费轮次。
1394
1625
 
1395
1626
  ## 「项目特写」= 基本用法 + 项目特征融合
1396
- submit_candidate 的 code 字段必须是「项目特写」— 将技术的基本用法与本项目的特征融合为一体:
1627
+ submit_knowledge 的 code 字段必须是「项目特写」— 将技术的基本用法与本项目的特征融合为一体:
1397
1628
  1. **项目选择了什么**: 采用了哪种写法/模式/约定
1398
1629
  2. **为什么这样选**: 统计数据(N 个文件、占比 M%)
1399
1630
  3. **项目禁止什么**: 被放弃的写法、反模式、显式禁用标记
@@ -1771,15 +2002,16 @@ submit_candidate 的 code 字段必须是「项目特写」— 将技术的基
1771
2002
  // rule: code-standard, code-style, best-practice, boundary-constraint
1772
2003
  // pattern: code-pattern, architecture, solution
1773
2004
  // fact: code-relation, inheritance, call-chain, data-flow, module-dependency
2005
+ // V3: knowledge_entries 统一表(candidates 已合并,lifecycle 替代 status)
1774
2006
  const stats = db.prepare(`
1775
2007
  SELECT
1776
- (SELECT COUNT(*) FROM recipes) as recipeCount,
1777
- (SELECT COUNT(*) FROM recipes WHERE knowledge_type IN ('code-standard','code-style','best-practice','boundary-constraint')) as ruleCount,
1778
- (SELECT COUNT(*) FROM recipes WHERE knowledge_type IN ('code-pattern','architecture','solution')) as patternCount,
1779
- (SELECT COUNT(*) FROM recipes WHERE knowledge_type IN ('code-relation','inheritance','call-chain','data-flow','module-dependency')) as factCount,
1780
- (SELECT COUNT(*) FROM recipes WHERE knowledge_type = 'boundary-constraint') as guardRuleCount,
1781
- (SELECT COUNT(*) FROM candidates WHERE status='pending') as pendingCandidates,
1782
- (SELECT COUNT(*) FROM candidates) as totalCandidates
2008
+ (SELECT COUNT(*) FROM knowledge_entries WHERE lifecycle = 'active') as recipeCount,
2009
+ (SELECT COUNT(*) FROM knowledge_entries WHERE lifecycle = 'active' AND knowledge_type IN ('code-standard','code-style','best-practice','boundary-constraint')) as ruleCount,
2010
+ (SELECT COUNT(*) FROM knowledge_entries WHERE lifecycle = 'active' AND knowledge_type IN ('code-pattern','architecture','solution')) as patternCount,
2011
+ (SELECT COUNT(*) FROM knowledge_entries WHERE lifecycle = 'active' AND knowledge_type IN ('code-relation','inheritance','call-chain','data-flow','module-dependency')) as factCount,
2012
+ (SELECT COUNT(*) FROM knowledge_entries WHERE lifecycle = 'active' AND knowledge_type = 'boundary-constraint') as guardRuleCount,
2013
+ (SELECT COUNT(*) FROM knowledge_entries WHERE lifecycle = 'pending') as pendingCandidates,
2014
+ (SELECT COUNT(*) FROM knowledge_entries) as totalCandidates
1783
2015
  `).get();
1784
2016
  if (!stats || stats.recipeCount === 0) {
1785
2017
  return '\n## 项目状态\n⚠️ 知识库为空。建议先执行冷启动(bootstrap_knowledge)。\n';