autosnippet 3.2.4 → 3.2.7

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 (67) hide show
  1. package/README.md +18 -5
  2. package/bin/cli.js +164 -145
  3. package/config/constitution.yaml +2 -0
  4. package/dashboard/dist/assets/{index-DNOHYBhy.css → index-BaGY7kJI.css} +1 -1
  5. package/dashboard/dist/assets/{index-6itPuGFl.js → index-DfHY_3ln.js} +25 -25
  6. package/dashboard/dist/index.html +2 -2
  7. package/lib/cli/CliLogger.js +78 -0
  8. package/lib/cli/SetupService.js +9 -718
  9. package/lib/cli/UpgradeService.js +23 -398
  10. package/lib/cli/deploy/FileDeployer.js +562 -0
  11. package/lib/cli/deploy/FileManifest.js +272 -0
  12. package/lib/external/mcp/McpServer.js +22 -26
  13. package/lib/external/mcp/autoApproveInjector.js +1 -0
  14. package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +5 -5
  15. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +25 -3
  16. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +6 -6
  17. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +4 -0
  18. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +5 -5
  19. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +89 -44
  20. package/lib/external/mcp/handlers/consolidated.js +8 -9
  21. package/lib/external/mcp/handlers/dimension-complete-external.js +4 -4
  22. package/lib/external/mcp/handlers/guard.js +283 -5
  23. package/lib/external/mcp/handlers/task.js +361 -15
  24. package/lib/external/mcp/tools.js +32 -81
  25. package/lib/http/HttpServer.js +4 -0
  26. package/lib/http/routes/remote.js +1138 -0
  27. package/lib/http/routes/task.js +56 -0
  28. package/lib/infrastructure/database/migrations/003_add_remote_commands.js +27 -0
  29. package/lib/service/chat/AnalystAgent.js +12 -12
  30. package/lib/service/chat/ChatAgent.js +227 -545
  31. package/lib/service/chat/ChatAgentPrompts.js +9 -11
  32. package/lib/service/chat/ContextWindow.js +2 -296
  33. package/lib/service/chat/EpisodicConsolidator.js +15 -15
  34. package/lib/service/chat/ExplorationTracker.js +1262 -0
  35. package/lib/service/chat/HandoffProtocol.js +8 -9
  36. package/lib/service/chat/Memory.js +4 -0
  37. package/lib/service/chat/ProducerAgent.js +9 -6
  38. package/lib/service/chat/ProjectSemanticMemory.js +4 -0
  39. package/lib/service/chat/ReasoningTrace.js +182 -0
  40. package/lib/service/chat/WorkingMemory.js +4 -0
  41. package/lib/service/chat/memory/ActiveContext.js +910 -0
  42. package/lib/service/chat/memory/MemoryCoordinator.js +662 -0
  43. package/lib/service/chat/memory/PersistentMemory.js +450 -0
  44. package/lib/service/chat/memory/SessionStore.js +896 -0
  45. package/lib/service/chat/memory/index.js +13 -0
  46. package/lib/service/chat/tools/ast-graph.js +17 -16
  47. package/lib/service/cursor/AgentInstructionsGenerator.js +76 -47
  48. package/lib/service/cursor/FileProtection.js +4 -1
  49. package/lib/service/guard/GuardCheckEngine.js +10 -3
  50. package/lib/service/task/TaskGraphService.js +3 -3
  51. package/lib/shared/LanguageService.js +2 -1
  52. package/package.json +12 -1
  53. package/skills/autosnippet-intent/SKILL.md +1 -3
  54. package/skills/autosnippet-recipes/SKILL.md +1 -3
  55. package/templates/claude-code/commands/prime.md +19 -0
  56. package/templates/claude-code/hooks/autosnippet-session.sh +63 -0
  57. package/templates/claude-code/settings.json +21 -0
  58. package/templates/copilot-instructions.md +64 -177
  59. package/templates/cursor-hooks/commands/prime.md +12 -0
  60. package/templates/cursor-hooks/hooks/session-start.sh +10 -0
  61. package/templates/cursor-hooks/hooks.json +11 -0
  62. package/templates/cursor-rules/autosnippet-conventions.mdc +52 -3
  63. package/templates/cursor-rules/autosnippet-workflow.mdc +51 -27
  64. package/lib/external/mcp/handlers/decide.js +0 -109
  65. package/lib/external/mcp/handlers/ready.js +0 -42
  66. package/lib/service/chat/ReasoningLayer.js +0 -888
  67. package/templates/claude-hooks.yaml +0 -19
@@ -21,16 +21,15 @@ import { AnalystAgent } from '../../../../../service/chat/AnalystAgent.js';
21
21
  import { EpisodicConsolidator } from '../../../../../service/chat/EpisodicConsolidator.js';
22
22
  import { ProducerAgent } from '../../../../../service/chat/ProducerAgent.js';
