@coreyuan/vector-mind 1.0.42 → 1.0.48

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.
package/dist/index.js CHANGED
@@ -14,9 +14,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
14
14
  import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
15
15
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
16
16
  import { BUILTIN_CONVENTIONS } from "./builtin-conventions.js";
17
- import { BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS, BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS, BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS, BUILTIN_GIT_COMMIT_SUMMARY_INSTRUCTIONS, BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS, BUILTIN_PAYLOAD_GUARD_INSTRUCTIONS, BUILTIN_PLAN_LITE_INSTRUCTIONS, BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS, BUILTIN_WRITE_POLICY_INSTRUCTIONS, } from "./builtin-instructions.js";
17
+ import { BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS, BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS, BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS, BUILTIN_GIT_COMMIT_SUMMARY_INSTRUCTIONS, BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS, BUILTIN_PAYLOAD_GUARD_INSTRUCTIONS, BUILTIN_PLAN_LITE_INSTRUCTIONS, BUILTIN_REQUIREMENT_BOUNDARY_AND_MODULARITY_INSTRUCTIONS, BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS, BUILTIN_WRITE_POLICY_INSTRUCTIONS, } from "./builtin-instructions.js";
18
18
  const SERVER_NAME = "vector-mind";
19
- const SERVER_VERSION = "1.0.42";
19
+ const SERVER_VERSION = "1.0.48";
20
20
  const rootFromEnv = process.env.VECTORMIND_ROOT?.trim() ?? "";
21
21
  const prettyJsonOutput = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_PRETTY_JSON ?? "").trim().toLowerCase());
22
22
  const debugLogEnabled = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_DEBUG_LOG ?? "").trim().toLowerCase());
@@ -65,6 +65,42 @@ const PENDING_PRUNE_EVERY = (() => {
65
65
  return 500;
66
66
  return n;
67
67
  })();
68
+ const DEVELOPMENT_WARN_FILE_LINES = (() => {
69
+ const raw = process.env.VECTORMIND_WARN_FILE_LINES?.trim();
70
+ if (!raw)
71
+ return 800;
72
+ const n = Number.parseInt(raw, 10);
73
+ if (!Number.isFinite(n) || n < 100)
74
+ return 800;
75
+ return Math.min(50_000, n);
76
+ })();
77
+ const DEVELOPMENT_BLOCK_FILE_LINES = (() => {
78
+ const raw = process.env.VECTORMIND_BLOCK_FILE_LINES?.trim();
79
+ if (!raw)
80
+ return 1200;
81
+ const n = Number.parseInt(raw, 10);
82
+ if (!Number.isFinite(n) || n < DEVELOPMENT_WARN_FILE_LINES)
83
+ return Math.max(1200, DEVELOPMENT_WARN_FILE_LINES);
84
+ return Math.min(100_000, n);
85
+ })();
86
+ const DEVELOPMENT_WARN_FILE_BYTES = (() => {
87
+ const raw = process.env.VECTORMIND_WARN_FILE_BYTES?.trim();
88
+ if (!raw)
89
+ return 120_000;
90
+ const n = Number.parseInt(raw, 10);
91
+ if (!Number.isFinite(n) || n < 10_000)
92
+ return 120_000;
93
+ return Math.min(20_000_000, n);
94
+ })();
95
+ const DEVELOPMENT_WARN_PENDING_FILES = (() => {
96
+ const raw = process.env.VECTORMIND_WARN_PENDING_FILES?.trim();
97
+ if (!raw)
98
+ return 12;
99
+ const n = Number.parseInt(raw, 10);
100
+ if (!Number.isFinite(n) || n < 1)
101
+ return 12;
102
+ return Math.min(500, n);
103
+ })();
68
104
  const RIPGREP_RESOLVE_TIMEOUT_MS = 5_000;
69
105
  const RIPGREP_SEARCH_TIMEOUT_MS = 30_000;
70
106
  const RIPGREP_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
@@ -122,6 +158,48 @@ const INDEX_AUTO_PRUNE_IGNORED = (() => {
122
158
  return true;
123
159
  return ["1", "true", "on", "yes"].includes(raw);
124
160
  })();