23
23
  import { ProjectSemanticMemory } from '../../../../../service/chat/ProjectSemanticMemory.js';
24
- import { WorkingMemory } from '../../../../../service/chat/WorkingMemory.js';
24
+ import { MemoryCoordinator } from '../../../../../service/chat/memory/MemoryCoordinator.js';
25
+ import { SessionStore } from '../../../../../service/chat/memory/SessionStore.js';
25
26
  import { clearCheckpoints, loadCheckpoints, saveDimensionCheckpoint } from './checkpoint.js';
26
27
  import { buildTierReflection, DIMENSION_CONFIGS_V3, getFullDimensionConfig } from './dimension-configs.js';
27
28
  import { getDimensionFocusKeywords } from '../shared/dimension-sop.js';
28
29
  import { DimensionContext, parseDimensionDigest } from './dimension-context.js';
29
- import { EpisodicMemory } from './EpisodicMemory.js';
30
30
  import { IncrementalBootstrap } from './IncrementalBootstrap.js';
31
- import { runNoAiFallback } from './noAiFallback.js';
32
- import { ToolResultCache } from './ToolResultCache.js';
33
31
  import { TierScheduler } from './tier-scheduler.js';
32
+ import { runNoAiFallback } from './noAiFallback.js';
34
33
  import { generateSkill } from '../shared/skill-generator.js';
35
34
  import { BootstrapEventEmitter } from '../../../../../shared/BootstrapEventEmitter.js';
36
35
 
@@ -283,25 +282,25 @@ export async function fillDimensionsV3(fillContext) {
283
282
  guardSummary: guardAudit?.summary || null,
284
283
  });
285
284
 
286
- // v4.0: EpisodicMemory — 替代 DimensionContext 提供更丰富的跨维度上下文
285
+ // v4.0: SessionStore — 替代 EpisodicMemory + ToolResultCache
287
286
  // v5.0: 增量模式下从快照恢复已完成维度的记忆
288
- let episodicMemory;
287
+ let sessionStore;
289
288
  if (isIncremental && incrementalPlan.restoredEpisodic) {
290
- episodicMemory = incrementalPlan.restoredEpisodic;
291
- const restoredDims = episodicMemory.getCompletedDimensions();
289
+ sessionStore = incrementalPlan.restoredEpisodic;
290
+ const restoredDims = sessionStore.getCompletedDimensions();
292
291
  logger.info(
293
- `[Bootstrap-v3] Restored EpisodicMemory: ${restoredDims.length} dims [${restoredDims.join(', ')}]`
292
+ `[Bootstrap-v3] Restored SessionStore: ${restoredDims.length} dims [${restoredDims.join(', ')}]`
294
293
  );
295
294
 
296
295
  // 同步恢复 DimensionContext 的 digests (兼容)
297
296
  for (const dimId of restoredDims) {
298
- const report = episodicMemory.getDimensionReport(dimId);
297
+ const report = sessionStore.getDimensionReport(dimId);
299
298
  if (report?.digest) {
300
299
  dimContext.addDimensionDigest(dimId, report.digest);
301
300
  }
302
301
  }
303
302
  } else {
304
- episodicMemory = new EpisodicMemory({
303
+ sessionStore = new SessionStore({
305
304
  projectName: projectInfo.name,
306
305
  primaryLang: projectInfo.lang,
307
306
  fileCount: projectInfo.fileCount,
@@ -309,9 +308,6 @@ export async function fillDimensionsV3(fillContext) {
309
308
  });
310
309
  }
311
310
 
312
- // v4.0: ToolResultCache — 跨维度工具结果缓存 (search/read 去重)
313
- const toolResultCache = new ToolResultCache();
314
-
315
311
  // v4.1: ProjectSemanticMemory — 项目级永久语义记忆 (Tier 3)
316
312
  // 加载历史 bootstrap 记忆 → 注入 AnalystAgent prompt
317
313
  let semanticMemory = null;
@@ -349,6 +345,13 @@ export async function fillDimensionsV3(fillContext) {
349
345
  logger.warn(`[Bootstrap-v3] CodeEntityGraph init failed (non-blocking): ${cegErr.message}`);
350
346
  }
351
347
 
348
+ // v5.0: MemoryCoordinator — 统一记忆协调器 (会话级)
349
+ const memoryCoordinator = new MemoryCoordinator({
350
+ persistentMemory: semanticMemory,
351
+ sessionStore,
352
+ mode: 'bootstrap',
353
+ });
354
+
352
355
  // ═══════════════════════════════════════════════════════════
353
356
  // Step 2: 按维度分层执行 (Analyst → Gate → Producer)
354
357
  // ═══════════════════════════════════════════════════════════
@@ -393,8 +396,8 @@ export async function fillDimensionsV3(fillContext) {
393
396
  // 恢复 DimensionContext 中的 digest
394
397
  if (checkpoint.digest) {
395
398
  dimContext.addDimensionDigest(dimId, checkpoint.digest);
396
- // v4.0: 同步恢复到 EpisodicMemory
397
- episodicMemory.addDimensionDigest(dimId, checkpoint.digest);
399
+ // v4.0: 同步恢复到 SessionStore
400
+ sessionStore.addDimensionDigest(dimId, checkpoint.digest);
398
401
  }
399
402
  emitter.emitDimensionComplete(dimId, {
400
403
  type: 'checkpoint-restored',
@@ -415,7 +418,7 @@ export async function fillDimensionsV3(fillContext) {
415
418
  async function executeDimension(dimId) {
416
419
  // v5.0: 增量模式 — 跳过未受影响的维度 (使用历史 EpisodicMemory)
417
420
  if (incrementalSkippedDims.includes(dimId)) {
418
- const report = episodicMemory.getDimensionReport(dimId);
421
+ const report = sessionStore.getDimensionReport(dimId);
419
422
  const dimResult = {
420
423
  candidateCount: report?.candidatesSummary?.length || 0,
421
424
  rejectedCount: 0,
@@ -462,7 +465,7 @@ export async function fillDimensionsV3(fillContext) {
462
465
  },
463
466
  producerResult: { candidateCount: cp.candidateCount || 0, toolCalls: [] },
464
467
  };
465
- episodicMemory.storeDimensionReport(dimId, {
468
+ sessionStore.storeDimensionReport(dimId, {
466
469
  analysisText: cp.analysisText,
467
470
  findings: [],
468
471
  referencedFiles: restoredFiles,
@@ -528,18 +531,17 @@ export async function fillDimensionsV3(fillContext) {
528
531
  const dimStartTime = Date.now();
529
532
 
530
533
  try {
531
- // v4.0: 为每个维度创建独立的 WorkingMemory
532
- const dimWorkingMemory = new WorkingMemory({ maxRecentRounds: 3 });
534
+ // v5.0: 为每个维度创建独立的 ActiveContext 作用域
535
+ const analystScopeId = `${dimId}:analyst`;
536
+ memoryCoordinator.createDimensionScope(analystScopeId);
533
537
 
534
538
  // ── Phase 1: Analyst ──
535
539
  const analysisReport = await Promise.race([
536
540
  analystAgent.analyze(dimConfig, projectInfo, {
537
541
  sessionId,
538
542
  dimensionContext: dimContext,
539
- // v4.0: Agent Memory 注入
540
- episodicMemory,
541
- workingMemory: dimWorkingMemory,
542
- toolResultCache,
543
+ // v5.0: 统一 MemoryCoordinator
544
+ memoryCoordinator,
543
545
  // v4.1: Semantic Memory (历史 bootstrap 记忆)
544
546
  semanticMemory,
545
547
  // Phase E: Code Entity Graph 代码实体图谱
@@ -550,9 +552,10 @@ export async function fillDimensionsV3(fillContext) {
550
552
  ),
551
553
  ]);
552
554
 
553
- // v4.0: 蒸馏 WorkingEpisodic
554
- const distilled = dimWorkingMemory.distill();
555
- episodicMemory.storeDimensionReport(dimId, {
555
+ // v5.0: 通过 coordinator 蒸馏 ActiveContextSessionStore
556
+ const ac = memoryCoordinator.getActiveContext(analystScopeId);
557
+ const distilled = ac ? ac.distill() : { keyFindings: [], totalObservations: 0, toolCallSummary: [] };
558
+ sessionStore.storeDimensionReport(dimId, {
556
559
  analysisText: analysisReport.analysisText,
557
560
  findings: analysisReport.findings || distilled.keyFindings,
558
561
  referencedFiles: analysisReport.referencedFiles || [],
@@ -593,8 +596,15 @@ export async function fillDimensionsV3(fillContext) {
593
596
 
594
597
  if (needsCandidates && analysisReport.analysisText.length >= 100) {
595
598
  try {
599
+ // v5.0: 为 Producer 创建独立作用域
600
+ const producerScopeId = `${dimId}:producer`;
601
+ memoryCoordinator.createDimensionScope(producerScopeId);
602
+
596
603
  producerResult = await Promise.race([
597
- producerAgent.produce(analysisReport, dimConfig, projectInfo, { sessionId }),
604
+ producerAgent.produce(analysisReport, dimConfig, projectInfo, {
605
+ sessionId,
606
+ memoryCoordinator,
607
+ }),
598
608
  new Promise((_, reject) =>
599
609
  setTimeout(() => reject(new Error(`Producer timeout for "${dimId}"`)), 180_000)
600
610
  ),
@@ -624,8 +634,8 @@ export async function fillDimensionsV3(fillContext) {
624
634
  };
625
635
  dimContext.addDimensionDigest(dimId, digest);
626
636
 
627
- // v4.0: 同步 digest 到 EpisodicMemory
628
- episodicMemory.addDimensionDigest(dimId, digest);
637
+ // v4.0: 同步 digest 到 SessionStore
638
+ sessionStore.addDimensionDigest(dimId, digest);
629
639
 
630
640
  // 记录到 DimensionContext + EpisodicMemory
631
641
  for (const tc of producerResult.toolCalls || []) {
@@ -637,8 +647,8 @@ export async function fillDimensionsV3(fillContext) {
637
647
  summary: tc.params?.summary || '',
638
648
  };
639
649
  dimContext.addSubmittedCandidate(dimId, candidateSummary);
640
- // v4.0: 同步到 EpisodicMemory
641
- episodicMemory.addSubmittedCandidate(dimId, candidateSummary);
650
+ // v4.0: 同步到 SessionStore
651
+ sessionStore.addSubmittedCandidate(dimId, candidateSummary);
642
652
  }
643
653
  }
644
654
 
@@ -718,8 +728,8 @@ export async function fillDimensionsV3(fillContext) {
718
728
 
719
729
  // v4.0: Tier 级 Reflection — 综合本 Tier 所有维度的发现
720
730
  try {
721
- const reflection = buildTierReflection(tierIndex, tierResults, episodicMemory);
722
- episodicMemory.addTierReflection(tierIndex, reflection);
731
+ const reflection = buildTierReflection(tierIndex, tierResults, sessionStore);
732
+ sessionStore.addTierReflection(tierIndex, reflection);
723
733
  logger.info(
724
734
  `[Bootstrap-v3] Tier ${tierIndex + 1} reflection: ` +
725
735
  `${reflection.topFindings.length} top findings, ` +
@@ -734,18 +744,19 @@ export async function fillDimensionsV3(fillContext) {
734
744
  logger.info(
735
745
  `[Bootstrap-v3] All tiers complete: ${results.size} dimensions in ${Date.now() - t0}ms`
736
746
  );
737
- // v4.0: 记录 EpisodicMemory 统计 + ToolResultCache 效率
738
- const emStats = episodicMemory.getStats();
739
- const cacheStats = toolResultCache.getStats();
747
+ // v4.0: 记录 SessionStore 统计
748
+ const emStats = sessionStore.getStats();
740
749
  logger.info(
741
750
  `[Bootstrap-v3] Memory stats: ${emStats.completedDimensions} dims, ` +
742
751
  `${emStats.totalFindings} findings, ${emStats.referencedFiles} files, ` +
743
752
  `${emStats.crossReferences} cross-refs, ${emStats.tierReflections} reflections`
744
753
  );
745
- logger.info(
746
- `[Bootstrap-v3] Cache stats: ${cacheStats.hitRate} hit rate, ` +
747
- `${cacheStats.searchCacheSize} searches, ${cacheStats.fileCacheSize} files`
748
- );
754
+ if (emStats.cacheStats) {
755
+ logger.info(
756
+ `[Bootstrap-v3] Cache stats: ${emStats.cacheStats.hitRate} hit rate, ` +
757
+ `${emStats.cacheStats.searchCacheSize} searches, ${emStats.cacheStats.fileCacheSize} files`
758
+ );
759
+ }
749
760
  } else {
750
761
  // 串行: 按 TierScheduler 内部顺序逐个执行
751
762
  for (const tier of scheduler.getTiers()) {
@@ -787,8 +798,42 @@ export async function fillDimensionsV3(fillContext) {
787
798
  const analysisText = dimData.analysisReport.analysisText;
788
799
  const referencedFiles = dimData.analysisReport.referencedFiles || [];
789
800
 
801
+ // 从 SessionStore 获取结构化发现,供 Skill 生成使用
802
+ const dimReport = sessionStore.getDimensionReport(dim.id);
803
+ const keyFindings = (dimReport?.findings || [])
804
+ .sort((a, b) => (b.importance || 5) - (a.importance || 5))
805
+ .slice(0, 10)
806
+ .map((f) => f.finding);
807
+
808
+ // 当 analysisText 过短(如 force-exit 时 AI 仅输出 JSON digest 被清洗后)
809
+ // 从 distilled findings 合成补充文本,避免 Skill 质量门控拦截
810
+ let effectiveText = analysisText;
811
+ if (analysisText.trim().length < 100 && keyFindings.length > 0) {
812
+ const distilled = dimReport?.workingMemoryDistilled;
813
+ const synthesized = [
814
+ `## ${dim.label || dim.id}`,
815
+ '',
816
+ analysisText.trim(),
817
+ '',
818
+ '## 关键发现',
819
+ '',
820
+ ...keyFindings.map((f, i) => `${i + 1}. ${f}`),
821
+ ];
822
+ if (distilled?.toolCallSummary?.length > 0) {
823
+ synthesized.push('', '## 探索记录', '');
824
+ for (const s of distilled.toolCallSummary.slice(0, 10)) {
825
+ synthesized.push(`- ${s}`);
826
+ }
827
+ }
828
+ effectiveText = synthesized.join('\n');
829
+ logger.info(
830
+ `[Bootstrap-v3] Skill "${dim.id}": analysisText too short (${analysisText.trim().length} chars), ` +
831
+ `synthesized from ${keyFindings.length} findings → ${effectiveText.length} chars`
832
+ );
833
+ }
834
+
790
835
  const result = await generateSkill(
791
- ctx, dim, analysisText, referencedFiles, [], 'bootstrap-v3'
836
+ ctx, dim, effectiveText, referencedFiles, keyFindings, 'bootstrap-v3'
792
837
  );
793
838
 
794
839
  if (result.success) {
@@ -870,7 +915,7 @@ export async function fillDimensionsV3(fillContext) {
870
915
  const semanticMemory = new ProjectSemanticMemory(db, { logger });
871
916
  const consolidator = new EpisodicConsolidator(semanticMemory, { logger });
872
917
 
873
- consolidationResult = consolidator.consolidate(episodicMemory, {
918
+ consolidationResult = consolidator.consolidate(sessionStore, {
874
919
  bootstrapSession: sessionId,
875
920
  clearPrevious: true, // 全量冷启动: 先清除旧的 bootstrap 记忆
876
921
  });
@@ -1042,7 +1087,7 @@ export async function fillDimensionsV3(fillContext) {
1042
1087
  sessionId,
1043
1088
  allFiles,
1044
1089
  dimensionStats,
1045
- episodicMemory,
1090
+ episodicMemory: sessionStore,
1046
1091
  meta: {
1047
1092
  durationMs: totalTimeMs,
1048
1093
  candidateCount: candidateResults.created,
@@ -127,23 +127,22 @@ export async function consolidatedGraph(ctx, args) {
127
127
  }
128
128
  }
129
129
 
130
- // ─── autosnippet_guard (整合 2 → 1) ─────────────────────────
130
+ // ─── autosnippet_guard (整合 3 → 1) ─────────────────────────
131
131
 
132
132
  /**
133
133
  * Guard 检查:按参数自动路由
134
- * 有 code guardCheck() (单文件)
135
- * 有 files guardAuditFiles() (多文件)
134
+ * 无参数 guardReview() (自动 git diff 检测 + inline recipe)
135
+ * 有 files guardReview() (指定文件 + inline recipe) — files 为 string[] 或 {path}[]
136
+ * 有 code → guardCheck() (单文件内联检查)
136
137
  */
137
138
  export async function consolidatedGuard(ctx, args) {
138
- if (args.files && Array.isArray(args.files) && args.files.length > 0) {
139
- return guardHandlers.guardAuditFiles(ctx, args);
140
- }
139
+ // code 单文件检查(旧模式)
141
140
  if (args.code) {
142
141
  return guardHandlers.guardCheck(ctx, args);
143
142
  }
144
- throw new Error(
145
- 'autosnippet_guard requires either "code" (single check) or "files" (batch audit) parameter'
146
- );
143
+ // files(string[] 或 {path}[])或无参数 → review 模式
144
+ // review 模式内部处理 files 参数和自动检测
145
+ return guardHandlers.guardReview(ctx, args);
147
146
  }
148
147
 
149
148
  // ─── autosnippet_skill (整合 6 → 1) ─────────────────────────
@@ -336,11 +336,11 @@ export async function dimensionComplete(ctx, args) {
336
336
  try {
337
337
  const { EpisodicConsolidator } = await import('../../../service/chat/EpisodicConsolidator.js');
338
338
  const db = ctx.container.get?.('database') ?? ctx.container.get?.('db');
339
- if (db && session.episodicMemory) {
339
+ if (db && session.sessionStore) {
340
340
  const { ProjectSemanticMemory } = await import('../../../service/chat/ProjectSemanticMemory.js');
341
341
  const semanticMemory = new ProjectSemanticMemory(db, { logger });
342
342
  const consolidator = new EpisodicConsolidator(semanticMemory, { logger });
343
- const result = await consolidator.consolidate(session.episodicMemory, {
343
+ const result = await consolidator.consolidate(session.sessionStore, {
344
344
  bootstrapSession: session.id,
345
345
  clearPrevious: true,
346
346
  });
@@ -409,12 +409,12 @@ export async function dimensionComplete(ctx, args) {
409
409
  titles: s.titles,
410
410
  referencedFiles: s.referencedFiles,
411
411
  })),
412
- // v3: 从 EpisodicMemory 提取前序维度分析摘要 + 关键发现(对标内部 Agent 的 buildContextForDimension)
412
+ // v3: 从 SessionStore 提取前序维度分析摘要 + 关键发现(对标内部 Agent 的 buildContextForDimension)
413
413
  previousDimensionAnalysis: (() => {
414
414
  try {
415
415
  const summaries = [];
416
416
  for (const dimSummary of accumulatedEvidence.completedDimSummaries) {
417
- const report = session.episodicMemory.getDimensionReport(dimSummary.dimId);
417
+ const report = session.sessionStore.getDimensionReport(dimSummary.dimId);
418
418
  if (report) {
419
419
  summaries.push({
420
420
  dimId: dimSummary.dimId,
@@ -1,12 +1,23 @@
1
1
  /**
2
2
  * MCP Handlers — Guard 审计 & 项目扫描
3
- * guardCheck, guardAuditFiles, scanProject
3
+ *
4
+ * 统一入口:autosnippet_guard
5
+ * 无参数 → review 模式(自动 git diff 增量文件 + inline recipe)
6
+ * files: string[] → 指定文件检查(+ inline recipe)
7
+ * code: string → 单文件内联检查
4
8
  */
5
9
 
6
10
  import fs from 'node:fs';
7
11
  import path from 'node:path';
12
+ import { execSync } from 'node:child_process';
8
13
  import { envelope } from '../envelope.js';
9
14
 
15
+ // ═══ Review 轮次追踪(模块私有) ═══════════════════
16
+
17
+ const _reviewRounds = new Map(); // projectRoot → round count
18
+ const _lastReviewPassed = new Map(); // projectRoot → boolean
19
+ const MAX_REVIEW_ROUNDS = 5;
20
+
10
21
  export async function guardCheck(ctx, args) {
11
22
  const { GuardCheckEngine, detectLanguage } = await import(
12
23
  '../../../service/guard/GuardCheckEngine.js'
@@ -81,11 +92,21 @@ export async function guardAuditFiles(ctx, args) {
81
92
  // 注入 Enhancement Pack Guard 规则
82
93
  await _injectEnhancementGuardRules(engine, ctx);
83
94
 
95
+ // 解析项目根路径(用于相对路径转绝对路径)
96
+ const projectRoot =
97
+ ctx.container?.singletons?._projectRoot ||
98
+ process.env.ASD_PROJECT_DIR ||
99
+ process.cwd();
100
+
84
101
  // 补充缺失的 content(从磁盘读取)
85
- const filesToAudit = args.files.map((f) => ({
86
- path: f.path,
87
- content: f.content || (fs.existsSync(f.path) ? fs.readFileSync(f.path, 'utf8') : ''),
88
- }));
102
+ // 相对路径自动转绝对路径,避免 MCP 进程 cwd 不在项目目录时读不到文件
103
+ const filesToAudit = args.files.map((f) => {
104
+ const absPath = path.isAbsolute(f.path) ? f.path : path.resolve(projectRoot, f.path);
105
+ return {
106
+ path: absPath,
107
+ content: f.content || (fs.existsSync(absPath) ? fs.readFileSync(absPath, 'utf8') : ''),
108
+ };
109
+ });
89
110
 
90
111
  const result = engine.auditFiles(filesToAudit, { scope });
91
112
 
@@ -131,6 +152,263 @@ export async function guardAuditFiles(ctx, args) {
131
152
  });
132
153
  }
133
154
 
155
+ // ═══ Review 模式 — 编码后质量门禁(无参数 = 自动检测) ═══
156
+
157
+ /**
158
+ * Guard Review — 编码后的代码质量检查
159
+ *
160
+ * 设计要点:
161
+ * 1. 无参数 → 自动从 git diff 检测增量文件(staged + unstaged + untracked)
162
+ * 2. files: string[] → 指定文件路径(简化,不再要求对象数组)
163
+ * 3. violations 内联 recipe 修复指南(doClause + coreCode)
164
+ * 4. 防无限循环:reviewRound 计数 + MAX_REVIEW_ROUNDS 限制
165
+ * 5. 不绑定 task ID — 代码检查独立于任务系统
166
+ *
167
+ * @param {object} ctx — MCP context with container
168
+ * @param {object} args — { files?: string[] }
169
+ */
170
+ export async function guardReview(ctx, args) {
171
+ const { GuardCheckEngine, detectLanguage } = await import(
172
+ '../../../service/guard/GuardCheckEngine.js'
173
+ );
174
+
175
+ const projectRoot = _getProjectRoot(ctx);
176
+
177
+ // 轮次追踪(基于 projectRoot,不绑定 task)
178
+ const round = (_reviewRounds.get(projectRoot) || 0) + 1;
179
+ _reviewRounds.set(projectRoot, round);
180
+
181
+ if (round > MAX_REVIEW_ROUNDS) {
182
+ _reviewRounds.delete(projectRoot);
183
+ _lastReviewPassed.set(projectRoot, true); // 强制通过
184
+ return envelope({
185
+ success: true,
186
+ data: { passed: true, files: [], totalViolations: 0, reviewRound: round, maxRoundsReached: true },
187
+ message: `⚠️ Guard review round ${round} exceeds max ${MAX_REVIEW_ROUNDS}. Force-passing. Remaining issues should be tracked as follow-up.`,
188
+ meta: { tool: 'autosnippet_guard', mode: 'review' },
189
+ });
190
+ }
191
+
192
+ // 1. 确定待检查文件
193
+ let filePaths = [];
194
+ let fileSource = 'git-diff';
195
+
196
+ if (args.files && Array.isArray(args.files) && args.files.length > 0) {
197
+ // files 参数: string[] — 简化版,自动读取文件内容
198
+ filePaths = args.files
199
+ .map(f => typeof f === 'string' ? f : (f.path || f))
200
+ .map(f => path.isAbsolute(f) ? f : path.resolve(projectRoot, f))
201
+ .filter(f => fs.existsSync(f));
202
+ fileSource = 'explicit';
203
+ } else {
204
+ // 无参数 → 自动检测 git 变更文件
205
+ filePaths = _detectChangedFiles(projectRoot);
206
+ }
207
+
208
+ if (!filePaths.length) {
209
+ _reviewRounds.delete(projectRoot);
210
+ _lastReviewPassed.set(projectRoot, true);
211
+ return envelope({
212
+ success: true,
213
+ data: { passed: true, files: [], totalViolations: 0, reviewRound: round, fileSource },
214
+ message: '✅ No changed source files detected. Guard review passed.',
215
+ meta: { tool: 'autosnippet_guard', mode: 'review' },
216
+ });
217
+ }
218
+
219
+ // 2. 预加载 rule recipe 缓存
220
+ const recipeMap = _loadRuleRecipes(ctx);
221
+
222
+ // 3. 创建引擎,注入 Enhancement Pack
223
+ const engine = _getOrCreateEngine(ctx, GuardCheckEngine);
224
+ await _injectEnhancementGuardRules(engine, ctx);
225
+
226
+ // 4. 逐文件检查
227
+ const results = [];
228
+ let totalViolations = 0;
229
+ let totalErrors = 0;
230
+ let totalWarnings = 0;
231
+
232
+ for (const fp of filePaths) {
233
+ try {
234
+ const code = fs.readFileSync(fp, 'utf8');
235
+ const lang = detectLanguage(fp);
236
+ const violations = engine.checkCode(code, lang, { filePath: fp });
237
+
238
+ const fileSummary = {
239
+ total: violations.length,
240
+ errors: violations.filter(v => v.severity === 'error').length,
241
+ warnings: violations.filter(v => v.severity === 'warning').length,
242
+ };
243
+
244
+ totalViolations += violations.length;
245
+ totalErrors += fileSummary.errors;
246
+ totalWarnings += fileSummary.warnings;
247
+
248
+ // 内联 recipe 修复指南
249
+ const enriched = violations.map(v => {
250
+ const base = {
251
+ ruleId: v.ruleId,
252
+ message: v.message,
253
+ severity: v.severity,
254
+ line: v.line,
255
+ snippet: v.snippet,
256
+ fixSuggestion: v.fixSuggestion || null,
257
+ };
258
+ const recipe = recipeMap.get(v.ruleId);
259
+ if (recipe) {
260
+ base.recipe = {
261
+ title: recipe.title,
262
+ doClause: recipe.doClause || null,
263
+ dontClause: recipe.dontClause || null,
264
+ coreCode: recipe.coreCode || null,
265
+ };
266
+ }
267
+ return base;
268
+ });
269
+
270
+ results.push({ filePath: fp, language: lang, violations: enriched, summary: fileSummary });
271
+ } catch (err) {
272
+ results.push({
273
+ filePath: fp, error: `Cannot read: ${err.message}`,
274
+ violations: [], summary: { total: 0, errors: 0, warnings: 0 },
275
+ });
276
+ }
277
+ }
278
+
279
+ const passed = totalViolations === 0;
280
+
281
+ // 5. 更新共享状态
282
+ if (passed) {
283
+ _reviewRounds.delete(projectRoot);
284
+ _lastReviewPassed.set(projectRoot, true);
285
+ } else {
286
+ _lastReviewPassed.set(projectRoot, false);
287
+ }
288
+
289
+ // 6. 写入 ViolationsStore
290
+ try {
291
+ const violationsStore = ctx.container.get('violationsStore');
292
+ for (const r of results) {
293
+ if (r.violations.length > 0) {
294
+ violationsStore.appendRun({
295
+ filePath: r.filePath, violations: r.violations,
296
+ summary: `guard review round ${round}: ${r.summary.errors}E ${r.summary.warnings}W`,
297
+ });
298
+ }
299
+ }
300
+ } catch { /* optional */ }
301
+
302
+ // 7. 构造消息
303
+ let message;
304
+ if (passed) {
305
+ message = `✅ Guard review passed (round ${round}). ${filePaths.length} file(s) checked, 0 violations.`;
306
+ } else {
307
+ const violatingFiles = results.filter(r => r.violations.length > 0);
308
+ const details = violatingFiles.map(f =>
309
+ ` ${path.basename(f.filePath)}: ${f.violations.map(v => `L${v.line} ${v.ruleId}`).join(', ')}`
310
+ ).join('\n');
311
+
312
+ message = [
313
+ `⚠️ Guard review round ${round}: ${totalViolations} violation(s) in ${violatingFiles.length} file(s).`,
314
+ details,
315
+ '',
316
+ 'Each violation includes inline `recipe` with doClause + coreCode — apply fixes directly.',
317
+ round >= MAX_REVIEW_ROUNDS - 1
318
+ ? `⚠️ Next round is the last (max ${MAX_REVIEW_ROUNDS}). Unresolved issues will be force-passed.`
319
+ : `Fix and call autosnippet_guard again (round ${round + 1}).`,
320
+ ].join('\n');
321
+ }
322
+
323
+ return envelope({
324
+ success: true,
325
+ data: {
326
+ passed,
327
+ reviewRound: round,
328
+ fileSource,
329
+ files: results,
330
+ totalViolations,
331
+ summary: { total: totalViolations, errors: totalErrors, warnings: totalWarnings, filesChecked: filePaths.length },
332
+ },
333
+ message,
334
+ meta: { tool: 'autosnippet_guard', mode: 'review' },
335
+ });
336
+ }
337
+
338
+ // ═══ Recipe 缓存 ═════════════════════════════════════════
339
+
340
+ /**
341
+ * 预加载所有 rule 类型 recipe 的修复字段
342
+ * 构建 guardId → recipe 映射
343
+ */
344
+ function _loadRuleRecipes(ctx) {
345
+ const map = new Map();
346
+ try {
347
+ const db = typeof ctx.container.get('database')?.getDb === 'function'
348
+ ? ctx.container.get('database').getDb()
349
+ : ctx.container.get('database');
350
+
351
+ const rows = db.prepare(`
352
+ SELECT id, title, doClause, dontClause, coreCode, constraints
353
+ FROM knowledge_entries
354
+ WHERE (kind = 'rule' OR knowledgeType = 'boundary-constraint')
355
+ AND lifecycle = 'active'
356
+ `).all();
357
+
358
+ for (const row of rows) {
359
+ try {
360
+ const constraints = JSON.parse(row.constraints || '{}');
361
+ const guards = constraints.guards || [];
362
+ for (const g of guards) {
363
+ if (g.id) {
364
+ map.set(g.id, { title: row.title, doClause: row.doClause, dontClause: row.dontClause, coreCode: row.coreCode });
365
+ }
366
+ }
367
+ } catch { /* skip */ }
368
+ map.set(row.id, { title: row.title, doClause: row.doClause, dontClause: row.dontClause, coreCode: row.coreCode });
369
+ }
370
+ } catch { /* DB not available */ }
371
+ return map;
372
+ }
373
+
374
+ // ═══ Git Diff 检测 ═══════════════════════════════════════
375
+
376
+ function _getProjectRoot(ctx) {
377
+ const root = ctx.container?.singletons?._projectRoot;
378
+ if (root) return root;
379
+ return process.env.ASD_PROJECT_DIR || process.cwd();
380
+ }
381
+
382
+ const SOURCE_EXTS = new Set([
383
+ '.m', '.mm', '.h', '.swift',
384
+ '.js', '.ts', '.jsx', '.tsx',
385
+ '.py', '.rb', '.java', '.kt', '.go', '.rs',
386
+ '.c', '.cpp', '.cc', '.cs',
387
+ '.vue', '.svelte',
388
+ ]);
389
+
390
+ function _detectChangedFiles(projectRoot) {
391
+ const root = projectRoot || process.env.ASD_PROJECT_DIR || process.cwd();
392
+ try {
393
+ const diffOutput = execSync(
394
+ 'git diff --name-only HEAD 2>/dev/null; git diff --staged --name-only 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null',
395
+ { cwd: root, encoding: 'utf8', timeout: 5000 }
396
+ );
397
+ const files = [...new Set(
398
+ diffOutput.split('\n')
399
+ .map(f => f.trim())
400
+ .filter(f => f && SOURCE_EXTS.has(path.extname(f).toLowerCase()))
401
+ )];
402
+ return files
403
+ .map(f => path.isAbsolute(f) ? f : path.resolve(root, f))
404
+ .filter(f => fs.existsSync(f));
405
+ } catch {
406
+ return [];
407
+ }
408
+ }
409
+
410
+ // ═══ 项目扫描 ════════════════════════════════════════════
411
+
134
412
  export async function scanProject(ctx, args) {
135
413
  const maxFiles = args.maxFiles || 200;
136
414
  const includeContent = args.includeContent || false;