161
+ const MAINTENANCE_AUTO_ENABLED = (() => {
162
+ const raw = (process.env.VECTORMIND_MAINTENANCE_AUTO ?? "").trim().toLowerCase();
163
+ if (!raw)
164
+ return true;
165
+ return ["1", "true", "on", "yes"].includes(raw);
166
+ })();
167
+ const MAINTENANCE_INTERVAL_HOURS = (() => {
168
+ const raw = process.env.VECTORMIND_MAINTENANCE_INTERVAL_HOURS?.trim();
169
+ if (!raw)
170
+ return 24;
171
+ const n = Number.parseInt(raw, 10);
172
+ if (!Number.isFinite(n) || n < 1)
173
+ return 24;
174
+ return Math.min(24 * 30, n);
175
+ })();
176
+ const MAINTENANCE_COMPACT_AFTER_DAYS = (() => {
177
+ const raw = process.env.VECTORMIND_COMPACT_AFTER_DAYS?.trim();
178
+ if (!raw)
179
+ return 45;
180
+ const n = Number.parseInt(raw, 10);
181
+ if (!Number.isFinite(n) || n < 1)
182
+ return 45;
183
+ return Math.min(3650, n);
184
+ })();
185
+ const MAINTENANCE_MAX_MEMORY_ITEMS = (() => {
186
+ const raw = process.env.VECTORMIND_MAINTENANCE_MAX_MEMORY_ITEMS?.trim();
187
+ if (!raw)
188
+ return 250;
189
+ const n = Number.parseInt(raw, 10);
190
+ if (!Number.isFinite(n) || n < 1)
191
+ return 250;
192
+ return Math.min(5000, n);
193
+ })();
194
+ const MAINTENANCE_MAX_INDEX_FILES = (() => {
195
+ const raw = process.env.VECTORMIND_MAINTENANCE_MAX_INDEX_FILES?.trim();
196
+ if (!raw)
197
+ return 1500;
198
+ const n = Number.parseInt(raw, 10);
199
+ if (!Number.isFinite(n) || n < 1)
200
+ return 1500;
201
+ return Math.min(50_000, n);
202
+ })();
125
203
  const ROOTS_LIST_TIMEOUT_MS = (() => {
126
204
  const raw = process.env.VECTORMIND_ROOTS_TIMEOUT_MS?.trim();
127
205
  if (!raw)
@@ -140,6 +218,15 @@ const BOOTSTRAP_SEMANTIC_TIMEOUT_MS = (() => {
140
218
  return 2500;
141
219
  return n;
142
220
  })();
221
+ const SEMANTIC_EMBEDDINGS_TIMEOUT_MS = (() => {
222
+ const raw = process.env.VECTORMIND_EMBEDDINGS_TIMEOUT_MS?.trim();
223
+ if (!raw)
224
+ return 1500;
225
+ const n = Number.parseInt(raw, 10);
226
+ if (!Number.isFinite(n) || n <= 0)
227
+ return 1500;
228
+ return n;
229
+ })();
143
230
  let initialized = false;
144
231
  let rootSource = "cwd";
145
232
  let projectRoot = "";
@@ -150,6 +237,7 @@ let watcherReady = false;
150
237
  let initializationPromise = null;
151
238
  let insertRequirementStmt;
152
239
  let getActiveRequirementStmt;
240
+ let listActiveRequirementsStmt;
153
241
  let listRecentRequirementsStmt;
154
242
  let completeAllActiveRequirementsStmt;
155
243
  let completeRequirementByIdStmt;
@@ -170,6 +258,7 @@ let listCurrentDecisionsStmt;
170
258
  let upsertProjectSummaryStmt;
171
259
  let getProjectSummaryStmt;
172
260
  let listRecentNotesStmt;
261
+ let listRecentContextItemsStmt;
173
262
  let getLatestChangeIntentForFileStmt;
174
263
  let deleteFileChunkItemsStmt;
175
264
  let getEmbeddingMetaStmt;
@@ -189,6 +278,8 @@ let insertTokenSavingsStmt;
189
278
  let summarizeTokenSavingsStmt;
190
279
  let summarizeTokenSavingsByToolStmt;
191
280
  let listRecentTokenSavingsStmt;
281
+ let getKvStmt;
282
+ let setKvStmt;
192
283
  let indexFileSymbolsTx = null;
193
284
  let activitySeq = 0;
194
285
  const activityLog = [];
@@ -284,6 +375,8 @@ function summarizeActivityEvent(e) {
284
375
  return `sync_change_intent #${String(d.req_id ?? "")} files=${String(d.files_total ?? "")}`;
285
376
  case "complete_requirement":
286
377
  return `complete_requirement ${String(d.all_active ? "all_active" : d.req_id ?? "")}`;
378
+ case "memory_maintenance":
379
+ return `memory_maintenance trigger=${String(d.trigger ?? "")} compacted=${String(d.compacted ?? "")} stale=${String(d.stale_files ?? "")} chunks_deleted=${String(d.chunks_deleted ?? "")}`;
287
380
  default:
288
381
  return e.type;
289
382
  }
@@ -723,9 +816,9 @@ function pruneFilenameNoiseIndexes() {
723
816
  return { chunks_deleted: 0, symbols_deleted: 0 };
724
817
  try {
725
818
  const suffixWhere = NOISE_FILE_SUFFIXES.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
726
- const baseWhere = NOISE_FILE_BASENAMES.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
819
+ const baseWhere = NOISE_FILE_BASENAMES.map(() => "(LOWER(file_path) = ? OR LOWER(file_path) LIKE ?)").join(" OR ");
727
820
  const suffixArgs = NOISE_FILE_SUFFIXES.map((s) => `%${s}`);
728
- const baseArgs = NOISE_FILE_BASENAMES.map((n) => `%/${n}`);
821
+ const baseArgs = NOISE_FILE_BASENAMES.flatMap((n) => [n, `%/${n}`]);
729
822
  const whereParts = [];
730
823
  const args = [];
731
824
  if (suffixWhere) {
@@ -764,6 +857,446 @@ function pruneFilenameNoiseIndexes() {
764
857
  return { chunks_deleted: 0, symbols_deleted: 0 };
765
858
  }
766
859
  }
860
+ function kvGet(key) {
861
+ try {
862
+ const row = getKvStmt?.get(key);
863
+ return row?.value ?? null;
864
+ }
865
+ catch {
866
+ return null;
867
+ }
868
+ }
869
+ function kvSet(key, value) {
870
+ try {
871
+ setKvStmt?.run(key, value);
872
+ }
873
+ catch (err) {
874
+ console.error("[vectormind] kv set failed:", err);
875
+ }
876
+ }
877
+ function distinctChunkAndSymbolFilePaths(limit) {
878
+ const rows = db
879
+ .prepare(`SELECT file_path
880
+ FROM (
881
+ SELECT file_path, MAX(updated_at) AS updated_at
882
+ FROM memory_items
883
+ WHERE file_path IS NOT NULL
884
+ AND (kind = 'code_chunk' OR kind = 'doc_chunk')
885
+ GROUP BY file_path
886
+ UNION
887
+ SELECT file_path, CURRENT_TIMESTAMP AS updated_at
888
+ FROM symbols
889
+ WHERE file_path IS NOT NULL
890
+ GROUP BY file_path
891
+ )
892
+ WHERE file_path IS NOT NULL
893
+ ORDER BY updated_at ASC
894
+ LIMIT ?`)
895
+ .all(limit);
896
+ return Array.from(new Set(rows.map((r) => r.file_path).filter(Boolean)));
897
+ }
898
+ function classifyStaleIndexFile(filePath) {
899
+ if (!filePath)
900
+ return "empty_path";
901
+ if (shouldIgnoreDbFilePath(filePath))
902
+ return "ignored_path";
903
+ if (shouldIgnoreContentFile(filePath))
904
+ return "filename_noise";
905
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath);
906
+ const rel = path.relative(projectRoot, absPath);
907
+ if (rel.startsWith("..") || path.isAbsolute(rel))
908
+ return "outside_project";
909
+ let stat;
910
+ try {
911
+ stat = fs.statSync(absPath);
912
+ }
913
+ catch {
914
+ return "missing_file";
915
+ }
916
+ if (!stat.isFile())
917
+ return "not_file";
918
+ if (!isContentIndexableFile(absPath) && !isSymbolIndexableFile(absPath))
919
+ return "not_indexable";
920
+ return null;
921
+ }
922
+ function pruneStaleFileIndexes(opts) {
923
+ const filePaths = distinctChunkAndSymbolFilePaths(Math.min(50_000, opts.maxIndexFiles * 3));
924
+ const matched = [];
925
+ for (const fp of filePaths) {
926
+ if (matched.length >= opts.maxIndexFiles)
927
+ break;
928
+ const reason = classifyStaleIndexFile(fp);
929
+ if (reason)
930
+ matched.push({ file_path: fp, reason });
931
+ }
932
+ let chunksDeleted = 0;
933
+ let symbolsDeleted = 0;
934
+ const samples = matched.slice(0, 20).map((m) => `${m.file_path} (${m.reason})`);
935
+ if (!opts.dryRun && matched.length) {
936
+ const tx = db.transaction(() => {
937
+ for (const m of matched) {
938
+ chunksDeleted += deleteFileChunkItemsStmt.run(m.file_path).changes;
939
+ symbolsDeleted += deleteSymbolsForFileStmt.run(m.file_path).changes;
940
+ }
941
+ });
942
+ try {
943
+ tx();
944
+ }
945
+ catch (err) {
946
+ console.error("[vectormind] prune stale indexes failed:", err);
947
+ }
948
+ }
949
+ else if (opts.dryRun && matched.length) {
950
+ const countChunksStmt = db.prepare(`SELECT COUNT(1) AS c
951
+ FROM memory_items
952
+ WHERE file_path = ?
953
+ AND (kind = 'code_chunk' OR kind = 'doc_chunk')`);
954
+ const countSymbolsStmt = db.prepare(`SELECT COUNT(1) AS c FROM symbols WHERE file_path = ?`);
955
+ for (const m of matched) {
956
+ chunksDeleted += Number(countChunksStmt.get(m.file_path)?.c ?? 0);
957
+ symbolsDeleted += Number(countSymbolsStmt.get(m.file_path)?.c ?? 0);
958
+ }
959
+ }
960
+ if (!opts.dryRun && (chunksDeleted || symbolsDeleted)) {
961
+ logActivity("index_prune", {
962
+ reason: "stale_files",
963
+ files_matched: matched.length,
964
+ chunks_deleted: chunksDeleted,
965
+ symbols_deleted: symbolsDeleted,
966
+ samples,
967
+ });
968
+ }
969
+ return {
970
+ files_checked: filePaths.length,
971
+ files_matched: matched.length,
972
+ chunks_deleted: chunksDeleted,
973
+ symbols_deleted: symbolsDeleted,
974
+ samples,
975
+ };
976
+ }
977
+ function countIgnoredIndexDeletes() {
978
+ if (!IGNORED_LIKE_PATTERNS.length)
979
+ return { chunks_deleted: 0, symbols_deleted: 0 };
980
+ const where = IGNORED_LIKE_PATTERNS
981
+ .map(() => "LOWER(REPLACE(file_path, '\\\\', '/')) LIKE ?")
982
+ .join(" OR ");
983
+ const chunksDeleted = Number(db
984
+ .prepare(`SELECT COUNT(1) AS c
985
+ FROM memory_items
986
+ WHERE file_path IS NOT NULL
987
+ AND (kind = 'code_chunk' OR kind = 'doc_chunk')
988
+ AND (${where})`)
989
+ .get(...IGNORED_LIKE_PATTERNS)?.c ?? 0);
990
+ const symbolsDeleted = Number(db
991
+ .prepare(`SELECT COUNT(1) AS c
992
+ FROM symbols
993
+ WHERE file_path IS NOT NULL
994
+ AND (${where})`)
995
+ .get(...IGNORED_LIKE_PATTERNS)?.c ?? 0);
996
+ return { chunks_deleted: chunksDeleted, symbols_deleted: symbolsDeleted };
997
+ }
998
+ function countFilenameNoiseIndexDeletes() {
999
+ const suffixWhere = NOISE_FILE_SUFFIXES.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
1000
+ const baseWhere = NOISE_FILE_BASENAMES.map(() => "(LOWER(file_path) = ? OR LOWER(file_path) LIKE ?)").join(" OR ");
1001
+ const suffixArgs = NOISE_FILE_SUFFIXES.map((s) => `%${s}`);
1002
+ const baseArgs = NOISE_FILE_BASENAMES.flatMap((n) => [n, `%/${n}`]);
1003
+ const whereParts = [];
1004
+ const args = [];
1005
+ if (suffixWhere) {
1006
+ whereParts.push(`(${suffixWhere})`);
1007
+ args.push(...suffixArgs);
1008
+ }
1009
+ if (baseWhere) {
1010
+ whereParts.push(`(${baseWhere})`);
1011
+ args.push(...baseArgs);
1012
+ }
1013
+ if (!whereParts.length)
1014
+ return { chunks_deleted: 0, symbols_deleted: 0 };
1015
+ const where = whereParts.join(" OR ");
1016
+ const chunksDeleted = Number(db
1017
+ .prepare(`SELECT COUNT(1) AS c
1018
+ FROM memory_items
1019
+ WHERE file_path IS NOT NULL
1020
+ AND (kind = 'code_chunk' OR kind = 'doc_chunk')
1021
+ AND (${where})`)
1022
+ .get(...args)?.c ?? 0);
1023
+ const symbolsDeleted = Number(db
1024
+ .prepare(`SELECT COUNT(1) AS c
1025
+ FROM symbols
1026
+ WHERE file_path IS NOT NULL
1027
+ AND (${where})`)
1028
+ .get(...args)?.c ?? 0);
1029
+ return { chunks_deleted: chunksDeleted, symbols_deleted: symbolsDeleted };
1030
+ }
1031
+ function hiddenEmbeddingIds(limit = 10_000) {
1032
+ const rows = db
1033
+ .prepare(`SELECT e.memory_id AS memory_id, m.metadata_json AS metadata_json
1034
+ FROM embeddings e
1035
+ JOIN memory_items m ON m.id = e.memory_id
1036
+ WHERE m.metadata_json LIKE '%compacted%'
1037
+ OR m.metadata_json LIKE '%superseded%'
1038
+ LIMIT ?`)
1039
+ .all(limit);
1040
+ return rows
1041
+ .filter((r) => isHiddenFromDefaultRecall({ metadata_json: r.metadata_json }))
1042
+ .map((r) => r.memory_id);
1043
+ }
1044
+ function pruneHiddenEmbeddings(dryRun) {
1045
+ const ids = hiddenEmbeddingIds();
1046
+ if (!ids.length)
1047
+ return { embeddings_deleted: 0 };
1048
+ if (!dryRun) {
1049
+ const deleteStmt = db.prepare(`DELETE FROM embeddings WHERE memory_id = ?`);
1050
+ const tx = db.transaction(() => {
1051
+ for (const id of ids)
1052
+ deleteStmt.run(id);
1053
+ });
1054
+ try {
1055
+ tx();
1056
+ }
1057
+ catch (err) {
1058
+ console.error("[vectormind] prune hidden embeddings failed:", err);
1059
+ }
1060
+ }
1061
+ return { embeddings_deleted: ids.length };
1062
+ }
1063
+ function selectCompactionCandidates(opts) {
1064
+ const kinds = opts.compactNotes
1065
+ ? ["requirement", "change_intent", "note"]
1066
+ : ["requirement", "change_intent"];
1067
+ const placeholders = kinds.map(() => "?").join(", ");
1068
+ const rows = db
1069
+ .prepare(`SELECT
1070
+ m.id, m.kind, m.title, m.content, m.file_path, m.start_line, m.end_line,
1071
+ m.req_id, m.metadata_json, m.content_hash, m.created_at, m.updated_at,
1072
+ r.status AS req_status
1073
+ FROM memory_items m
1074
+ LEFT JOIN requirements r ON r.id = m.req_id
1075
+ WHERE m.kind IN (${placeholders})
1076
+ AND m.updated_at < datetime('now', ?)
1077
+ ORDER BY m.updated_at ASC, m.id ASC
1078
+ LIMIT ?`)
1079
+ .all(...kinds, `-${opts.compactAfterDays} days`, Math.min(20_000, opts.maxMemoryItems * 5));
1080
+ return rows
1081
+ .filter((row) => !isHiddenFromDefaultRecall(row))
1082
+ .filter((row) => metadataStatus(row) !== "current" && metadataStatus(row) !== "active")
1083
+ .filter((row) => row.req_status !== "active")
1084
+ .filter((row) => row.kind !== "note" || opts.compactNotes)
1085
+ .slice(0, opts.maxMemoryItems);
1086
+ }
1087
+ function compactionLine(row) {
1088
+ const date = oneLine(row.updated_at || row.created_at, 19);
1089
+ const title = row.title ? ` ${oneLine(row.title, 80)}` : "";
1090
+ const file = row.file_path ? ` file=${row.file_path}${row.start_line != null ? `:${row.start_line}` : ""}` : "";
1091
+ const req = row.req_id != null ? ` req#${row.req_id}` : "";
1092
+ return `- ${date} #${row.id} ${row.kind}${req}${file}${title}: ${oneLine(row.content, 220)}`;
1093
+ }
1094
+ function compactOldMemoryItems(opts) {
1095
+ const candidates = selectCompactionCandidates(opts);
1096
+ const cutoff = new Date(Date.now() - opts.compactAfterDays * 86_400_000).toISOString();
1097
+ const samples = candidates.slice(0, 20).map((row) => ({
1098
+ id: row.id,
1099
+ kind: row.kind,
1100
+ title: row.title,
1101
+ file_path: row.file_path,
1102
+ updated_at: row.updated_at,
1103
+ }));
1104
+ if (opts.dryRun || !candidates.length) {
1105
+ return {
1106
+ cutoff,
1107
+ candidates: candidates.length,
1108
+ compacted: 0,
1109
+ summary_memory_id: null,
1110
+ archived: 0,
1111
+ samples,
1112
+ };
1113
+ }
1114
+ const now = new Date().toISOString();
1115
+ const lines = [
1116
+ `Auto-compacted ${candidates.length} old VectorMind memory items.`,
1117
+ `Cutoff: items updated before ${cutoff} (${opts.compactAfterDays} days).`,
1118
+ "",
1119
+ "This compact summary keeps old history searchable while detailed stale items are hidden from default recall.",
1120
+ "Durable decisions, conventions, and project summaries are never compacted by this automatic pass.",
1121
+ "",
1122
+ ...candidates.map(compactionLine),
1123
+ ];
1124
+ const content = lines.join("\n");
1125
+ const title = `Memory compaction ${now.slice(0, 10)}`;
1126
+ const metadata = {
1127
+ source: "maintenance",
1128
+ status: "current",
1129
+ compacted_item_ids: candidates.map((c) => c.id),
1130
+ compact_after_days: opts.compactAfterDays,
1131
+ compact_notes: opts.compactNotes,
1132
+ generated_at: now,
1133
+ };
1134
+ let summaryMemoryId = 0;
1135
+ let archived = 0;
1136
+ const archiveStmt = db.prepare(`INSERT OR IGNORE INTO memory_item_archive
1137
+ (memory_id, original_kind, original_title, original_content, original_file_path,
1138
+ original_start_line, original_end_line, original_req_id, original_metadata_json,
1139
+ original_content_hash, original_created_at, original_updated_at, archive_reason, compacted_into_id)
1140
+ VALUES
1141
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
1142
+ const updateMemoryStmt = db.prepare(`UPDATE memory_items
1143
+ SET content = ?, metadata_json = ?, content_hash = ?, updated_at = CURRENT_TIMESTAMP
1144
+ WHERE id = ?`);
1145
+ const deleteEmbeddingStmt = db.prepare(`DELETE FROM embeddings WHERE memory_id = ?`);
1146
+ const tx = db.transaction(() => {
1147
+ const info = insertMemoryItemStmt.run("memory_compaction", title, content, null, null, null, null, safeJson(metadata), sha256Hex(content));
1148
+ summaryMemoryId = Number(info.lastInsertRowid);
1149
+ for (const row of candidates) {
1150
+ const archiveInfo = archiveStmt.run(row.id, row.kind, row.title, row.content, row.file_path, row.start_line, row.end_line, row.req_id, row.metadata_json, row.content_hash, row.created_at, row.updated_at, "auto_compaction", summaryMemoryId);
1151
+ if (archiveInfo.changes > 0)
1152
+ archived += 1;
1153
+ const patchedMeta = {
1154
+ ...parseMetadataJson(row.metadata_json),
1155
+ status: "compacted",
1156
+ compacted: true,
1157
+ compacted_at: now,
1158
+ compacted_into_memory_id: summaryMemoryId,
1159
+ };
1160
+ const stub = [
1161
+ `[compacted into memory item #${summaryMemoryId}]`,
1162
+ `Original ${row.kind} #${row.id} was older than ${opts.compactAfterDays} days and is excluded from default recall.`,
1163
+ `Summary: ${oneLine(row.title || row.content, 260)}`,
1164
+ ].join("\n");
1165
+ updateMemoryStmt.run(stub, safeJson(patchedMeta), sha256Hex(stub), row.id);
1166
+ deleteEmbeddingStmt.run(row.id);
1167
+ }
1168
+ });
1169
+ try {
1170
+ tx();
1171
+ if (summaryMemoryId)
1172
+ enqueueEmbedding(summaryMemoryId);
1173
+ }
1174
+ catch (err) {
1175
+ console.error("[vectormind] compact old memory failed:", err);
1176
+ summaryMemoryId = 0;
1177
+ }
1178
+ if (summaryMemoryId) {
1179
+ logActivity("memory_maintenance", {
1180
+ reason: "compact_old_memories",
1181
+ compacted: candidates.length,
1182
+ summary_memory_id: summaryMemoryId,
1183
+ archived,
1184
+ });
1185
+ }
1186
+ return {
1187
+ cutoff,
1188
+ candidates: candidates.length,
1189
+ compacted: summaryMemoryId ? candidates.length : 0,
1190
+ summary_memory_id: summaryMemoryId || null,
1191
+ archived,
1192
+ samples,
1193
+ };
1194
+ }
1195
+ function runMemoryMaintenance(args, trigger = "manual") {
1196
+ const compactedMemory = args.compact_old_memories
1197
+ ? compactOldMemoryItems({
1198
+ dryRun: args.dry_run,
1199
+ compactAfterDays: args.compact_after_days,
1200
+ maxMemoryItems: args.max_memory_items,
1201
+ compactNotes: args.compact_notes,
1202
+ })
1203
+ : {
1204
+ cutoff: new Date(Date.now() - args.compact_after_days * 86_400_000).toISOString(),
1205
+ candidates: 0,
1206
+ compacted: 0,
1207
+ summary_memory_id: null,
1208
+ archived: 0,
1209
+ samples: [],
1210
+ };
1211
+ const ignoredPaths = args.prune_ignored_paths
1212
+ ? args.dry_run
1213
+ ? countIgnoredIndexDeletes()
1214
+ : pruneIgnoredIndexesByPathPatterns()
1215
+ : { chunks_deleted: 0, symbols_deleted: 0 };
1216
+ const filenameNoise = args.prune_filename_noise
1217
+ ? args.dry_run
1218
+ ? countFilenameNoiseIndexDeletes()
1219
+ : pruneFilenameNoiseIndexes()
1220
+ : { chunks_deleted: 0, symbols_deleted: 0 };
1221
+ const staleFiles = args.prune_stale_indexes
1222
+ ? pruneStaleFileIndexes({ dryRun: args.dry_run, maxIndexFiles: args.max_index_files })
1223
+ : { files_checked: 0, files_matched: 0, chunks_deleted: 0, symbols_deleted: 0, samples: [] };
1224
+ const hiddenEmbeddings = args.prune_hidden_embeddings
1225
+ ? pruneHiddenEmbeddings(args.dry_run)
1226
+ : { embeddings_deleted: 0 };
1227
+ let vacuumed = false;
1228
+ if (!args.dry_run && args.vacuum) {
1229
+ try {
1230
+ db.exec("VACUUM");
1231
+ vacuumed = true;
1232
+ }
1233
+ catch (err) {
1234
+ console.error("[vectormind] maintenance vacuum failed:", err);
1235
+ }
1236
+ }
1237
+ const result = {
1238
+ ok: true,
1239
+ dry_run: args.dry_run,
1240
+ trigger,
1241
+ generated_at: new Date().toISOString(),
1242
+ project_root: projectRoot,
1243
+ db_path: dbPath,
1244
+ config: {
1245
+ compact_after_days: args.compact_after_days,
1246
+ max_memory_items: args.max_memory_items,
1247
+ max_index_files: args.max_index_files,
1248
+ compact_notes: args.compact_notes,
1249
+ },
1250
+ compacted_memory: compactedMemory,
1251
+ pruned: {
1252
+ ignored_paths: ignoredPaths,
1253
+ filename_noise: filenameNoise,
1254
+ stale_files: staleFiles,
1255
+ hidden_embeddings: hiddenEmbeddings,
1256
+ },
1257
+ vacuumed,
1258
+ };
1259
+ logActivity("memory_maintenance", {
1260
+ trigger,
1261
+ dry_run: args.dry_run,
1262
+ compacted: result.compacted_memory.compacted,
1263
+ stale_files: result.pruned.stale_files.files_matched,
1264
+ chunks_deleted: result.pruned.ignored_paths.chunks_deleted +
1265
+ result.pruned.filename_noise.chunks_deleted +
1266
+ result.pruned.stale_files.chunks_deleted,
1267
+ });
1268
+ return result;
1269
+ }
1270
+ function runAutoMaintenanceIfDue() {
1271
+ if (!MAINTENANCE_AUTO_ENABLED || !db)
1272
+ return;
1273
+ const lastRaw = kvGet("maintenance.last_auto_at");
1274
+ const last = lastRaw ? Date.parse(lastRaw) : 0;
1275
+ const dueMs = MAINTENANCE_INTERVAL_HOURS * 3_600_000;
1276
+ if (Number.isFinite(last) && last > 0 && Date.now() - last < dueMs)
1277
+ return;
1278
+ try {
1279
+ runMemoryMaintenance({
1280
+ project_root: projectRoot,
1281
+ dry_run: false,
1282
+ format: "compact",
1283
+ compact_old_memories: true,
1284
+ compact_notes: false,
1285
+ prune_stale_indexes: true,
1286
+ prune_ignored_paths: true,
1287
+ prune_filename_noise: true,
1288
+ prune_hidden_embeddings: true,
1289
+ compact_after_days: MAINTENANCE_COMPACT_AFTER_DAYS,
1290
+ max_memory_items: MAINTENANCE_MAX_MEMORY_ITEMS,
1291
+ max_index_files: MAINTENANCE_MAX_INDEX_FILES,
1292
+ vacuum: false,
1293
+ }, "auto");
1294
+ kvSet("maintenance.last_auto_at", new Date().toISOString());
1295
+ }
1296
+ catch (err) {
1297
+ console.error("[vectormind] auto maintenance failed:", err);
1298
+ }
1299
+ }
767
1300
  function shouldIgnorePath(inputPath) {
768
1301
  const normalizedAbs = path.resolve(inputPath);
769
1302
  const rel = path.relative(projectRoot, normalizedAbs);
@@ -887,6 +1420,416 @@ function isContentIndexableFile(filePath) {
887
1420
  return false;
888
1421
  return getContentChunkKind(filePath) !== null;
889
1422
  }
1423
+ function isLikelySourceImplementationFile(filePath) {
1424
+ const ext = path.extname(filePath).toLowerCase();
1425
+ return new Set([
1426
+ ".ts",
1427
+ ".tsx",
1428
+ ".js",
1429
+ ".jsx",
1430
+ ".mjs",
1431
+ ".cjs",
1432
+ ".py",
1433
+ ".go",
1434
+ ".rs",
1435
+ ".java",
1436
+ ".kt",
1437
+ ".cs",
1438
+ ".c",
1439
+ ".cc",
1440
+ ".cpp",
1441
+ ".h",
1442
+ ".hpp",
1443
+ ".vue",
1444
+ ".svelte",
1445
+ ]).has(ext);
1446
+ }
1447
+ function countFileLinesBounded(absPath, maxBytes) {
1448
+ let stat;
1449
+ try {
1450
+ stat = fs.statSync(absPath);
1451
+ }
1452
+ catch {
1453
+ return null;
1454
+ }
1455
+ if (!stat.isFile())
1456
+ return null;
1457
+ const bytesToRead = Math.min(stat.size, maxBytes);
1458
+ const fd = fs.openSync(absPath, "r");
1459
+ try {
1460
+ const buffer = Buffer.alloc(bytesToRead);
1461
+ const read = fs.readSync(fd, buffer, 0, bytesToRead, 0);
1462
+ let lines = read > 0 ? 1 : 0;
1463
+ for (let i = 0; i < read; i++) {
1464
+ if (buffer[i] === 10)
1465
+ lines += 1;
1466
+ }
1467
+ return { lines, truncated: stat.size > bytesToRead };
1468
+ }
1469
+ finally {
1470
+ fs.closeSync(fd);
1471
+ }
1472
+ }
1473
+ function isPathInsideProjectRoot(absPath) {
1474
+ const root = path.resolve(projectRoot);
1475
+ const rel = path.relative(root, path.resolve(absPath));
1476
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
1477
+ }
1478
+ function checkPathScope(inputPath) {
1479
+ const normalizedInput = inputPath.trim() || ".";
1480
+ const absPath = path.resolve(path.isAbsolute(normalizedInput) ? normalizedInput : path.join(projectRoot, normalizedInput));
1481
+ return {
1482
+ input_path: inputPath,
1483
+ abs_path: absPath,
1484
+ in_project: isPathInsideProjectRoot(absPath),
1485
+ project_root: path.resolve(projectRoot),
1486
+ };
1487
+ }
1488
+ function buildCrossProjectPathWarnings(paths) {
1489
+ const checks = (paths ?? []).map((p) => checkPathScope(p)).filter((c) => !c.in_project);
1490
+ if (!checks.length)
1491
+ return [];
1492
+ return [
1493
+ {
1494
+ code: "cross_project_path",
1495
+ severity: "warning",
1496
+ message: "A path points outside the current project_root. Switch project_root intentionally before reading/searching another repo; do not mix unrelated project context into the current requirement.",
1497
+ files: checks.slice(0, 10).map((c) => c.input_path),
1498
+ details: {
1499
+ project_root: path.resolve(projectRoot),
1500
+ paths: checks.slice(0, 10),
1501
+ total_paths: checks.length,
1502
+ },
1503
+ },
1504
+ ];
1505
+ }
1506
+ function buildFileReadDevelopmentWarnings(filePath, absPath, stat) {
1507
+ const warnings = [];
1508
+ if (!isPathInsideProjectRoot(absPath)) {
1509
+ warnings.push(...buildCrossProjectPathWarnings([filePath]));
1510
+ return warnings;
1511
+ }
1512
+ if (!isLikelySourceImplementationFile(filePath))
1513
+ return warnings;
1514
+ let st = stat;
1515
+ try {
1516
+ st ??= fs.statSync(absPath);
1517
+ }
1518
+ catch {
1519
+ return warnings;
1520
+ }
1521
+ if (!st.isFile())
1522
+ return warnings;
1523
+ const lineInfo = countFileLinesBounded(absPath, 2_000_000);
1524
+ const lineCount = lineInfo?.lines ?? 0;
1525
+ const tooManyLines = lineCount >= DEVELOPMENT_BLOCK_FILE_LINES;
1526
+ const warnLines = lineCount >= DEVELOPMENT_WARN_FILE_LINES;
1527
+ const warnBytes = st.size >= DEVELOPMENT_WARN_FILE_BYTES;
1528
+ if (!tooManyLines && !warnLines && !warnBytes)
1529
+ return warnings;
1530
+ warnings.push({
1531
+ code: "large_file_read",
1532
+ severity: tooManyLines ? "blocker" : "warning",
1533
+ message: tooManyLines
1534
+ ? "You are reading a very large implementation file. Do not keep patching new feature code into it; identify a narrow function and split new behavior into focused modules unless this task is explicitly a planned extraction."
1535
+ : "You are reading a large implementation file. Keep the target narrow and prefer extracting focused modules before adding responsibilities.",
1536
+ files: [filePath],
1537
+ details: {
1538
+ lines: lineInfo?.truncated ? `${lineCount}+` : lineCount,
1539
+ bytes: st.size,
1540
+ warn_lines: DEVELOPMENT_WARN_FILE_LINES,
1541
+ block_lines: DEVELOPMENT_BLOCK_FILE_LINES,
1542
+ warn_bytes: DEVELOPMENT_WARN_FILE_BYTES,
1543
+ },
1544
+ });
1545
+ return warnings;
1546
+ }
1547
+ function buildMatchedFileDevelopmentWarnings(filePaths) {
1548
+ const seen = new Set();
1549
+ const warnings = [];
1550
+ for (const fp of filePaths) {
1551
+ if (!fp || seen.has(fp))
1552
+ continue;
1553
+ seen.add(fp);
1554
+ const abs = path.isAbsolute(fp) ? path.resolve(fp) : path.join(projectRoot, fp);
1555
+ warnings.push(...buildFileReadDevelopmentWarnings(normalizeToDbPath(fp), abs));
1556
+ if (warnings.length >= 8)
1557
+ break;
1558
+ }
1559
+ return warnings;
1560
+ }
1561
+ function buildRequirementStartWarnings(args) {
1562
+ const warnings = [];
1563
+ const activeReqs = listActiveRequirementsStmt.all(10);
1564
+ if (!args.close_previous && activeReqs.length > 0) {
1565
+ warnings.push({
1566
+ code: "multiple_active_requirements",
1567
+ severity: "warning",
1568
+ message: "Starting a requirement without closing previous active requirements can mix unrelated context. Only keep multiple active requirements when the user explicitly asked for parallel work.",
1569
+ details: {
1570
+ active_requirements: activeReqs.slice(0, 5).map((r) => ({ id: r.id, title: r.title, status: r.status })),
1571
+ },
1572
+ });
1573
+ }
1574
+ const text = `${args.title}\n${args.background}`.toLowerCase();
1575
+ const broadTerms = [
1576
+ "顺便",
1577
+ "一起",
1578
+ "所有",
1579
+ "全部",
1580
+ "整体",
1581
+ "重构",
1582
+ "统一",
1583
+ "优化一下",
1584
+ "顺手",
1585
+ "相关的",
1586
+ "all ",
1587
+ "everything",
1588
+ "refactor",
1589
+ "cleanup",
1590
+ "clean up",
1591
+ ];
1592
+ const matched = broadTerms.filter((term) => text.includes(term));
1593
+ if (matched.length >= 2 || text.length > 1800) {
1594
+ warnings.push({
1595
+ code: "broad_requirement_scope",
1596
+ severity: "warning",
1597
+ message: "The requirement wording looks broad. Treat the current user request as the only boundary; do not add extra workflows, fields, pages, interfaces, or touch completed related features unless explicitly required.",
1598
+ details: { matched_terms: matched.slice(0, 10), text_length: text.length },
1599
+ });
1600
+ }
1601
+ return warnings;
1602
+ }
1603
+ function normalizeScopeTerms(values) {
1604
+ return Array.from(new Set((values ?? []).map((v) => v.trim()).filter(Boolean)));
1605
+ }
1606
+ function buildRequirementScopeContract(args) {
1607
+ const allowTerms = normalizeScopeTerms(args.scope_allow);
1608
+ const denyTerms = normalizeScopeTerms(args.scope_deny);
1609
+ const allowedPaths = normalizeScopeTerms(args.allowed_paths).map(normalizeToDbPath);
1610
+ const deniedPaths = normalizeScopeTerms(args.denied_paths).map((p) => p.replace(/\\/g, "/"));
1611
+ return {
1612
+ allow_terms: allowTerms,
1613
+ deny_terms: Array.from(new Set(denyTerms)),
1614
+ allowed_paths: Array.from(new Set(allowedPaths)),
1615
+ denied_paths: Array.from(new Set(deniedPaths)),
1616
+ inferred_from: [],
1617
+ };
1618
+ }
1619
+ function getRequirementScopeContract(reqId) {
1620
+ const memId = getRequirementMemoryItemIdStmt.get(reqId)?.id;
1621
+ if (memId == null)
1622
+ return null;
1623
+ const row = getMemoryItemByIdStmt.get(memId);
1624
+ const meta = parseMetadataJson(row?.metadata_json);
1625
+ const raw = meta.scope_contract;
1626
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
1627
+ return null;
1628
+ const obj = raw;
1629
+ return {
1630
+ allow_terms: Array.isArray(obj.allow_terms) ? obj.allow_terms.filter((v) => typeof v === "string") : [],
1631
+ deny_terms: Array.isArray(obj.deny_terms) ? obj.deny_terms.filter((v) => typeof v === "string") : [],
1632
+ allowed_paths: Array.isArray(obj.allowed_paths) ? obj.allowed_paths.filter((v) => typeof v === "string") : [],
1633
+ denied_paths: Array.isArray(obj.denied_paths) ? obj.denied_paths.filter((v) => typeof v === "string") : [],
1634
+ inferred_from: Array.isArray(obj.inferred_from) ? obj.inferred_from.filter((v) => typeof v === "string") : [],
1635
+ };
1636
+ }
1637
+ function wildcardToRegex(pattern) {
1638
+ const source = escapeRegExp(pattern).replace(/\\\*/g, ".*");
1639
+ return new RegExp(source, "i");
1640
+ }
1641
+ function pathMatchesAnyPattern(filePath, patterns) {
1642
+ const normalized = filePath.replace(/\\/g, "/");
1643
+ return patterns.filter((p) => wildcardToRegex(p.replace(/\\/g, "/")).test(normalized));
1644
+ }
1645
+ function fileContentHasDeniedTerms(filePath, terms) {
1646
+ if (!terms.length || filePath === "(unspecified)")
1647
+ return [];
1648
+ const abs = path.isAbsolute(filePath) ? path.resolve(filePath) : path.join(projectRoot, filePath);
1649
+ let st;
1650
+ try {
1651
+ st = fs.statSync(abs);
1652
+ }
1653
+ catch {
1654
+ return [];
1655
+ }
1656
+ if (!st.isFile() || st.size > 2_000_000)
1657
+ return [];
1658
+ let content = "";
1659
+ try {
1660
+ content = fs.readFileSync(abs, "utf8");
1661
+ }
1662
+ catch {
1663
+ return [];
1664
+ }
1665
+ const lower = `${filePath}\n${content.slice(0, 250_000)}`.toLowerCase();
1666
+ return terms.filter((term) => lower.includes(term.toLowerCase()));
1667
+ }
1668
+ function mergeScopeContracts(base, extra) {
1669
+ if (!base && !extra)
1670
+ return null;
1671
+ return {
1672
+ allow_terms: Array.from(new Set([...(base?.allow_terms ?? []), ...(extra?.allow_terms ?? [])])),
1673
+ deny_terms: Array.from(new Set([...(base?.deny_terms ?? []), ...(extra?.deny_terms ?? [])])),
1674
+ allowed_paths: Array.from(new Set([...(base?.allowed_paths ?? []), ...(extra?.allowed_paths ?? [])])),
1675
+ denied_paths: Array.from(new Set([...(base?.denied_paths ?? []), ...(extra?.denied_paths ?? [])])),
1676
+ inferred_from: Array.from(new Set([...(base?.inferred_from ?? []), ...(extra?.inferred_from ?? [])])),
1677
+ };
1678
+ }
1679
+ function buildScopeDriftWarnings(args) {
1680
+ const requirementContract = args.requirement ? getRequirementScopeContract(args.requirement.id) : null;
1681
+ const contract = mergeScopeContracts(requirementContract, args.contract);
1682
+ const hasScopeRules = !!contract && (contract.allow_terms.length > 0 ||
1683
+ contract.deny_terms.length > 0 ||
1684
+ contract.allowed_paths.length > 0 ||
1685
+ contract.denied_paths.length > 0);
1686
+ const warnings = [];
1687
+ if (!contract || !hasScopeRules) {
1688
+ if (args.includeMissingContractHint && args.files.length > 0) {
1689
+ warnings.push({
1690
+ code: "scope_contract_missing",
1691
+ severity: "warning",
1692
+ message: "No explicit scope allow/deny contract is set for this requirement, so the planned files cannot be proven in-scope before editing. Define scope_allow/scope_deny or allowed_paths/denied_paths before editing.",
1693
+ files: args.files.map((f) => normalizeToDbPath(f.file_path)).slice(0, 12),
1694
+ details: {
1695
+ requirement_id: args.requirement?.id ?? null,
1696
+ requirement_title: args.requirement?.title ?? null,
1697
+ },
1698
+ });
1699
+ }
1700
+ return warnings;
1701
+ }
1702
+ const allowTerms = contract.allow_terms;
1703
+ const denyTerms = contract.deny_terms;
1704
+ const allowedPaths = contract.allowed_paths;
1705
+ const deniedPaths = contract.denied_paths;
1706
+ const intentText = args.intent ?? "";
1707
+ const intentDenied = denyTerms.filter((term) => intentText.toLowerCase().includes(term.toLowerCase()));
1708
+ const suspicious = [];
1709
+ for (const f of args.files) {
1710
+ const fp = normalizeToDbPath(f.file_path);
1711
+ if (fp === "(unspecified)")
1712
+ continue;
1713
+ const matchedDeniedPaths = pathMatchesAnyPattern(fp, deniedPaths);
1714
+ const matchedDeniedTerms = [
1715
+ ...denyTerms.filter((term) => fp.toLowerCase().includes(term.toLowerCase())),
1716
+ ...fileContentHasDeniedTerms(fp, denyTerms),
1717
+ ];
1718
+ const isExplicitlyAllowed = pathMatchesAnyPattern(fp, allowedPaths).length > 0 ||
1719
+ allowTerms.some((term) => fp.toLowerCase().includes(term.toLowerCase()));
1720
+ const violatesAllowedPaths = allowedPaths.length > 0 && pathMatchesAnyPattern(fp, allowedPaths).length === 0;
1721
+ if ((matchedDeniedPaths.length || matchedDeniedTerms.length || intentDenied.length || violatesAllowedPaths) &&
1722
+ !isExplicitlyAllowed) {
1723
+ suspicious.push({
1724
+ file_path: fp,
1725
+ matched_terms: Array.from(new Set([...matchedDeniedTerms, ...intentDenied])).slice(0, 12),
1726
+ matched_paths: [
1727
+ ...matchedDeniedPaths.slice(0, 12),
1728
+ ...(violatesAllowedPaths ? [`outside allowed_paths: ${allowedPaths.slice(0, 5).join(", ")}`] : []),
1729
+ ],
1730
+ });
1731
+ }
1732
+ }
1733
+ if (suspicious.length) {
1734
+ warnings.push({
1735
+ code: "scope_drift",
1736
+ severity: "blocker",
1737
+ message: "The current requirement appears to be touching a denied or out-of-scope domain. Stop and narrow the change unless the user explicitly expanded this requirement.",
1738
+ files: suspicious.slice(0, 12).map((s) => s.file_path),
1739
+ details: {
1740
+ requirement_id: args.requirement?.id ?? null,
1741
+ requirement_title: args.requirement?.title ?? null,
1742
+ inferred_from: contract.inferred_from,
1743
+ deny_terms: denyTerms.slice(0, 30),
1744
+ denied_paths: deniedPaths.slice(0, 30),
1745
+ suspicious: suspicious.slice(0, 12),
1746
+ },
1747
+ });
1748
+ }
1749
+ return warnings;
1750
+ }
1751
+ function buildDevelopmentWarnings(files, opts = {}) {
1752
+ const warnings = [];
1753
+ const uniqueFiles = Array.from(new Set(files
1754
+ .map((f) => f.file_path)
1755
+ .filter((f) => !!f && f !== "(unspecified)")
1756
+ .map((f) => normalizeToDbPath(f))));
1757
+ if (opts.includeUnspecified || files.some((f) => f.file_path === "(unspecified)")) {
1758
+ warnings.push({
1759
+ code: "unspecified_change_target",
1760
+ severity: "warning",
1761
+ message: "No changed file target was captured. For development work, sync concrete files so the current requirement owns only its real changes.",
1762
+ });
1763
+ }
1764
+ if (uniqueFiles.length >= DEVELOPMENT_WARN_PENDING_FILES) {
1765
+ warnings.push({
1766
+ code: "many_pending_files",
1767
+ severity: "warning",
1768
+ message: "This requirement touches many files. Re-check the user request and keep only files required by the current requirement.",
1769
+ files: uniqueFiles.slice(0, 20),
1770
+ details: { total_files: uniqueFiles.length, threshold: DEVELOPMENT_WARN_PENDING_FILES },
1771
+ });
1772
+ }
1773
+ const topDirs = new Set(uniqueFiles
1774
+ .map((f) => f.replace(/\\/g, "/").split("/").filter(Boolean)[0] ?? "")
1775
+ .filter(Boolean));
1776
+ if (uniqueFiles.length >= 6 && topDirs.size >= 4) {
1777
+ warnings.push({
1778
+ code: "broad_change_surface",
1779
+ severity: "warning",
1780
+ message: "Changed files span several top-level areas. Avoid modifying completed or merely related features unless the current requirement explicitly needs it.",
1781
+ files: uniqueFiles.slice(0, 20),
1782
+ details: { top_level_dirs: Array.from(topDirs).slice(0, 12), total_dirs: topDirs.size },
1783
+ });
1784
+ }
1785
+ for (const relPath of uniqueFiles) {
1786
+ if (!isLikelySourceImplementationFile(relPath))
1787
+ continue;
1788
+ const absPath = path.isAbsolute(relPath) ? relPath : path.join(projectRoot, relPath);
1789
+ let stat;
1790
+ try {
1791
+ stat = fs.statSync(absPath);
1792
+ }
1793
+ catch {
1794
+ continue;
1795
+ }
1796
+ if (!stat.isFile())
1797
+ continue;
1798
+ const lineInfo = countFileLinesBounded(absPath, 2_000_000);
1799
+ const lineCount = lineInfo?.lines ?? 0;
1800
+ const tooManyLines = lineCount >= DEVELOPMENT_BLOCK_FILE_LINES;
1801
+ const warnLines = lineCount >= DEVELOPMENT_WARN_FILE_LINES;
1802
+ const warnBytes = stat.size >= DEVELOPMENT_WARN_FILE_BYTES;
1803
+ if (!tooManyLines && !warnLines && !warnBytes)
1804
+ continue;
1805
+ warnings.push({
1806
+ code: tooManyLines ? "very_large_file" : "large_file",
1807
+ severity: tooManyLines ? "blocker" : "warning",
1808
+ message: tooManyLines
1809
+ ? "This implementation file is already very large. Do not add new feature code here by default; split into a focused module/service/component and keep this file as a thin entry."
1810
+ : "This implementation file is getting large. Prefer extracting focused modules instead of continuing to pile unrelated responsibilities into it.",
1811
+ files: [relPath],
1812
+ details: {
1813
+ lines: lineInfo?.truncated ? `${lineCount}+` : lineCount,
1814
+ bytes: stat.size,
1815
+ warn_lines: DEVELOPMENT_WARN_FILE_LINES,
1816
+ block_lines: DEVELOPMENT_BLOCK_FILE_LINES,
1817
+ warn_bytes: DEVELOPMENT_WARN_FILE_BYTES,
1818
+ },
1819
+ });
1820
+ }
1821
+ return warnings;
1822
+ }
1823
+ function compactDevelopmentWarningsText(warnings) {
1824
+ if (!warnings.length)
1825
+ return [];
1826
+ const lines = ["development warnings:"];
1827
+ for (const w of warnings.slice(0, 8)) {
1828
+ const files = w.files?.length ? ` files=${w.files.slice(0, 5).join(",")}` : "";
1829
+ lines.push(`- ${w.severity} ${w.code}: ${oneLine(w.message, 180)}${files}`);
1830
+ }
1831
+ return lines;
1832
+ }
890
1833
  function extractSymbols(filePath, content) {
891
1834
  const ext = path.extname(filePath).toLowerCase();
892
1835
  if (ext === ".py")
@@ -1247,12 +2190,25 @@ const StartRequirementArgsSchema = ProjectRootArgSchema.merge(z.object({
1247
2190
  title: z.string().min(1),
1248
2191
  background: z.string().optional().default(""),
1249
2192
  close_previous: z.boolean().optional().default(true),
2193
+ scope_allow: z.array(z.string().min(1)).optional(),
2194
+ scope_deny: z.array(z.string().min(1)).optional(),
2195
+ allowed_paths: z.array(z.string().min(1)).optional(),
2196
+ denied_paths: z.array(z.string().min(1)).optional(),
1250
2197
  }));
1251
2198
  const SyncChangeIntentArgsSchema = ProjectRootArgSchema.merge(z.object({
1252
2199
  intent: z.string().min(1),
1253
2200
  files: z.array(z.string().min(1)).optional(),
1254
2201
  affected_files: z.array(z.string().min(1)).optional(),
1255
2202
  }));
2203
+ const PreflightChangeScopeArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
2204
+ intent: z.string().optional().default(""),
2205
+ files: z.array(z.string().min(1)).optional(),
2206
+ planned_files: z.array(z.string().min(1)).optional(),
2207
+ scope_allow: z.array(z.string().min(1)).optional(),
2208
+ scope_deny: z.array(z.string().min(1)).optional(),
2209
+ allowed_paths: z.array(z.string().min(1)).optional(),
2210
+ denied_paths: z.array(z.string().min(1)).optional(),
2211
+ }));
1256
2212
  const QueryCodebaseArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
1257
2213
  query: z.string().min(1),
1258
2214
  }));
@@ -1352,6 +2308,19 @@ const SupersedeMemoryArgsSchema = ProjectRootArgSchema.merge(z.object({
1352
2308
  replacement_memory_id: z.number().int().positive().optional(),
1353
2309
  reason: z.string().min(1),
1354
2310
  }));
2311
+ const MaintainMemoryArgsSchema = ProjectRootArgSchema.merge(OutputFormatSchema).merge(z.object({
2312
+ dry_run: z.boolean().optional().default(true),
2313
+ compact_old_memories: z.boolean().optional().default(true),
2314
+ compact_notes: z.boolean().optional().default(false),
2315
+ prune_stale_indexes: z.boolean().optional().default(true),
2316
+ prune_ignored_paths: z.boolean().optional().default(true),
2317
+ prune_filename_noise: z.boolean().optional().default(true),
2318
+ prune_hidden_embeddings: z.boolean().optional().default(true),
2319
+ compact_after_days: z.number().int().min(1).max(3650).optional().default(MAINTENANCE_COMPACT_AFTER_DAYS),
2320
+ max_memory_items: z.number().int().min(1).max(5000).optional().default(MAINTENANCE_MAX_MEMORY_ITEMS),
2321
+ max_index_files: z.number().int().min(1).max(50_000).optional().default(MAINTENANCE_MAX_INDEX_FILES),
2322
+ vacuum: z.boolean().optional().default(false),
2323
+ }));
1355
2324
  const DEFAULT_PENDING_LIMIT = 10;
1356
2325
  const MAX_PENDING_LIMIT = 2000;
1357
2326
  const PendingPagingSchema = z.object({
@@ -1371,13 +2340,22 @@ const DEFAULT_RECENT_CHANGES_PER_REQ = 3;
1371
2340
  const DEFAULT_RECENT_NOTES = 3;
1372
2341
  const DEFAULT_CONVENTIONS_LIMIT = 0;
1373
2342
  const DEFAULT_DECISIONS_LIMIT = 5;
2343
+ const DEFAULT_CURRENT_CONTEXT_LIMIT = 8;
1374
2344
  const MAX_DECISIONS_LIMIT = 50;
2345
+ const MAX_CURRENT_CONTEXT_LIMIT = 50;
1375
2346
  const BrainDumpLimitsSchema = z.object({
1376
2347
  requirements_limit: z.number().int().min(1).max(20).optional().default(DEFAULT_RECENT_REQUIREMENTS),
1377
2348
  changes_limit: z.number().int().min(1).max(100).optional().default(DEFAULT_RECENT_CHANGES_PER_REQ),
1378
2349
  notes_limit: z.number().int().min(0).max(50).optional().default(DEFAULT_RECENT_NOTES),
1379
2350
  conventions_limit: z.number().int().min(0).max(200).optional().default(DEFAULT_CONVENTIONS_LIMIT),
1380
2351
  decisions_limit: z.number().int().min(0).max(MAX_DECISIONS_LIMIT).optional().default(DEFAULT_DECISIONS_LIMIT),
2352
+ current_context_limit: z
2353
+ .number()
2354
+ .int()
2355
+ .min(0)
2356
+ .max(MAX_CURRENT_CONTEXT_LIMIT)
2357
+ .optional()
2358
+ .default(DEFAULT_CURRENT_CONTEXT_LIMIT),
1381
2359
  });
1382
2360
  const GetPendingChangesArgsSchema = ProjectRootArgSchema.merge(z.object({
1383
2361
  offset: z.number().int().min(0).optional().default(0),
@@ -1560,6 +2538,7 @@ function compactGrepText(data) {
1560
2538
  const lines = [
1561
2539
  `grep ${data.backend}${fallback} mode=${data.mode} matches=${data.matches.length}/${total} truncated=${data.truncated}${candidateText} q="${oneLine(data.query, 100)}"`,
1562
2540
  ];
2541
+ lines.push(...compactDevelopmentWarningsText(data.development_warnings ?? []));
1563
2542
  if (data.ripgrep_error)
1564
2543
  lines.push(`ripgrep_error ${oneLine(data.ripgrep_error, 180)}`);
1565
2544
  for (const m of data.matches.slice(0, 80)) {
@@ -1589,15 +2568,18 @@ function compactReadTextFileText(data) {
1589
2568
  const offset = data.offset != null ? ` offset=${data.offset}` : "";
1590
2569
  const header = `file ${data.file_path}${offset} chars=${data.returned_chars}/${data.total_chars} truncated=${data.truncated}`;
1591
2570
  const hint = data.truncated ? "\nhint: continue with offset or read_file_lines; use format=json for metadata fields" : "";
1592
- return `${header}\n${data.text}${hint}`;
2571
+ const warnings = compactDevelopmentWarningsText(data.development_warnings ?? []).join("\n");
2572
+ return `${header}${warnings ? `\n${warnings}` : ""}\n${data.text}${hint}`;
1593
2573
  }
1594
2574
  function compactReadFileLinesText(data) {
1595
2575
  const header = `lines ${data.file_path}:${data.from_line}-${data.to_line} returned=${data.returned} truncated=${data.truncated}`;
1596
2576
  const hint = data.truncated ? "\nhint: narrow range or raise max_lines/max_chars; use format=json for metadata fields" : "";
1597
- return `${header}\n${data.text}${hint}`;
2577
+ const warnings = compactDevelopmentWarningsText(data.development_warnings ?? []).join("\n");
2578
+ return `${header}${warnings ? `\n${warnings}` : ""}\n${data.text}${hint}`;
1598
2579
  }
1599
2580
  function compactQueryCodebaseText(data) {
1600
2581
  const lines = [`query_codebase matches=${data.matches.length} q="${oneLine(data.query, 100)}"`];
2582
+ lines.push(...compactDevelopmentWarningsText(data.development_warnings ?? []));
1601
2583
  for (const m of data.matches.slice(0, 50)) {
1602
2584
  lines.push(`${m.file_path}: ${m.type} ${m.name}${m.signature ? ` — ${oneLine(m.signature, 160)}` : ""}`);
1603
2585
  }
@@ -1605,6 +2587,47 @@ function compactQueryCodebaseText(data) {
1605
2587
  lines.push("- no matches");
1606
2588
  return lines.join("\n");
1607
2589
  }
2590
+ function compactMaintenanceText(data) {
2591
+ const prunedChunks = data.pruned.ignored_paths.chunks_deleted +
2592
+ data.pruned.filename_noise.chunks_deleted +
2593
+ data.pruned.stale_files.chunks_deleted;
2594
+ const prunedSymbols = data.pruned.ignored_paths.symbols_deleted +
2595
+ data.pruned.filename_noise.symbols_deleted +
2596
+ data.pruned.stale_files.symbols_deleted;
2597
+ const lines = [
2598
+ `maintain_memory ok dry_run=${data.dry_run} trigger=${data.trigger} compacted=${data.compacted_memory.compacted}/${data.compacted_memory.candidates} archived=${data.compacted_memory.archived} pruned_chunks=${prunedChunks} pruned_symbols=${prunedSymbols} hidden_embeddings=${data.pruned.hidden_embeddings.embeddings_deleted}`,
2599
+ ];
2600
+ if (data.compacted_memory.summary_memory_id) {
2601
+ lines.push(`summary memory_compaction #${data.compacted_memory.summary_memory_id}`);
2602
+ }
2603
+ if (data.compacted_memory.samples.length) {
2604
+ lines.push("memory candidates:");
2605
+ for (const s of data.compacted_memory.samples.slice(0, 8)) {
2606
+ lines.push(`- #${s.id} ${s.kind} ${s.file_path ?? ""} ${oneLine(s.title ?? "", 80)} ${s.updated_at}`);
2607
+ }
2608
+ }
2609
+ if (data.pruned.stale_files.samples.length) {
2610
+ lines.push("stale index samples:");
2611
+ for (const s of data.pruned.stale_files.samples.slice(0, 8))
2612
+ lines.push(`- ${s}`);
2613
+ }
2614
+ lines.push("hint: dry_run=false applies changes; vacuum=true reclaims sqlite file space after pruning");
2615
+ return lines.join("\n");
2616
+ }
2617
+ function compactPreflightChangeScopeText(data) {
2618
+ const req = data.active_requirement ? `#${data.active_requirement.id} ${data.active_requirement.title}` : "none";
2619
+ const lines = [
2620
+ `preflight_change_scope ok=${data.ok} safe_to_edit=${data.safe_to_edit} requirement=${req} files=${data.files.length} intent="${oneLine(data.intent, 120)}"`,
2621
+ `action: ${oneLine(data.recommended_action, 180)}`,
2622
+ ];
2623
+ if (data.scope_contract) {
2624
+ lines.push(`scope allow_terms=${data.scope_contract.allow_terms.length} deny_terms=${data.scope_contract.deny_terms.length} allowed_paths=${data.scope_contract.allowed_paths.length} denied_paths=${data.scope_contract.denied_paths.length}`);
2625
+ }
2626
+ lines.push(...compactDevelopmentWarningsText(data.development_warnings));
2627
+ if (!data.development_warnings.length)
2628
+ lines.push("- no development warnings");
2629
+ return lines.join("\n");
2630
+ }
1608
2631
  function compactBootstrapText(data) {
1609
2632
  const lines = [];
1610
2633
  lines.push(`ok ctx ${data.root_source} watcher=${data.watcher_enabled ? (data.watcher_ready ? "ready" : "starting") : "off"} root=${data.project_root}`);
@@ -1615,6 +2638,11 @@ function compactBootstrapText(data) {
1615
2638
  for (const d of data.decisions.slice(0, 5))
1616
2639
  lines.push(`- ${compactMemoryLabel(d, 160)}`);
1617
2640
  }
2641
+ if (data.current_context.length) {
2642
+ lines.push("current context:");
2643
+ for (const c of data.current_context.slice(0, 8))
2644
+ lines.push(`- ${compactMemoryLabel(c, 160)}`);
2645
+ }
1618
2646
  if (data.pending_total) {
1619
2647
  lines.push(`pending ${data.pending_changes.length}/${data.pending_total}${data.pending_truncated ? " truncated" : ""}: ${data.pending_changes
1620
2648
  .slice(0, 8)
@@ -1624,6 +2652,7 @@ function compactBootstrapText(data) {
1624
2652
  else {
1625
2653
  lines.push("pending 0");
1626
2654
  }
2655
+ lines.push(...compactDevelopmentWarningsText(data.development_warnings ?? []));
1627
2656
  if (data.items.length) {
1628
2657
  lines.push("requirements:");
1629
2658
  for (const item of data.items) {
@@ -1719,8 +2748,8 @@ function runRtkProbe(spec) {
1719
2748
  ? `Prefer prefixing shell commands with ${spec.displayCommand} for compact outputs. This is VectorMind's bundled RTK shim; first run auto-installs/caches rtk-ai/rtk if needed.`
1720
2749
  : "Prefer prefixing shell commands with rtk for compact outputs, e.g. rtk git status / rtk npm run build / rtk rg pattern ."
1721
2750
  : spec.source === "package_shim"
1722
- ? "VectorMind's bundled RTK shim exists, but `gain` failed. Check network/cache or set VECTORMIND_RTK_REAL to an existing rtk-ai/rtk binary."
1723
- : "An rtk binary exists, but `rtk gain` failed. This may be the wrong rtk project. Use install_rtk with uninstall_wrong_cargo_rtk=true only after confirming it is safe.",
2751
+ ? "VectorMind's bundled RTK shim exists, but `gain` failed. Check npm/cache or set VECTORMIND_RTK_REAL to an existing rtk-ai/rtk binary."
2752
+ : "An rtk binary exists, but `rtk gain` failed. This may be the wrong rtk project. Use install_rtk with uninstall_wrong_cargo_rtk=true only when you intentionally want to replace it.",
1724
2753
  };
1725
2754
  }
1726
2755
  return null;
@@ -1755,7 +2784,7 @@ function detectRtk() {
1755
2784
  path: shimPath ?? undefined,
1756
2785
  source: shimPath ? "package_shim" : undefined,
1757
2786
  note: shimPath
1758
- ? "rtk was not found on PATH, and VectorMind's bundled RTK shim could not verify rtk gain. VectorMind compact MCP output still works; check network/cache or set VECTORMIND_RTK_REAL."
2787
+ ? "rtk was not found on PATH, and VectorMind's bundled RTK shim could not verify rtk gain. VectorMind compact MCP output still works; check npm/cache or set VECTORMIND_RTK_REAL."
1759
2788
  : "rtk was not found on PATH and the package RTK shim is unavailable. VectorMind compact MCP output still works; install rtk to compact shell command output too.",
1760
2789
  };
1761
2790
  }
@@ -2129,6 +3158,80 @@ function dotProduct(a, b) {
2129
3158
  s += a[i] * b[i];
2130
3159
  return s;
2131
3160
  }
3161
+ const SEMANTIC_TOKEN_STOPWORDS = new Set([
3162
+ "a",
3163
+ "an",
3164
+ "and",
3165
+ "are",
3166
+ "as",
3167
+ "at",
3168
+ "be",
3169
+ "by",
3170
+ "for",
3171
+ "from",
3172
+ "has",
3173
+ "have",
3174
+ "if",
3175
+ "in",
3176
+ "into",
3177
+ "is",
3178
+ "it",
3179
+ "its",
3180
+ "of",
3181
+ "on",
3182
+ "or",
3183
+ "that",
3184
+ "the",
3185
+ "this",
3186
+ "to",
3187
+ "with",
3188
+ ]);
3189
+ const BOOTSTRAP_DEFAULT_CONTEXT_KINDS = [
3190
+ "decision",
3191
+ "convention",
3192
+ "project_summary",
3193
+ "memory_compaction",
3194
+ "note",
3195
+ "requirement",
3196
+ "change_intent",
3197
+ ];
3198
+ const TOKEN_SEARCH_DEFAULT_KINDS = [
3199
+ "decision",
3200
+ "convention",
3201
+ "project_summary",
3202
+ "memory_compaction",
3203
+ "note",
3204
+ "requirement",
3205
+ "change_intent",
3206
+ "code_chunk",
3207
+ "doc_chunk",
3208
+ ];
3209
+ const DECISION_CANDIDATE_KEYWORDS = [
3210
+ "用户确认",
3211
+ "用户要求",
3212
+ "明确",
3213
+ "架构决策",
3214
+ "最终",
3215
+ "默认",
3216
+ "只保留",
3217
+ "统一",
3218
+ "不需要",
3219
+ "无需",
3220
+ "不再",
3221
+ "改成",
3222
+ "改为",
3223
+ "直接通过",
3224
+ "不用审核",
3225
+ "decision",
3226
+ "decided",
3227
+ "confirmed",
3228
+ "must",
3229
+ "default",
3230
+ "only",
3231
+ "single",
3232
+ "no longer",
3233
+ "instead",
3234
+ ];
2132
3235
  function parseMetadataJson(metadata) {
2133
3236
  if (!metadata)
2134
3237
  return {};
@@ -2150,6 +3253,13 @@ function isSupersededMemory(row) {
2150
3253
  const meta = parseMetadataJson(row.metadata_json);
2151
3254
  return meta.superseded === true || meta.status === "superseded";
2152
3255
  }
3256
+ function isCompactedMemory(row) {
3257
+ const meta = parseMetadataJson(row.metadata_json);
3258
+ return meta.compacted === true || meta.status === "compacted";
3259
+ }
3260
+ function isHiddenFromDefaultRecall(row) {
3261
+ return isSupersededMemory(row) || isCompactedMemory(row);
3262
+ }
2153
3263
  function semanticRecencyWeight(updatedAt) {
2154
3264
  if (!updatedAt)
2155
3265
  return 0;
@@ -2168,7 +3278,7 @@ function semanticRecencyWeight(updatedAt) {
2168
3278
  function semanticKindWeight(kind) {
2169
3279
  switch (kind) {
2170
3280
  case "decision":
2171
- return 3.5;
3281
+ return 16;
2172
3282
  case "convention":
2173
3283
  return 2.6;
2174
3284
  case "project_summary":
@@ -2179,16 +3289,20 @@ function semanticKindWeight(kind) {
2179
3289
  return 0.4;
2180
3290
  case "change_intent":
2181
3291
  return 0.2;
3292
+ case "memory_compaction":
3293
+ return 0.7;
2182
3294
  default:
2183
3295
  return 0;
2184
3296
  }
2185
3297
  }
2186
3298
  function adjustSemanticScore(row, rawScore) {
2187
- if (isSupersededMemory(row))
3299
+ if (isHiddenFromDefaultRecall(row))
2188
3300
  return rawScore - 1000;
2189
3301
  let score = rawScore + semanticKindWeight(row.kind) + semanticRecencyWeight(row.updated_at);
2190
3302
  const status = metadataStatus(row);
2191
- if (status === "active" || status === "current")
3303
+ if (status === "current")
3304
+ score += row.kind === "decision" ? 24 : 1.2;
3305
+ if (status === "active")
2192
3306
  score += 1.2;
2193
3307
  if (row.kind === "change_intent" && row.file_path && shouldIgnoreDbFilePath(row.file_path)) {
2194
3308
  // Human-synced intent for generated/build/runtime files is often the only durable
@@ -2197,11 +3311,110 @@ function adjustSemanticScore(row, rawScore) {
2197
3311
  }
2198
3312
  return score;
2199
3313
  }
3314
+ function normalizeSearchText(input) {
3315
+ return (input ?? "").normalize("NFKC").toLowerCase();
3316
+ }
3317
+ function extractSearchTokens(raw) {
3318
+ const text = normalizeSearchText(raw);
3319
+ const tokens = new Set();
3320
+ for (const token of text.match(/[a-z0-9_./:@#-]{2,}/g) ?? []) {
3321
+ if (!SEMANTIC_TOKEN_STOPWORDS.has(token))
3322
+ tokens.add(token);
3323
+ for (const part of token.split(/[^a-z0-9]+/).filter((p) => p.length >= 2)) {
3324
+ if (!SEMANTIC_TOKEN_STOPWORDS.has(part))
3325
+ tokens.add(part);
3326
+ }
3327
+ }
3328
+ for (const seq of text.match(/\p{Script=Han}+/gu) ?? []) {
3329
+ if (seq.length >= 2 && seq.length <= 18)
3330
+ tokens.add(seq);
3331
+ for (const n of [2, 3, 4]) {
3332
+ if (seq.length < n)
3333
+ continue;
3334
+ for (let i = 0; i <= seq.length - n; i++) {
3335
+ tokens.add(seq.slice(i, i + n));
3336
+ }
3337
+ }
3338
+ }
3339
+ return Array.from(tokens)
3340
+ .filter((token) => token.length >= 2 && !SEMANTIC_TOKEN_STOPWORDS.has(token))
3341
+ .sort((a, b) => b.length - a.length)
3342
+ .slice(0, 48);
3343
+ }
3344
+ function countNeedleOccurrences(haystack, needle) {
3345
+ if (!haystack || !needle)
3346
+ return 0;
3347
+ let count = 0;
3348
+ let idx = 0;
3349
+ while ((idx = haystack.indexOf(needle, idx)) >= 0) {
3350
+ count++;
3351
+ idx += Math.max(1, needle.length);
3352
+ if (count >= 8)
3353
+ break;
3354
+ }
3355
+ return count;
3356
+ }
3357
+ function tokenLexicalScore(row, query, tokens) {
3358
+ if (!tokens.length)
3359
+ return 0;
3360
+ const title = normalizeSearchText(row.title);
3361
+ const content = normalizeSearchText(row.content);
3362
+ const filePath = normalizeSearchText(row.file_path);
3363
+ const metadata = normalizeSearchText(row.metadata_json);
3364
+ const exact = normalizeSearchText(query).trim();
3365
+ let score = 0;
3366
+ if (exact.length >= 4) {
3367
+ if (title.includes(exact))
3368
+ score += 8;
3369
+ if (content.includes(exact))
3370
+ score += 6;
3371
+ if (filePath.includes(exact))
3372
+ score += 4;
3373
+ }
3374
+ let matched = 0;
3375
+ for (const token of tokens) {
3376
+ let tokenScore = 0;
3377
+ if (title.includes(token))
3378
+ tokenScore += 3.2;
3379
+ if (filePath.includes(token))
3380
+ tokenScore += 2.4;
3381
+ const contentHits = countNeedleOccurrences(content, token);
3382
+ if (contentHits)
3383
+ tokenScore += Math.min(3.2, 0.75 + contentHits * 0.45);
3384
+ if (metadata.includes(token))
3385
+ tokenScore += 0.8;
3386
+ if (tokenScore > 0) {
3387
+ matched++;
3388
+ score += tokenScore * Math.min(2.4, Math.max(1, token.length / 4));
3389
+ }
3390
+ }
3391
+ if (matched >= Math.min(3, tokens.length))
3392
+ score += 2;
3393
+ score += matched / Math.max(1, tokens.length);
3394
+ return score;
3395
+ }
3396
+ function looksLikeDecisionContent(content) {
3397
+ const text = normalizeSearchText(content);
3398
+ return DECISION_CANDIDATE_KEYWORDS.some((kw) => text.includes(normalizeSearchText(kw)));
3399
+ }
3400
+ function mergeSemanticMatches(sets, opts) {
3401
+ const best = new Map();
3402
+ for (const matches of sets) {
3403
+ for (const match of matches) {
3404
+ const prev = best.get(match.item.id);
3405
+ if (!prev || match.score > prev.score)
3406
+ best.set(match.item.id, match);
3407
+ }
3408
+ }
3409
+ return Array.from(best.values())
3410
+ .sort((a, b) => b.score - a.score || b.item.id - a.item.id)
3411
+ .slice(0, opts.topK);
3412
+ }
2200
3413
  function filterAndRankSemanticRows(rows, scoreOf, opts) {
2201
3414
  return rows
2202
3415
  .map((r) => ({ row: r, score: adjustSemanticScore(r, scoreOf(r)) }))
2203
3416
  .filter(({ row }) => {
2204
- if (isSupersededMemory(row))
3417
+ if (isHiddenFromDefaultRecall(row))
2205
3418
  return false;
2206
3419
  if (shouldIgnoreDbFilePath(row.file_path) && row.kind !== "change_intent")
2207
3420
  return false;
@@ -2290,10 +3503,43 @@ function getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars) {
2290
3503
  return [];
2291
3504
  const rows = listCurrentDecisionsStmt.all(Math.min(MAX_DECISIONS_LIMIT * 4, Math.max(decisionsLimit, decisionsLimit * 4)));
2292
3505
  return rows
2293
- .filter((d) => !isSupersededMemory(d))
3506
+ .filter((d) => !isHiddenFromDefaultRecall(d))
2294
3507
  .slice(0, decisionsLimit)
2295
3508
  .map((d) => toMemoryItemPreview(d, false, previewChars, contentMaxChars));
2296
3509
  }
3510
+ function getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars) {
3511
+ if (currentContextLimit <= 0)
3512
+ return [];
3513
+ const picked = new Map();
3514
+ const addRow = (row) => {
3515
+ if (!row)
3516
+ return;
3517
+ if (isHiddenFromDefaultRecall(row))
3518
+ return;
3519
+ if (shouldIgnoreDbFilePath(row.file_path) && row.kind !== "change_intent")
3520
+ return;
3521
+ if (!picked.has(row.id)) {
3522
+ picked.set(row.id, toMemoryItemPreview(row, false, previewChars, contentMaxChars));
3523
+ }
3524
+ };
3525
+ const activeReqs = listActiveRequirementsStmt.all(Math.max(currentContextLimit, 10));
3526
+ for (const req of activeReqs) {
3527
+ const memId = getRequirementMemoryItemIdStmt.get(req.id)?.id;
3528
+ if (memId != null)
3529
+ addRow(getMemoryItemByIdStmt.get(memId));
3530
+ }
3531
+ const recentRows = listRecentContextItemsStmt.all(Math.max(currentContextLimit * 8, 40));
3532
+ for (const row of recentRows) {
3533
+ if (picked.size >= currentContextLimit)
3534
+ break;
3535
+ if (row.kind === "requirement" || row.kind === "change_intent") {
3536
+ if (!looksLikeDecisionContent(`${row.title ?? ""}\n${row.content}`))
3537
+ continue;
3538
+ }
3539
+ addRow(row);
3540
+ }
3541
+ return Array.from(picked.values()).slice(0, currentContextLimit);
3542
+ }
2297
3543
  function toRequirementPreview(req, includeContent, previewChars, contentMaxChars) {
2298
3544
  const context = req.context_data ?? null;
2299
3545
  const contextPreview = context ? makePreviewText(context, previewChars) : null;
@@ -2431,7 +3677,7 @@ async function semanticSearchInternal(opts) {
2431
3677
  .filter(Boolean);
2432
3678
  const filtered = matches
2433
3679
  .filter((m) => {
2434
- if (isSupersededMemory({ metadata_json: m.item.metadata_json }))
3680
+ if (isHiddenFromDefaultRecall({ metadata_json: m.item.metadata_json }))
2435
3681
  return false;
2436
3682
  if (shouldIgnoreDbFilePath(m.item.file_path) && m.item.kind !== "change_intent")
2437
3683
  return false;
@@ -2591,24 +3837,212 @@ function likeSearchInternal(opts) {
2591
3837
  const matches = filterAndRankSemanticRows(rows, (r) => Number(r.score), opts);
2592
3838
  return { query: q, top_k: opts.topK, mode: "like", matches };
2593
3839
  }
2594
- async function semanticSearchHybridInternal(opts) {
2595
- if (embeddingsEnabled) {
2596
- try {
2597
- return await semanticSearchInternal(opts);
2598
- }
2599
- catch (err) {
2600
- console.error("[vectormind] embeddings semantic_search failed; falling back:", err);
2601
- }
3840
+ function tokenSearchInternal(opts) {
3841
+ const q = opts.query.trim();
3842
+ if (!q)
3843
+ return { query: "", top_k: opts.topK, mode: "token", matches: [] };
3844
+ const tokens = extractSearchTokens(q);
3845
+ if (!tokens.length)
3846
+ return { query: q, top_k: opts.topK, mode: "token", matches: [] };
3847
+ const rawLimit = Math.min(160, Math.max(opts.topK * 12, 80));
3848
+ const searchTokens = tokens.slice(0, 8);
3849
+ const effectiveKinds = opts.kinds?.length ? opts.kinds : TOKEN_SEARCH_DEFAULT_KINDS;
3850
+ const kindClause = effectiveKinds.length
3851
+ ? `AND kind IN (${effectiveKinds.map(() => "?").join(", ")})`
3852
+ : "";
3853
+ const recencyBoost = `
3854
+ + CASE
3855
+ WHEN updated_at >= datetime('now', '-2 days') THEN 4
3856
+ WHEN updated_at >= datetime('now', '-14 days') THEN 2
3857
+ WHEN updated_at >= datetime('now', '-60 days') THEN 1
3858
+ ELSE 0
3859
+ END`;
3860
+ const candidateScore = `
3861
+ (
3862
+ CASE kind
3863
+ WHEN 'decision' THEN 9
3864
+ WHEN 'convention' THEN 7
3865
+ WHEN 'project_summary' THEN 6
3866
+ WHEN 'note' THEN 5
3867
+ WHEN 'requirement' THEN 4
3868
+ WHEN 'change_intent' THEN 3
3869
+ ELSE 0
3870
+ END
3871
+ ${recencyBoost}
3872
+ )`;
3873
+ const includesIndexedChunks = effectiveKinds.some((k) => k === "code_chunk" || k === "doc_chunk");
3874
+ if (!includesIndexedChunks) {
3875
+ const candidateLimit = Math.min(1600, Math.max(rawLimit * 5, 800));
3876
+ const stmt = db.prepare(`
3877
+ SELECT
3878
+ id,
3879
+ kind,
3880
+ title,
3881
+ content,
3882
+ file_path,
3883
+ start_line,
3884
+ end_line,
3885
+ req_id,
3886
+ metadata_json,
3887
+ updated_at
3888
+ FROM memory_items
3889
+ WHERE 1=1
3890
+ ${kindClause}
3891
+ ORDER BY
3892
+ ${candidateScore} DESC,
3893
+ updated_at DESC,
3894
+ id DESC
3895
+ LIMIT ?
3896
+ `);
3897
+ const candidates = stmt.all(...effectiveKinds, candidateLimit);
3898
+ const scoreMap = new Map();
3899
+ const rows = candidates.filter((row) => {
3900
+ const score = tokenLexicalScore(row, q, tokens);
3901
+ if (score <= 0)
3902
+ return false;
3903
+ scoreMap.set(row.id, score);
3904
+ return true;
3905
+ });
3906
+ const matches = filterAndRankSemanticRows(rows, (r) => scoreMap.get(r.id) ?? 0, opts);
3907
+ return { query: q, top_k: opts.topK, mode: "token", matches };
3908
+ }
3909
+ const memoryFirstKinds = effectiveKinds.filter((k) => k !== "code_chunk" && k !== "doc_chunk");
3910
+ const memoryFirstLimit = Math.min(1200, Math.max(rawLimit * 4, 300));
3911
+ let memoryFirstRows = [];
3912
+ const memoryFirstScores = new Map();
3913
+ if (memoryFirstKinds.length) {
3914
+ const memoryKindClause = `AND kind IN (${memoryFirstKinds.map(() => "?").join(", ")})`;
3915
+ const memoryStmt = db.prepare(`
3916
+ SELECT
3917
+ id,
3918
+ kind,
3919
+ title,
3920
+ content,
3921
+ file_path,
3922
+ start_line,
3923
+ end_line,
3924
+ req_id,
3925
+ metadata_json,
3926
+ updated_at
3927
+ FROM memory_items
3928
+ WHERE 1=1
3929
+ ${memoryKindClause}
3930
+ ORDER BY
3931
+ ${candidateScore} DESC,
3932
+ updated_at DESC,
3933
+ id DESC
3934
+ LIMIT ?
3935
+ `);
3936
+ const candidates = memoryStmt.all(...memoryFirstKinds, memoryFirstLimit);
3937
+ memoryFirstRows = candidates.filter((row) => {
3938
+ const score = tokenLexicalScore(row, q, tokens);
3939
+ if (score <= 0)
3940
+ return false;
3941
+ memoryFirstScores.set(row.id, score);
3942
+ return true;
3943
+ });
2602
3944
  }
3945
+ const conditions = [];
3946
+ const values = [];
3947
+ for (const token of searchTokens) {
3948
+ const like = `%${escapeLike(token)}%`;
3949
+ conditions.push(`content LIKE ? ESCAPE '\\'`);
3950
+ values.push(like);
3951
+ conditions.push(`title LIKE ? ESCAPE '\\'`);
3952
+ values.push(like);
3953
+ conditions.push(`file_path LIKE ? ESCAPE '\\'`);
3954
+ values.push(like);
3955
+ }
3956
+ if (!conditions.length)
3957
+ return { query: q, top_k: opts.topK, mode: "token", matches: [] };
3958
+ const stmt = db.prepare(`
3959
+ SELECT
3960
+ id,
3961
+ kind,
3962
+ title,
3963
+ content,
3964
+ file_path,
3965
+ start_line,
3966
+ end_line,
3967
+ req_id,
3968
+ metadata_json,
3969
+ updated_at
3970
+ FROM memory_items
3971
+ WHERE (${conditions.join(" OR ")})
3972
+ ${kindClause}
3973
+ ORDER BY
3974
+ ${candidateScore} DESC,
3975
+ updated_at DESC,
3976
+ id DESC
3977
+ LIMIT ?
3978
+ `);
3979
+ const rows = stmt.all(...values, ...effectiveKinds, rawLimit);
3980
+ const scoreMap = new Map(memoryFirstScores);
3981
+ for (const row of rows) {
3982
+ if (!scoreMap.has(row.id))
3983
+ scoreMap.set(row.id, tokenLexicalScore(row, q, tokens));
3984
+ }
3985
+ const rowMap = new Map();
3986
+ for (const row of memoryFirstRows)
3987
+ rowMap.set(row.id, row);
3988
+ for (const row of rows)
3989
+ rowMap.set(row.id, row);
3990
+ const matches = filterAndRankSemanticRows(Array.from(rowMap.values()), (r) => scoreMap.get(r.id) ?? 0, opts);
3991
+ return { query: q, top_k: opts.topK, mode: "token", matches };
3992
+ }
3993
+ function chooseLexicalResult(opts) {
3994
+ const tokenResult = tokenSearchInternal(opts);
3995
+ const tokenTopScore = tokenResult.matches[0]?.score ?? 0;
3996
+ const tokenEnough = tokenResult.matches.length >= Math.min(opts.topK, 3) && tokenTopScore >= 8;
3997
+ if (tokenEnough || tokenResult.matches.length >= opts.topK) {
3998
+ return { result: tokenResult, mode: "token" };
3999
+ }
4000
+ let textResult = null;
2603
4001
  if (ftsAvailable) {
2604
4002
  try {
2605
- return ftsSearchInternal(opts);
4003
+ textResult = ftsSearchInternal(opts);
2606
4004
  }
2607
4005
  catch (err) {
2608
4006
  console.error("[vectormind] fts semantic_search failed; falling back:", err);
2609
4007
  }
2610
4008
  }
2611
- return likeSearchInternal(opts);
4009
+ if (!textResult) {
4010
+ textResult = likeSearchInternal(opts);
4011
+ }
4012
+ const merged = mergeSemanticMatches([textResult.matches, tokenResult.matches], opts);
4013
+ const tokenIds = new Set(tokenResult.matches.map((m) => m.item.id));
4014
+ const ftsKept = textResult.matches.some((m) => !tokenIds.has(m.item.id));
4015
+ if (tokenResult.matches.length && ftsKept) {
4016
+ return {
4017
+ result: { query: opts.query.trim(), top_k: opts.topK, mode: "hybrid", matches: merged },
4018
+ mode: "hybrid",
4019
+ };
4020
+ }
4021
+ if (tokenResult.matches.length) {
4022
+ return { result: { query: opts.query.trim(), top_k: opts.topK, mode: "token", matches: merged }, mode: "token" };
4023
+ }
4024
+ return { result: textResult, mode: textResult.mode === "fts" ? "fts" : "like" };
4025
+ }
4026
+ async function semanticSearchHybridInternal(opts) {
4027
+ const lexical = chooseLexicalResult(opts).result;
4028
+ if (!embeddingsEnabled)
4029
+ return lexical;
4030
+ const embeddingsResult = await Promise.race([
4031
+ semanticSearchInternal(opts),
4032
+ new Promise((resolve) => setTimeout(resolve, SEMANTIC_EMBEDDINGS_TIMEOUT_MS, null)),
4033
+ ]).catch((err) => {
4034
+ console.error("[vectormind] embeddings semantic_search failed; falling back:", err);
4035
+ return null;
4036
+ });
4037
+ if (!embeddingsResult)
4038
+ return lexical;
4039
+ const merged = mergeSemanticMatches([lexical.matches, embeddingsResult.matches], opts);
4040
+ return {
4041
+ query: opts.query.trim(),
4042
+ top_k: opts.topK,
4043
+ mode: merged.length ? "hybrid" : embeddingsResult.mode,
4044
+ matches: merged,
4045
+ };
2612
4046
  }
2613
4047
  function escapeRegExp(literal) {
2614
4048
  return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3313,7 +4747,7 @@ function listProjectFilesInternal(opts) {
3313
4747
  function buildServerInstructions() {
3314
4748
  return [
3315
4749
  "VectorMind MCP is available in this session. Use it to avoid guessing project context.",
3316
- "Development guideline scope: VectorMind instructions define development conventions, project-memory conventions, code-organization conventions, and delivery-quality expectations only. They are unrelated to AI access permissions, runtime permissions, command permissions, filesystem/network permissions, approval mechanisms, or sandbox behavior.",
4750
+ "Development guideline scope: VectorMind instructions define development conventions, project-memory conventions, code-organization conventions, and delivery-quality expectations.",
3317
4751
  "Project root resolution order: tool argument project_root (recommended for clients without roots/list), then VECTORMIND_ROOT (avoid hardcoding in global config), then MCP roots/list (best-effort; falls back quickly if unsupported), then process.cwd() (so start your MCP client in the project directory for per-project isolation).",
3318
4752
  "If root_source is fallback, file watching/indexing is disabled (pass project_root to enable per-project tracking).",
3319
4753
  "",
@@ -3329,6 +4763,9 @@ function buildServerInstructions() {
3329
4763
  "Built-in architecture and code-organization quality policy:",
3330
4764
  BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS,
3331
4765
  "",
4766
+ "Built-in requirement boundary and modularity quality policy:",
4767
+ BUILTIN_REQUIREMENT_BOUNDARY_AND_MODULARITY_INSTRUCTIONS,
4768
+ "",
3332
4769
  "Built-in frontend output-purity quality policy:",
3333
4770
  BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS,
3334
4771
  "",
@@ -3348,24 +4785,30 @@ function buildServerInstructions() {
3348
4785
  "- Tool outputs are compact by default. Pass format=json only when you need full structured data.",
3349
4786
  "- On every new conversation/session for analysis/design/development work: call bootstrap_context({ query: <current goal> }) first (or at least get_brain_dump()) to restore compact context and retrieve relevant matches from the local memory store (vector if enabled; otherwise FTS/LIKE).",
3350
4787
  " - Output is compact by default. Use include_content=true only when you truly need full text (it increases tokens).",
3351
- " - Tune output size with: requirements_limit/changes_limit/notes_limit/decisions_limit, preview_chars, pending_limit/pending_offset.",
4788
+ " - bootstrap_context/get_brain_dump always include a small recency anchor named current_context (latest active requirements, recent notes, and recent change intents) in addition to query matches; tune output size with: requirements_limit/changes_limit/notes_limit/decisions_limit/current_context_limit, preview_chars, pending_limit/pending_offset.",
3352
4789
  " - Prefer read_memory_item(id, offset, limit) to fetch full text on demand instead of returning large content in other tool outputs.",
3353
4790
  "- For pure execution-first tasks with explicit targets (for example compile/build/run/launch/package/publish/test rerun), you may skip retrieval and go straight to the minimum necessary shell or host tools unless code/context lookup is actually needed to unblock execution.",
3354
4791
  "- If rtk is installed or VectorMind's bundled RTK shim is verified (detect_rtk with gain_ok=true), prefix shell commands with the command returned by detect_rtk. Usually this is rtk (rtk git status, rtk npm run build, rtk rg ...); in npx/MCP-only installs it may be a package shim command such as node <...>/rtk-shim.js.",
3355
- "- If rtk is missing and the user asks to install it, use install_rtk first with dry_run=true to show the exact commands; execute with dry_run=false only after the user clearly approves installation/init choices.",
3356
- "- To read local Codex skill/prompt/rule files (for example SKILL.md under CODEX_HOME or AGENTS_HOME), prefer read_codex_text_file({ path }) instead of assuming a filesystem MCP resource server exists.",
4792
+ "- If rtk is missing and the user asks to install it, use install_rtk first with dry_run=true to show the exact commands; execute with dry_run=false only when the user explicitly asks to install/init.",
4793
+ "- To read local Codex skill/prompt/rule files (for example SKILL.md under CODEX_HOME or AGENTS_HOME), prefer read_codex_text_file({ path }) instead of assuming another local-file MCP resource server exists.",
3357
4794
  "- For project file/directory browsing, prefer list_project_files({ path, recursive?, max_depth? }) over shelling out to Get-ChildItem/ls. It respects ignore rules and keeps output bounded.",
3358
4795
  "- For small/medium raw file reads, prefer read_file_text({ path, offset?, max_chars? }) over Get-Content -Raw. Use read_file_lines(...) when you need deterministic line ranges or the file may be large.",
3359
4796
  "- For raw repo text search with exact file+line+col matches, prefer grep({ query: <pattern> }). It uses ripgrep against real project files when available, applies built-in noise filters, and only falls back to indexed search if ripgrep is unavailable.",
3360
4797
  "- To read a bounded segment of a file, prefer read_file_lines({ path: <file>, from_line/to_line or total_count }) over unbounded file reads.",
3361
4798
  "- BEFORE editing code: call start_requirement(title, background) to set the active requirement.",
4799
+ " - When the user names a narrow feature/domain, pass scope_allow/allowed_paths and scope_deny/denied_paths when useful. These are generic project-specific scope boundaries, not business-specific built-ins.",
4800
+ "- BEFORE editing once target files/modules are known: call preflight_change_scope(intent, files/planned_files, optional scope_allow/scope_deny/allowed_paths/denied_paths). If ok=false/safe_to_edit=false or it returns development_warnings, stop before editing and narrow the plan unless the user explicitly expands the requirement.",
4801
+ "- Treat the active requirement as the only change boundary. Do not add extra business behavior, new flows, new fields, new interfaces, or touch completed/related features unless the user explicitly asked or the change is strictly necessary.",
4802
+ "- Do not keep piling new feature code into a large single file. Prefer small modules/services/components; if an implementation file is already large, split it before adding more responsibilities.",
3362
4803
  "- AFTER editing + saving: call get_pending_changes() to see unsynced files, then call sync_change_intent(intent, files). (You can omit files to auto-link all pending changes.)",
4804
+ "- If preflight_change_scope, read_file_lines, grep, query_codebase, get_pending_changes, or sync_change_intent returns development_warnings, address those warnings before continuing or explain why the current requirement truly needs that scope.",
3363
4805
  "- After major milestones/decisions: call upsert_project_summary(summary) and/or add_note(...) to persist durable context locally.",
3364
4806
  "- When a requirement or user decision changes/reverses an older behavior, call upsert_decision(key, title, content, supersedes_req_ids?/supersedes_memory_ids?) and/or supersede_memory(...). Current decisions are shown in bootstrap_context/get_brain_dump and superseded memories are hidden from default semantic recall so stale requirements do not override newer facts.",
3365
4807
  "- If the user states a durable project convention (build commands, frameworks, naming rules, output paths): call upsert_convention(key, content, tags) so it is applied in future sessions.",
3366
4808
  "- When you need full text for a specific note/summary/match: call read_memory_item(id, offset, limit) and page through it.",
3367
4809
  "- When asked to locate code (class/function/type): call query_codebase(query) instead of guessing.",
3368
- "- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing.",
4810
+ "- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing. It blends lexical/FTS recall with embeddings when enabled, so recent explicit wording and durable decisions are not hidden by older semantically similar matches.",
4811
+ "- VectorMind automatically runs small, throttled memory maintenance to compact old completed history and prune stale indexes in long-lived projects. For large repos that feel slow, call maintain_memory({ dry_run: true }) first, then maintain_memory({ dry_run: false }) if the plan looks correct.",
3369
4812
  "- Use get_token_savings({ format: 'compact' }) when you need to verify how many tokens VectorMind compact outputs saved.",
3370
4813
  "",
3371
4814
  "If tool output conflicts with assumptions, trust the tool output.",
@@ -3483,6 +4926,15 @@ function initMemoryItemsFts() {
3483
4926
  ftsAvailable = false;
3484
4927
  }
3485
4928
  }
4929
+ function columnExists(table, column) {
4930
+ try {
4931
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all();
4932
+ return rows.some((row) => row.name === column);
4933
+ }
4934
+ catch {
4935
+ return false;
4936
+ }
4937
+ }
3486
4938
  function initDatabase() {
3487
4939
  const vmDir = path.join(projectRoot, ".vectormind");
3488
4940
  try {
@@ -3535,7 +4987,8 @@ function initDatabase() {
3535
4987
  title TEXT NOT NULL,
3536
4988
  status TEXT DEFAULT 'active',
3537
4989
  context_data TEXT,
3538
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
4990
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
4991
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
3539
4992
  );
3540
4993
 
3541
4994
  CREATE TABLE IF NOT EXISTS change_logs (
@@ -3634,6 +5087,49 @@ function initDatabase() {
3634
5087
 
3635
5088
  CREATE INDEX IF NOT EXISTS idx_token_savings_tool
3636
5089
  ON token_savings(tool);
5090
+
5091
+ CREATE TABLE IF NOT EXISTS memory_item_archive (
5092
+ memory_id INTEGER PRIMARY KEY,
5093
+ original_kind TEXT NOT NULL,
5094
+ original_title TEXT,
5095
+ original_content TEXT NOT NULL,
5096
+ original_file_path TEXT,
5097
+ original_start_line INTEGER,
5098
+ original_end_line INTEGER,
5099
+ original_req_id INTEGER,
5100
+ original_metadata_json TEXT,
5101
+ original_content_hash TEXT,
5102
+ original_created_at DATETIME,
5103
+ original_updated_at DATETIME,
5104
+ archive_reason TEXT NOT NULL,
5105
+ compacted_into_id INTEGER,
5106
+ archived_at DATETIME DEFAULT CURRENT_TIMESTAMP
5107
+ );
5108
+
5109
+ CREATE INDEX IF NOT EXISTS idx_memory_item_archive_compacted_into
5110
+ ON memory_item_archive(compacted_into_id);
5111
+
5112
+ CREATE TABLE IF NOT EXISTS meta_kv (
5113
+ key TEXT PRIMARY KEY,
5114
+ value TEXT NOT NULL,
5115
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
5116
+ );
5117
+ `);
5118
+ if (!columnExists("requirements", "updated_at")) {
5119
+ db.exec(`ALTER TABLE requirements ADD COLUMN updated_at DATETIME`);
5120
+ db.exec(`UPDATE requirements SET updated_at = COALESCE(created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL`);
5121
+ }
5122
+ db.exec(`
5123
+ UPDATE requirements SET updated_at = COALESCE(updated_at, created_at, CURRENT_TIMESTAMP) WHERE updated_at IS NULL;
5124
+ CREATE INDEX IF NOT EXISTS idx_requirements_status_updated_at
5125
+ ON requirements(status, updated_at DESC, id DESC);
5126
+ CREATE TRIGGER IF NOT EXISTS vectormind_requirements_touch_updated_at
5127
+ AFTER UPDATE ON requirements
5128
+ FOR EACH ROW
5129
+ WHEN NEW.updated_at = OLD.updated_at
5130
+ BEGIN
5131
+ UPDATE requirements SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
5132
+ END;
3637
5133
  `);
3638
5134
  initMemoryItemsFts();
3639
5135
  insertRequirementStmt = db.prepare(`INSERT INTO requirements (title, context_data, status) VALUES (?, ?, 'active')`);
@@ -3642,11 +5138,16 @@ function initDatabase() {
3642
5138
  getActiveRequirementStmt = db.prepare(`SELECT id, title, status, context_data, created_at
3643
5139
  FROM requirements
3644
5140
  WHERE status = 'active'
3645
- ORDER BY created_at DESC, id DESC
5141
+ ORDER BY updated_at DESC, created_at DESC, id DESC
3646
5142
  LIMIT 1`);
5143
+ listActiveRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, created_at
5144
+ FROM requirements
5145
+ WHERE status = 'active'
5146
+ ORDER BY updated_at DESC, created_at DESC, id DESC
5147
+ LIMIT ?`);
3647
5148
  listRecentRequirementsStmt = db.prepare(`SELECT id, title, status, context_data, created_at
3648
5149
  FROM requirements
3649
- ORDER BY created_at DESC, id DESC
5150
+ ORDER BY updated_at DESC, created_at DESC, id DESC
3650
5151
  LIMIT ?`);
3651
5152
  completeAllActiveRequirementMemoryItemsStmt = db.prepare(`UPDATE memory_items
3652
5153
  SET metadata_json = ?, updated_at = CURRENT_TIMESTAMP
@@ -3723,6 +5224,11 @@ function initDatabase() {
3723
5224
  WHERE kind = 'note'
3724
5225
  ORDER BY updated_at DESC, id DESC
3725
5226
  LIMIT ?`);
5227
+ listRecentContextItemsStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
5228
+ FROM memory_items
5229
+ WHERE kind IN ('note', 'requirement', 'change_intent')
5230
+ ORDER BY updated_at DESC, id DESC
5231
+ LIMIT ?`);
3726
5232
  getLatestChangeIntentForFileStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
3727
5233
  FROM memory_items
3728
5234
  WHERE kind = 'change_intent' AND file_path = ?
@@ -3801,6 +5307,10 @@ function initDatabase() {
3801
5307
  FROM token_savings
3802
5308
  ORDER BY created_at DESC, id DESC
3803
5309
  LIMIT ?`);
5310
+ getKvStmt = db.prepare(`SELECT value FROM meta_kv WHERE key = ?`);
5311
+ setKvStmt = db.prepare(`INSERT INTO meta_kv (key, value)
5312
+ VALUES (?, ?)
5313
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`);
3804
5314
  indexFileSymbolsTx = db.transaction((filePath, symbols) => {
3805
5315
  deleteSymbolsForFileStmt.run(filePath);
3806
5316
  for (const s of symbols) {
@@ -3816,6 +5326,9 @@ function initDatabase() {
3816
5326
  // Clean up common "file name noise" recorded by older versions.
3817
5327
  // (These files are ignored by current index rules; keep the DB consistent automatically.)
3818
5328
  pruneFilenameNoiseIndexes();
5329
+ // Bounded, throttled maintenance keeps long-lived project memory fast without
5330
+ // deleting durable decisions/conventions/project summaries.
5331
+ runAutoMaintenanceIfDue();
3819
5332
  }
3820
5333
  function initWatcher() {
3821
5334
  watcherReady = false;
@@ -3947,14 +5460,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3947
5460
  tools: [
3948
5461
  {
3949
5462
  name: "start_requirement",
3950
- description: "MUST call BEFORE editing code. Starts/activates a requirement so all subsequent code changes can be linked to a concrete intent (do not edit code without an active requirement).",
5463
+ description: "MUST call BEFORE editing code. Starts/activates the concrete user requirement so subsequent changes stay inside that requirement boundary and do not accumulate unrelated work. Supports scope_allow/scope_deny and allowed_paths/denied_paths to prevent unrelated domain drift.",
3951
5464
  inputSchema: toJsonSchemaCompat(StartRequirementArgsSchema),
3952
5465
  },
3953
5466
  {
3954
5467
  name: "sync_change_intent",
3955
- description: "MUST call AFTER you edit code and save files. Archives the intent summary and links affected files to the current active requirement. If you omit files, the server will auto-link all pending changed files since the last sync.",
5468
+ description: "MUST call AFTER you edit code and save files. Archives the intent summary, links affected files to the current active requirement, and returns development_warnings for oversized files, broad change scope, or missing file targets.",
3956
5469
  inputSchema: toJsonSchemaCompat(SyncChangeIntentArgsSchema),
3957
5470
  },
5471
+ {
5472
+ name: "preflight_change_scope",
5473
+ description: "MUST call BEFORE editing once you know the intended files/modules. Checks planned files against the active requirement and optional generic scope_allow/scope_deny/allowed_paths/denied_paths. If ok=false/safe_to_edit=false, stop before editing and narrow the plan or scope contract.",
5474
+ inputSchema: toJsonSchemaCompat(PreflightChangeScopeArgsSchema),
5475
+ },
3958
5476
  {
3959
5477
  name: "get_brain_dump",
3960
5478
  description: "Restore recent requirements/changes/notes/summary/pending changes. Prefer bootstrap_context() at session start when you also want recall from the local memory store.",
@@ -3962,12 +5480,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3962
5480
  },
3963
5481
  {
3964
5482
  name: "bootstrap_context",
3965
- description: "MUST call at the start of every new chat/session. Returns brain dump + pending changes, and (if you pass query) matches from the local memory store to avoid guessing.",
5483
+ description: "MUST call at the start of every new chat/session. Returns brain dump + pending changes + development_warnings, and (if you pass query) matches from the local memory store to avoid guessing.",
3966
5484
  inputSchema: toJsonSchemaCompat(BootstrapContextArgsSchema),
3967
5485
  },
3968
5486
  {
3969
5487
  name: "get_pending_changes",
3970
- description: "List files that changed locally but have not been acknowledged by sync_change_intent yet. Use this to see what needs syncing (or omit files in sync_change_intent to auto-link them).",
5488
+ description: "List files that changed locally but have not been acknowledged by sync_change_intent yet. Also returns development_warnings to catch god-file growth, broad change scope, and requirement-boundary drift.",
3971
5489
  inputSchema: toJsonSchemaCompat(GetPendingChangesArgsSchema),
3972
5490
  },
3973
5491
  {
@@ -4012,7 +5530,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
4012
5530
  },
4013
5531
  {
4014
5532
  name: "grep",
4015
- description: "Repo text search with precise file/line/col matches, powered by ripgrep against real project files plus built-in noise filters. Falls back to indexed search only when ripgrep is unavailable.",
5533
+ description: "Repo text search with precise file/line/col matches, powered by ripgrep against real project files plus built-in noise filters. Falls back to indexed search only when ripgrep is unavailable. Returns development_warnings for cross-project paths or huge implementation-file matches.",
4016
5534
  inputSchema: toJsonSchemaCompat(GrepArgsSchema),
4017
5535
  },
4018
5536
  {
@@ -4022,12 +5540,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
4022
5540
  },
4023
5541
  {
4024
5542
  name: "read_codex_text_file",
4025
- description: "Read bounded text from local Codex/agents files such as SKILL.md, prompt files, and rules under CODEX_HOME/AGENTS_HOME. Prefer this over assuming a filesystem MCP resource server exists.",
5543
+ description: "Read bounded text from local Codex/agents files such as SKILL.md, prompt files, and rules under CODEX_HOME/AGENTS_HOME. Prefer this over assuming another local-file MCP resource server exists.",
4026
5544
  inputSchema: toJsonSchemaCompat(ReadCodexTextFileArgsSchema),
4027
5545
  },
4028
5546
  {
4029
5547
  name: "read_file_lines",
4030
- description: "Read a specific line range from a file under project_root (with strict size limits). Prefer this over Get-Content for deterministic reads.",
5548
+ description: "Read a specific line range from a file under project_root (with strict size limits). Prefer this over Get-Content for deterministic reads. Returns development_warnings when the target is a huge implementation file.",
4031
5549
  inputSchema: toJsonSchemaCompat(ReadFileLinesArgsSchema),
4032
5550
  },
4033
5551
  {
@@ -4037,7 +5555,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
4037
5555
  },
4038
5556
  {
4039
5557
  name: "query_codebase",
4040
- description: "Search the symbol index for class/function/type names (or substrings) to locate definitions by file path and signature. Use this when you need to find codedo not guess locations.",
5558
+ description: "Search the symbol index for class/function/type names (or substrings) to locate definitions by file path and signature. Use this when you need to find code; do not guess locations. Returns development_warnings when matches point at huge implementation files.",
4041
5559
  inputSchema: toJsonSchemaCompat(QueryCodebaseArgsSchema),
4042
5560
  },
4043
5561
  {
@@ -4070,6 +5588,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
4070
5588
  description: "Semantic search across the local memory store (requirements, change intents, notes, project summary, and indexed code/doc chunks). Use this to retrieve relevant context instead of guessing.",
4071
5589
  inputSchema: toJsonSchemaCompat(SemanticSearchArgsSchema),
4072
5590
  },
5591
+ {
5592
+ name: "maintain_memory",
5593
+ description: "Compact old completed memory and prune stale/noisy indexes to keep long-lived large projects fast. Defaults to dry_run=true; automatic safe maintenance also runs periodically.",
5594
+ inputSchema: toJsonSchemaCompat(MaintainMemoryArgsSchema),
5595
+ },
4073
5596
  {
4074
5597
  name: "prune_index",
4075
5598
  description: "Prune noisy auto-indexed items (code_chunk/doc_chunk + symbols). Useful after tightening ignore rules to shrink the index and improve search relevance.",
@@ -4086,6 +5609,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4086
5609
  if (toolName === "start_requirement") {
4087
5610
  const args = StartRequirementArgsSchema.parse(rawArgs);
4088
5611
  flushPendingChangeBuffer();
5612
+ const scope_contract = buildRequirementScopeContract({
5613
+ title: args.title,
5614
+ background: args.background,
5615
+ scope_allow: args.scope_allow,
5616
+ scope_deny: args.scope_deny,
5617
+ allowed_paths: args.allowed_paths,
5618
+ denied_paths: args.denied_paths,
5619
+ });
5620
+ const development_warnings = buildRequirementStartWarnings({
5621
+ title: args.title,
5622
+ background: args.background,
5623
+ close_previous: args.close_previous,
5624
+ });
4089
5625
  if (args.close_previous) {
4090
5626
  try {
4091
5627
  completeAllActiveRequirementsStmt.run();
@@ -4099,13 +5635,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4099
5635
  const id = Number(info.lastInsertRowid);
4100
5636
  const background = args.background?.trim() ?? "";
4101
5637
  const content = background ? `${args.title}\n\n${background}` : args.title;
4102
- const memoryInfo = insertMemoryItemStmt.run("requirement", args.title, content, null, null, null, id, safeJson({ status: "active" }), sha256Hex(content));
5638
+ const memoryInfo = insertMemoryItemStmt.run("requirement", args.title, content, null, null, null, id, safeJson({ status: "active", scope_contract }), sha256Hex(content));
4103
5639
  const memory_id = Number(memoryInfo.lastInsertRowid);
4104
5640
  enqueueEmbedding(memory_id);
4105
5641
  logActivity("start_requirement", {
4106
5642
  req_id: id,
4107
5643
  title: args.title,
4108
5644
  closed_previous: args.close_previous,
5645
+ scope_contract,
5646
+ development_warnings: development_warnings.length,
4109
5647
  });
4110
5648
  return {
4111
5649
  content: [
@@ -4116,6 +5654,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4116
5654
  requirement: { id, title: args.title },
4117
5655
  memory_item: { id: memory_id },
4118
5656
  closed_previous: args.close_previous,
5657
+ scope_contract,
5658
+ development_warnings,
4119
5659
  }),
4120
5660
  },
4121
5661
  ],
@@ -4257,6 +5797,78 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4257
5797
  ],
4258
5798
  };
4259
5799
  }
5800
+ if (toolName === "maintain_memory") {
5801
+ const args = MaintainMemoryArgsSchema.parse(rawArgs);
5802
+ flushPendingChangeBuffer();
5803
+ const result = runMemoryMaintenance(args, "manual");
5804
+ return {
5805
+ content: [
5806
+ {
5807
+ type: "text",
5808
+ text: toolCompactOrJson("maintain_memory", result, compactMaintenanceText(result), args.format),
5809
+ },
5810
+ ],
5811
+ };
5812
+ }
5813
+ if (toolName === "preflight_change_scope") {
5814
+ const args = PreflightChangeScopeArgsSchema.parse(rawArgs);
5815
+ flushPendingChangeBuffer();
5816
+ const files = (args.files ?? args.planned_files ?? []).filter((f) => typeof f === "string" && f.length > 0);
5817
+ const active = getActiveRequirementStmt.get();
5818
+ const explicitContract = buildRequirementScopeContract({
5819
+ title: active?.title ?? "",
5820
+ background: active?.context_data ?? "",
5821
+ scope_allow: args.scope_allow,
5822
+ scope_deny: args.scope_deny,
5823
+ allowed_paths: args.allowed_paths,
5824
+ denied_paths: args.denied_paths,
5825
+ });
5826
+ const fileInputs = files.map((file_path) => ({ file_path }));
5827
+ const development_warnings = [
5828
+ ...buildDevelopmentWarnings(fileInputs, { includeUnspecified: fileInputs.length === 0 }),
5829
+ ...buildScopeDriftWarnings({
5830
+ requirement: active,
5831
+ contract: explicitContract,
5832
+ intent: args.intent,
5833
+ files: fileInputs,
5834
+ includeMissingContractHint: true,
5835
+ }),
5836
+ ];
5837
+ const scope_contract = mergeScopeContracts(active ? getRequirementScopeContract(active.id) : null, explicitContract);
5838
+ logActivity("preflight_change_scope", {
5839
+ req_id: active?.id ?? null,
5840
+ intent_preview: makePreviewText(args.intent, 200),
5841
+ files: files.slice(0, 25),
5842
+ files_total: files.length,
5843
+ development_warnings: development_warnings.length,
5844
+ });
5845
+ const hasTargetFiles = fileInputs.length > 0;
5846
+ const hasBlockingWarnings = development_warnings.some((w) => w.severity === "blocker" || w.severity === "warning");
5847
+ const safeToEdit = hasTargetFiles && !hasBlockingWarnings;
5848
+ const recommendedAction = !hasTargetFiles
5849
+ ? "Identify the intended target files/modules and rerun preflight_change_scope before editing."
5850
+ : hasBlockingWarnings
5851
+ ? "Stop before editing. Narrow the planned files or explicitly expand the current requirement/scope contract."
5852
+ : "Planned files are within the current generic scope checks.";
5853
+ const outputValue = {
5854
+ ok: safeToEdit,
5855
+ safe_to_edit: safeToEdit,
5856
+ recommended_action: recommendedAction,
5857
+ active_requirement: active ? { id: active.id, title: active.title } : null,
5858
+ intent: args.intent,
5859
+ files: files.map(normalizeToDbPath),
5860
+ scope_contract,
5861
+ development_warnings,
5862
+ };
5863
+ return {
5864
+ content: [
5865
+ {
5866
+ type: "text",
5867
+ text: toolCompactOrJson("preflight_change_scope", outputValue, compactPreflightChangeScopeText(outputValue), args.format),
5868
+ },
5869
+ ],
5870
+ };
5871
+ }
4260
5872
  if (toolName === "sync_change_intent") {
4261
5873
  const args = SyncChangeIntentArgsSchema.parse(rawArgs);
4262
5874
  flushPendingChangeBuffer();
@@ -4341,12 +5953,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4341
5953
  }
4342
5954
  });
4343
5955
  insertTx();
5956
+ const development_warnings = [
5957
+ ...buildDevelopmentWarnings(synced_files, {
5958
+ includeUnspecified: synced_files.some((f) => f.file_path === "(unspecified)"),
5959
+ }),
5960
+ ...buildScopeDriftWarnings({
5961
+ requirement: active,
5962
+ intent: args.intent,
5963
+ files: synced_files,
5964
+ }),
5965
+ ];
4344
5966
  logActivity("sync_change_intent", {
4345
5967
  req_id: active.id,
4346
5968
  title: active.title,
4347
5969
  intent_preview: makePreviewText(args.intent, 200),
4348
5970
  files: synced_files.slice(0, 25),
4349
5971
  files_total: synced_files.length,
5972
+ development_warnings: development_warnings.length,
4350
5973
  });
4351
5974
  return {
4352
5975
  content: [
@@ -4357,6 +5980,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4357
5980
  linked_to_requirement: { id: active.id, title: active.title },
4358
5981
  synced_files,
4359
5982
  created,
5983
+ development_warnings,
4360
5984
  }),
4361
5985
  },
4362
5986
  ],
@@ -4373,6 +5997,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4373
5997
  const notesLimit = args.notes_limit;
4374
5998
  const conventionsLimit = args.conventions_limit;
4375
5999
  const decisionsLimit = args.decisions_limit;
6000
+ const currentContextLimit = args.current_context_limit;
4376
6001
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
4377
6002
  const items = recent.map((req) => {
4378
6003
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -4388,6 +6013,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4388
6013
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
4389
6014
  const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
4390
6015
  const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
6016
+ const current_context = getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars);
4391
6017
  const pending_offset = args.pending_offset;
4392
6018
  const pending_limit = args.pending_limit;
4393
6019
  const pendingDbRows = listPendingChangesStmt.all();
@@ -4395,13 +6021,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4395
6021
  const pending_total = mergedPending.total;
4396
6022
  const pending_truncated = mergedPending.truncated;
4397
6023
  const pending_changes = mergedPending.page;
6024
+ const activeForScope = getActiveRequirementStmt.get();
6025
+ const development_warnings = [
6026
+ ...buildDevelopmentWarnings(pending_changes),
6027
+ ...(activeForScope
6028
+ ? buildScopeDriftWarnings({ requirement: activeForScope, files: pending_changes })
6029
+ : []),
6030
+ ];
4398
6031
  const q = args.query?.trim() ?? "";
6032
+ const semanticKinds = args.kinds?.length ? args.kinds : BOOTSTRAP_DEFAULT_CONTEXT_KINDS;
4399
6033
  const semantic = q
4400
6034
  ? await Promise.race([
4401
6035
  semanticSearchHybridInternal({
4402
6036
  query: q,
4403
6037
  topK: args.top_k,
4404
- kinds: args.kinds?.length ? args.kinds : null,
6038
+ kinds: semanticKinds,
4405
6039
  includeContent,
4406
6040
  previewChars,
4407
6041
  contentMaxChars,
@@ -4418,6 +6052,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4418
6052
  pending_returned: pending_changes.length,
4419
6053
  requirements_returned: items.length,
4420
6054
  decisions_returned: decisions.length,
6055
+ current_context_returned: current_context.length,
4421
6056
  conventions_returned: conventions.length,
4422
6057
  semantic_mode: semantic?.mode ?? null,
4423
6058
  semantic_matches: semantic?.matches?.length ?? 0,
@@ -4444,17 +6079,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4444
6079
  changes_limit: changesLimit,
4445
6080
  notes_limit: notesLimit,
4446
6081
  decisions_limit: decisionsLimit,
6082
+ current_context_limit: currentContextLimit,
4447
6083
  conventions_limit: conventionsLimit,
4448
6084
  },
4449
6085
  project_summary,
4450
6086
  decisions,
4451
6087
  conventions,
6088
+ current_context,
4452
6089
  recent_notes,
4453
6090
  pending_total,
4454
6091
  pending_offset,
4455
6092
  pending_limit,
4456
6093
  pending_truncated,
4457
6094
  pending_changes,
6095
+ development_warnings,
4458
6096
  items,
4459
6097
  semantic,
4460
6098
  };
@@ -4478,6 +6116,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4478
6116
  const notesLimit = args.notes_limit;
4479
6117
  const conventionsLimit = args.conventions_limit;
4480
6118
  const decisionsLimit = args.decisions_limit;
6119
+ const currentContextLimit = args.current_context_limit;
4481
6120
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
4482
6121
  const items = recent.map((req) => {
4483
6122
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -4493,6 +6132,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4493
6132
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
4494
6133
  const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
4495
6134
  const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
6135
+ const current_context = getCurrentContextPreviews(currentContextLimit, previewChars, contentMaxChars);
4496
6136
  const pending_offset = args.pending_offset;
4497
6137
  const pending_limit = args.pending_limit;
4498
6138
  const pendingDbRows = listPendingChangesStmt.all();
@@ -4500,12 +6140,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4500
6140
  const pending_total = mergedPending.total;
4501
6141
  const pending_truncated = mergedPending.truncated;
4502
6142
  const pending_changes = mergedPending.page;
6143
+ const activeForScope = getActiveRequirementStmt.get();
6144
+ const development_warnings = [
6145
+ ...buildDevelopmentWarnings(pending_changes),
6146
+ ...(activeForScope
6147
+ ? buildScopeDriftWarnings({ requirement: activeForScope, files: pending_changes })
6148
+ : []),
6149
+ ];
4503
6150
  logActivity("get_brain_dump", {
4504
6151
  pending_total,
4505
6152
  pending_returned: pending_changes.length,
4506
6153
  requirements_returned: items.length,
4507
6154
  notes_returned: recent_notes.length,
4508
6155
  decisions_returned: decisions.length,
6156
+ current_context_returned: current_context.length,
4509
6157
  conventions_returned: conventions.length,
4510
6158
  });
4511
6159
  const outputValue = {
@@ -4530,17 +6178,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4530
6178
  changes_limit: changesLimit,
4531
6179
  notes_limit: notesLimit,
4532
6180
  decisions_limit: decisionsLimit,
6181
+ current_context_limit: currentContextLimit,
4533
6182
  conventions_limit: conventionsLimit,
4534
6183
  },
4535
6184
  project_summary,
4536
6185
  decisions,
4537
6186
  conventions,
6187
+ current_context,
4538
6188
  recent_notes,
4539
6189
  pending_total,
4540
6190
  pending_offset,
4541
6191
  pending_limit,
4542
6192
  pending_truncated,
4543
6193
  pending_changes,
6194
+ development_warnings,
4544
6195
  items,
4545
6196
  semantic: null,
4546
6197
  };
@@ -4563,18 +6214,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4563
6214
  const total = mergedPending.total;
4564
6215
  const truncated = mergedPending.truncated;
4565
6216
  const pending = mergedPending.page;
6217
+ const activeForScope = getActiveRequirementStmt.get();
6218
+ const development_warnings = [
6219
+ ...buildDevelopmentWarnings(pending),
6220
+ ...(activeForScope ? buildScopeDriftWarnings({ requirement: activeForScope, files: pending }) : []),
6221
+ ];
4566
6222
  logActivity("get_pending_changes", {
4567
6223
  total,
4568
6224
  offset,
4569
6225
  limit,
4570
6226
  returned: pending.length,
4571
6227
  truncated,
6228
+ development_warnings: development_warnings.length,
4572
6229
  });
4573
6230
  return {
4574
6231
  content: [
4575
6232
  {
4576
6233
  type: "text",
4577
- text: toolJson({ ok: true, total, offset, limit, truncated, pending }),
6234
+ text: toolJson({ ok: true, total, offset, limit, truncated, pending, development_warnings }),
4578
6235
  },
4579
6236
  ],
4580
6237
  };
@@ -4765,6 +6422,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4765
6422
  const includePaths = args.include_paths?.length ? args.include_paths : null;
4766
6423
  const excludePaths = args.exclude_paths?.length ? args.exclude_paths : null;
4767
6424
  const maxResults = args.max_results;
6425
+ const development_warnings = [
6426
+ ...buildCrossProjectPathWarnings(includePaths),
6427
+ ...buildCrossProjectPathWarnings(excludePaths),
6428
+ ];
4768
6429
  const caseSensitive = args.case_sensitive ?? (smartCase ? hasUppercaseAscii(q) : true);
4769
6430
  const ripgrepResult = runRipgrepSearch({
4770
6431
  query: q,
@@ -4776,6 +6437,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4776
6437
  maxResults,
4777
6438
  });
4778
6439
  if (ripgrepResult.ok) {
6440
+ const grepDevelopmentWarnings = [
6441
+ ...development_warnings,
6442
+ ...buildMatchedFileDevelopmentWarnings(ripgrepResult.matches.map((m) => m.file_path)),
6443
+ ];
4779
6444
  logActivity("grep", {
4780
6445
  backend: ripgrepResult.backend,
4781
6446
  rg_command: ripgrepResult.rg_command,
@@ -4788,6 +6453,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4788
6453
  matches: ripgrepResult.matches.length,
4789
6454
  total_matches: ripgrepResult.total_matches,
4790
6455
  truncated: ripgrepResult.truncated,
6456
+ development_warnings: grepDevelopmentWarnings.length,
4791
6457
  });
4792
6458
  const outputValue = {
4793
6459
  ok: true,
@@ -4802,6 +6468,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4802
6468
  matches: ripgrepResult.matches,
4803
6469
  total_matches: ripgrepResult.total_matches,
4804
6470
  truncated: ripgrepResult.truncated,
6471
+ development_warnings: grepDevelopmentWarnings,
4805
6472
  };
4806
6473
  return {
4807
6474
  content: [
@@ -4867,6 +6534,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4867
6534
  ],
4868
6535
  };
4869
6536
  }
6537
+ const grepDevelopmentWarnings = [
6538
+ ...development_warnings,
6539
+ ...buildMatchedFileDevelopmentWarnings(indexedResult.matches.map((m) => m.file_path)),
6540
+ ];
4870
6541
  logActivity("grep", {
4871
6542
  backend: indexedResult.backend,
4872
6543
  fallback_reason: "ripgrep_unavailable",
@@ -4883,6 +6554,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4883
6554
  candidates_scanned: indexedResult.candidates.scanned,
4884
6555
  matches: indexedResult.matches.length,
4885
6556
  truncated: indexedResult.truncated,
6557
+ development_warnings: grepDevelopmentWarnings.length,
4886
6558
  });
4887
6559
  const outputValue = {
4888
6560
  ok: true,
@@ -4901,6 +6573,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4901
6573
  candidates: indexedResult.candidates,
4902
6574
  matches: indexedResult.matches,
4903
6575
  truncated: indexedResult.truncated,
6576
+ development_warnings: grepDevelopmentWarnings,
4904
6577
  };
4905
6578
  return {
4906
6579
  content: [
@@ -5020,6 +6693,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5020
6693
  total_chars: result.totalChars,
5021
6694
  truncated: result.truncated,
5022
6695
  });
6696
+ const development_warnings = buildFileReadDevelopmentWarnings(resolved.dbFilePath, resolved.absPath, st);
5023
6697
  const outputValue = {
5024
6698
  ok: true,
5025
6699
  file_path: resolved.dbFilePath,
@@ -5027,6 +6701,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5027
6701
  returned_chars: result.returnedChars,
5028
6702
  total_chars: result.totalChars,
5029
6703
  truncated: result.truncated,
6704
+ development_warnings,
5030
6705
  text: result.text,
5031
6706
  };
5032
6707
  return {
@@ -5156,6 +6831,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5156
6831
  returned: result.returned,
5157
6832
  truncated: result.truncated,
5158
6833
  });
6834
+ const development_warnings = buildFileReadDevelopmentWarnings(resolved.dbFilePath, resolved.absPath, st);
5159
6835
  const outputValue = {
5160
6836
  ok: true,
5161
6837
  file_path: resolved.dbFilePath,
@@ -5163,6 +6839,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5163
6839
  to_line: toLine,
5164
6840
  returned: result.returned,
5165
6841
  truncated: result.truncated,
6842
+ development_warnings,
5166
6843
  text: result.text,
5167
6844
  };
5168
6845
  return {
@@ -5181,12 +6858,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5181
6858
  const like = `%${escaped}%`;
5182
6859
  const rows = searchSymbolsStmt.all(like, like, q, like, 250);
5183
6860
  const filtered = rows.filter((r) => !shouldIgnoreDbFilePath(r.file_path)).slice(0, 50);
6861
+ const development_warnings = buildMatchedFileDevelopmentWarnings(filtered.map((m) => m.file_path));
5184
6862
  logActivity("query_codebase", {
5185
6863
  query: q,
5186
6864
  matches: filtered.length,
6865
+ development_warnings: development_warnings.length,
5187
6866
  sample: filtered.slice(0, 10).map((m) => ({ name: m.name, type: m.type, file_path: m.file_path })),
5188
6867
  });
5189
- const outputValue = { ok: true, query: q, matches: filtered };
6868
+ const outputValue = { ok: true, query: q, matches: filtered, development_warnings };
5190
6869
  return {
5191
6870
  content: [
5192
6871
  {