@coreyuan/vector-mind 1.0.39 → 1.0.42

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
@@ -16,7 +16,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
16
16
  import { BUILTIN_CONVENTIONS } from "./builtin-conventions.js";
17
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";
18
18
  const SERVER_NAME = "vector-mind";
19
- const SERVER_VERSION = "1.0.39";
19
+ const SERVER_VERSION = "1.0.42";
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());
@@ -164,9 +164,13 @@ let getConventionByKeyStmt;
164
164
  let insertConventionStmt;
165
165
  let updateConventionByIdStmt;
166
166
  let listConventionsStmt;
167
+ let upsertDecisionStmt;
168
+ let getDecisionByKeyStmt;
169
+ let listCurrentDecisionsStmt;
167
170
  let upsertProjectSummaryStmt;
168
171
  let getProjectSummaryStmt;
169
172
  let listRecentNotesStmt;
173
+ let getLatestChangeIntentForFileStmt;
170
174
  let deleteFileChunkItemsStmt;
171
175
  let getEmbeddingMetaStmt;
172
176
  let upsertEmbeddingStmt;
@@ -556,6 +560,89 @@ function shouldIgnoreDbFilePath(filePath) {
556
560
  return false;
557
561
  return pathHasIgnoredSegments(filePath);
558
562
  }
563
+ function isProbablyGitRepository() {
564
+ try {
565
+ return fs.existsSync(path.join(projectRoot, ".git"));
566
+ }
567
+ catch {
568
+ return false;
569
+ }
570
+ }
571
+ function normalizeGitStatusPath(raw) {
572
+ const first = raw.split("\0")[0] ?? "";
573
+ return first.trim().replace(/\\/g, "/").replace(/^"(.*)"$/, "$1");
574
+ }
575
+ function collectGitPendingChanges(limit) {
576
+ if (limit <= 0 || !isProbablyGitRepository())
577
+ return [];
578
+ const git = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=normal"], {
579
+ cwd: projectRoot,
580
+ encoding: "utf8",
581
+ timeout: 5000,
582
+ windowsHide: true,
583
+ maxBuffer: 2_000_000,
584
+ });
585
+ if (git.error || git.status !== 0 || !git.stdout)
586
+ return [];
587
+ const parts = git.stdout.split("\0").filter(Boolean);
588
+ const rows = [];
589
+ for (let i = 0; i < parts.length && rows.length < limit; i++) {
590
+ const rec = parts[i] ?? "";
591
+ const status = rec.slice(0, 2);
592
+ let rawPath = rec.slice(3);
593
+ if (status.startsWith("R") || status.startsWith("C")) {
594
+ // Porcelain -z rename/copy records include the destination in the next NUL field.
595
+ rawPath = parts[i + 1] ?? rawPath;
596
+ i += 1;
597
+ }
598
+ const filePath = normalizeGitStatusPath(rawPath);
599
+ if (!filePath || filePath === ".vectormind" || filePath.startsWith(".vectormind/"))
600
+ continue;
601
+ rows.push({
602
+ file_path: filePath,
603
+ last_event: status.includes("D") ? "unlink" : status === "??" ? "add" : "change",
604
+ updated_at: new Date().toISOString(),
605
+ source: "git",
606
+ git_status: status.trim() || "modified",
607
+ file_state_hash: getFileStateHash(filePath) ?? undefined,
608
+ });
609
+ }
610
+ return rows;
611
+ }
612
+ function mergePendingWithGit(pending, opts) {
613
+ const byPath = new Map();
614
+ for (const p of pending) {
615
+ if (shouldIgnoreDbFilePath(p.file_path))
616
+ continue;
617
+ byPath.set(p.file_path, { ...p, source: p.source ?? "watcher" });
618
+ }
619
+ const gitRows = collectGitPendingChanges(Math.max(500, opts.offset + opts.limit * 4));
620
+ for (const g of gitRows) {
621
+ const latestSyncedHash = getLatestSyncedFileHash(g.file_path);
622
+ if (latestSyncedHash && g.file_state_hash && latestSyncedHash === g.file_state_hash)
623
+ continue;
624
+ const existing = byPath.get(g.file_path);
625
+ if (!existing) {
626
+ byPath.set(g.file_path, g);
627
+ continue;
628
+ }
629
+ byPath.set(g.file_path, {
630
+ ...existing,
631
+ source: existing.source === "watcher" ? "watcher" : g.source,
632
+ git_status: g.git_status,
633
+ file_state_hash: g.file_state_hash,
634
+ });
635
+ }
636
+ const all = Array.from(byPath.values()).sort((a, b) => {
637
+ const at = Date.parse(a.updated_at) || 0;
638
+ const bt = Date.parse(b.updated_at) || 0;
639
+ if (bt !== at)
640
+ return bt - at;
641
+ return a.file_path.localeCompare(b.file_path);
642
+ });
643
+ const page = all.slice(opts.offset, opts.offset + opts.limit);
644
+ return { total: all.length, page, truncated: all.length > opts.offset + opts.limit };
645
+ }
559
646
  function pruneIgnoredPendingChanges() {
560
647
  if (!db)
561
648
  return;
@@ -1249,6 +1336,22 @@ const UpsertConventionArgsSchema = ProjectRootArgSchema.merge(z.object({
1249
1336
  content: z.string().min(1),
1250
1337
  tags: z.array(z.string().min(1)).optional(),
1251
1338
  }));
1339
+ const UpsertDecisionArgsSchema = ProjectRootArgSchema.merge(z.object({
1340
+ key: z.string().min(1),
1341
+ title: z.string().optional().default(""),
1342
+ content: z.string().min(1),
1343
+ tags: z.array(z.string().min(1)).optional(),
1344
+ supersedes_req_ids: z.array(z.number().int().positive()).optional(),
1345
+ supersedes_memory_ids: z.array(z.number().int().positive()).optional(),
1346
+ related_files: z.array(z.string().min(1)).optional(),
1347
+ }));
1348
+ const SupersedeMemoryArgsSchema = ProjectRootArgSchema.merge(z.object({
1349
+ superseded_req_ids: z.array(z.number().int().positive()).optional(),
1350
+ superseded_memory_ids: z.array(z.number().int().positive()).optional(),
1351
+ replacement_req_id: z.number().int().positive().optional(),
1352
+ replacement_memory_id: z.number().int().positive().optional(),
1353
+ reason: z.string().min(1),
1354
+ }));
1252
1355
  const DEFAULT_PENDING_LIMIT = 10;
1253
1356
  const MAX_PENDING_LIMIT = 2000;
1254
1357
  const PendingPagingSchema = z.object({
@@ -1267,11 +1370,14 @@ const DEFAULT_RECENT_REQUIREMENTS = 2;
1267
1370
  const DEFAULT_RECENT_CHANGES_PER_REQ = 3;
1268
1371
  const DEFAULT_RECENT_NOTES = 3;
1269
1372
  const DEFAULT_CONVENTIONS_LIMIT = 0;
1373
+ const DEFAULT_DECISIONS_LIMIT = 5;
1374
+ const MAX_DECISIONS_LIMIT = 50;
1270
1375
  const BrainDumpLimitsSchema = z.object({
1271
1376
  requirements_limit: z.number().int().min(1).max(20).optional().default(DEFAULT_RECENT_REQUIREMENTS),
1272
1377
  changes_limit: z.number().int().min(1).max(100).optional().default(DEFAULT_RECENT_CHANGES_PER_REQ),
1273
1378
  notes_limit: z.number().int().min(0).max(50).optional().default(DEFAULT_RECENT_NOTES),
1274
1379
  conventions_limit: z.number().int().min(0).max(200).optional().default(DEFAULT_CONVENTIONS_LIMIT),
1380
+ decisions_limit: z.number().int().min(0).max(MAX_DECISIONS_LIMIT).optional().default(DEFAULT_DECISIONS_LIMIT),
1275
1381
  });
1276
1382
  const GetPendingChangesArgsSchema = ProjectRootArgSchema.merge(z.object({
1277
1383
  offset: z.number().int().min(0).optional().default(0),
@@ -1329,7 +1435,7 @@ const InstallRtkArgsSchema = ProjectRootArgSchema.merge(z.object({
1329
1435
  dry_run: z.boolean().optional().default(true),
1330
1436
  method: z.enum(["auto", "cargo", "brew", "shell_script"]).optional().default("auto"),
1331
1437
  init: z
1332
- .enum(["none", "global_no_patch", "global_auto_patch", "global_hook_only", "local"])
1438
+ .enum(["none", "global_no_patch", "global_auto_patch", "global_hook_only", "local", "codex_global", "codex_local"])
1333
1439
  .optional()
1334
1440
  .default("none"),
1335
1441
  uninstall_wrong_cargo_rtk: z.boolean().optional().default(false),
@@ -1346,6 +1452,28 @@ function escapeLike(pattern) {
1346
1452
  function sha256Hex(input) {
1347
1453
  return crypto.createHash("sha256").update(input).digest("hex");
1348
1454
  }
1455
+ function getFileStateHash(dbOrAbsPath) {
1456
+ try {
1457
+ const abs = path.isAbsolute(dbOrAbsPath) ? dbOrAbsPath : path.join(projectRoot, dbOrAbsPath);
1458
+ const st = fs.statSync(abs);
1459
+ if (!st.isFile())
1460
+ return sha256Hex(`non-file:${st.mtimeMs}:${st.size}`);
1461
+ if (st.size <= 5_000_000) {
1462
+ return crypto.createHash("sha256").update(fs.readFileSync(abs)).digest("hex");
1463
+ }
1464
+ return sha256Hex(`large:${st.size}:${Math.floor(st.mtimeMs)}`);
1465
+ }
1466
+ catch {
1467
+ return sha256Hex("missing");
1468
+ }
1469
+ }
1470
+ function getLatestSyncedFileHash(dbFilePath) {
1471
+ const row = getLatestChangeIntentForFileStmt?.get(dbFilePath);
1472
+ if (!row)
1473
+ return null;
1474
+ const meta = parseMetadataJson(row.metadata_json);
1475
+ return typeof meta.file_state_hash === "string" ? meta.file_state_hash : null;
1476
+ }
1349
1477
  function safeJson(value) {
1350
1478
  if (value === undefined)
1351
1479
  return null;
@@ -1409,7 +1537,9 @@ function compactChangeLabel(change) {
1409
1537
  return `change#${change.id} ${change.file_path}: ${oneLine(change.intent_preview, 120)}`;
1410
1538
  }
1411
1539
  function compactPendingLabel(p) {
1412
- return `${p.last_event} ${p.file_path}`;
1540
+ const source = "source" in p && p.source === "git" ? " git" : "";
1541
+ const status = "git_status" in p && p.git_status ? ` ${p.git_status}` : "";
1542
+ return `${p.last_event}${source}${status} ${p.file_path}`;
1413
1543
  }
1414
1544
  function compactSemanticSearchText(data) {
1415
1545
  const lines = [
@@ -1480,6 +1610,11 @@ function compactBootstrapText(data) {
1480
1610
  lines.push(`ok ctx ${data.root_source} watcher=${data.watcher_enabled ? (data.watcher_ready ? "ready" : "starting") : "off"} root=${data.project_root}`);
1481
1611
  if (data.project_summary)
1482
1612
  lines.push(`summary ${compactMemoryLabel(data.project_summary, 140)}`);
1613
+ if (data.decisions.length) {
1614
+ lines.push("current decisions:");
1615
+ for (const d of data.decisions.slice(0, 5))
1616
+ lines.push(`- ${compactMemoryLabel(d, 160)}`);
1617
+ }
1483
1618
  if (data.pending_total) {
1484
1619
  lines.push(`pending ${data.pending_changes.length}/${data.pending_total}${data.pending_truncated ? " truncated" : ""}: ${data.pending_changes
1485
1620
  .slice(0, 8)
@@ -1523,42 +1658,105 @@ function compactBootstrapText(data) {
1523
1658
  function compactBrainDumpText(data) {
1524
1659
  return compactBootstrapText(data);
1525
1660
  }
1526
- function detectRtk() {
1527
- const command = process.platform === "win32" ? "rtk.exe" : "rtk";
1528
- const result = spawnSync(command, ["--version"], {
1661
+ function shellQuoteArg(arg) {
1662
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg))
1663
+ return arg;
1664
+ if (process.platform === "win32")
1665
+ return `"${arg.replace(/"/g, '\\"')}"`;
1666
+ return `'${arg.replace(/'/g, "'\\''")}'`;
1667
+ }
1668
+ function getPackageRtkShimPath() {
1669
+ try {
1670
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
1671
+ const candidate = path.join(currentDir, "rtk-shim.js");
1672
+ if (fs.existsSync(candidate))
1673
+ return candidate;
1674
+ }
1675
+ catch {
1676
+ // import.meta.url may be unavailable only in unexpected runtimes.
1677
+ }
1678
+ return null;
1679
+ }
1680
+ function runRtkProbe(spec) {
1681
+ const argsPrefix = spec.execArgsPrefix ?? [];
1682
+ const result = spawnSync(spec.execCommand, [...argsPrefix, "--version"], {
1529
1683
  encoding: "utf8",
1530
- timeout: 2000,
1684
+ timeout: 120_000,
1531
1685
  windowsHide: true,
1686
+ shell: spec.execShell ?? false,
1532
1687
  });
1533
1688
  if (result.status === 0) {
1534
- const gain = spawnSync(command, ["gain"], {
1535
- encoding: "utf8",
1536
- timeout: 5000,
1537
- windowsHide: true,
1538
- });
1539
- const whereCommand = process.platform === "win32" ? "where.exe" : "which";
1540
- const whereResult = spawnSync(whereCommand, [process.platform === "win32" ? "rtk.exe" : "rtk"], {
1689
+ const gain = spawnSync(spec.execCommand, [...argsPrefix, "gain"], {
1541
1690
  encoding: "utf8",
1542
- timeout: 2000,
1691
+ timeout: 120_000,
1543
1692
  windowsHide: true,
1693
+ shell: spec.execShell ?? false,
1544
1694
  });
1695
+ let resolvedPath = spec.path;
1696
+ if (spec.source === "path") {
1697
+ const whereCommand = process.platform === "win32" ? "where.exe" : "which";
1698
+ const whereResult = spawnSync(whereCommand, ["rtk"], {
1699
+ encoding: "utf8",
1700
+ timeout: 2000,
1701
+ windowsHide: true,
1702
+ });
1703
+ resolvedPath = whereResult.status === 0 ? oneLine(whereResult.stdout, 240) : resolvedPath;
1704
+ }
1545
1705
  const gainText = `${gain.stdout}${gain.stderr}`.trim();
1546
1706
  return {
1547
1707
  available: gain.status === 0,
1548
- command,
1708
+ command: spec.displayCommand,
1549
1709
  version: `${result.stdout}${result.stderr}`.trim(),
1550
1710
  gain_ok: gain.status === 0,
1551
1711
  gain_preview: oneLine(gainText, 240),
1552
- path: whereResult.status === 0 ? oneLine(whereResult.stdout, 240) : undefined,
1712
+ path: resolvedPath,
1713
+ source: spec.source,
1714
+ exec_command: spec.execCommand,
1715
+ exec_args_prefix: argsPrefix,
1716
+ exec_shell: spec.execShell ?? false,
1553
1717
  note: gain.status === 0
1554
- ? "Prefer prefixing shell commands with rtk for compact outputs, e.g. rtk git status / rtk npm run build / rtk rg pattern ."
1555
- : "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.",
1718
+ ? spec.source === "package_shim"
1719
+ ? `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
+ : "Prefer prefixing shell commands with rtk for compact outputs, e.g. rtk git status / rtk npm run build / rtk rg pattern ."
1721
+ : 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.",
1556
1724
  };
1557
1725
  }
1726
+ return null;
1727
+ }
1728
+ function detectRtk() {
1729
+ const pathProbe = runRtkProbe({
1730
+ source: "path",
1731
+ displayCommand: "rtk",
1732
+ execCommand: "rtk",
1733
+ execShell: process.platform === "win32",
1734
+ });
1735
+ if (pathProbe?.available)
1736
+ return pathProbe;
1737
+ const shimPath = getPackageRtkShimPath();
1738
+ if (shimPath) {
1739
+ const displayCommand = `node ${shellQuoteArg(shimPath)}`;
1740
+ const shimProbe = runRtkProbe({
1741
+ source: "package_shim",
1742
+ displayCommand,
1743
+ execCommand: process.execPath,
1744
+ execArgsPrefix: [shimPath],
1745
+ path: shimPath,
1746
+ });
1747
+ if (shimProbe)
1748
+ return shimProbe;
1749
+ }
1750
+ if (pathProbe)
1751
+ return pathProbe;
1558
1752
  return {
1559
1753
  available: false,
1560
- command,
1561
- note: "rtk was not found on PATH. VectorMind compact MCP output still works; install rtk to compact shell command output too.",
1754
+ command: shimPath ? `node ${shellQuoteArg(shimPath)}` : "rtk",
1755
+ path: shimPath ?? undefined,
1756
+ source: shimPath ? "package_shim" : undefined,
1757
+ 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."
1759
+ : "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.",
1562
1760
  };
1563
1761
  }
1564
1762
  function commandExists(command) {
@@ -1581,6 +1779,40 @@ function runInstallStep(command, args, timeoutMs) {
1581
1779
  output: oneLine(output, 1200),
1582
1780
  };
1583
1781
  }
1782
+ function runDetectedRtkStep(detected, args, timeoutMs) {
1783
+ const execCommand = detected.exec_command ?? "rtk";
1784
+ const argsPrefix = detected.exec_args_prefix ?? [];
1785
+ const result = spawnSync(execCommand, [...argsPrefix, ...args], {
1786
+ encoding: "utf8",
1787
+ timeout: timeoutMs,
1788
+ windowsHide: true,
1789
+ shell: detected.exec_shell ?? (execCommand === "rtk" && process.platform === "win32"),
1790
+ });
1791
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
1792
+ return {
1793
+ command: [detected.command, ...args].join(" "),
1794
+ status: result.status,
1795
+ ok: result.status === 0,
1796
+ output: oneLine(output, 1200),
1797
+ };
1798
+ }
1799
+ function appendRtkInitStep(steps, detected, init, timeoutMs) {
1800
+ if (init === "none")
1801
+ return;
1802
+ if (init === "global_no_patch")
1803
+ steps.push(runDetectedRtkStep(detected, ["init", "-g", "--no-patch"], timeoutMs));
1804
+ if (init === "global_auto_patch")
1805
+ steps.push(runDetectedRtkStep(detected, ["init", "-g", "--auto-patch"], timeoutMs));
1806
+ if (init === "global_hook_only") {
1807
+ steps.push(runDetectedRtkStep(detected, ["init", "-g", "--hook-only", "--no-patch"], timeoutMs));
1808
+ }
1809
+ if (init === "local")
1810
+ steps.push(runDetectedRtkStep(detected, ["init"], timeoutMs));
1811
+ if (init === "codex_global")
1812
+ steps.push(runDetectedRtkStep(detected, ["init", "-g", "--codex"], timeoutMs));
1813
+ if (init === "codex_local")
1814
+ steps.push(runDetectedRtkStep(detected, ["init", "--codex"], timeoutMs));
1815
+ }
1584
1816
  function chooseRtkInstallMethod(method) {
1585
1817
  if (method !== "auto")
1586
1818
  return method;
@@ -1623,6 +1855,10 @@ function buildRtkInstallPlan(args) {
1623
1855
  commands.push("rtk init -g --hook-only --no-patch");
1624
1856
  if (args.init === "local")
1625
1857
  commands.push("rtk init");
1858
+ if (args.init === "codex_global")
1859
+ commands.push("rtk init -g --codex");
1860
+ if (args.init === "codex_local")
1861
+ commands.push("rtk init --codex");
1626
1862
  if (args.init !== "none") {
1627
1863
  notes.push("rtk init may modify Claude/RTK configuration. Use init=none for binary-only installation.");
1628
1864
  }
@@ -1635,6 +1871,9 @@ function installRtk(args) {
1635
1871
  const notes = [...plan.notes];
1636
1872
  if (detectedBefore.available) {
1637
1873
  notes.push("rtk is already installed and verified with `rtk gain`; installation skipped.");
1874
+ if (!args.dry_run && args.init !== "none") {
1875
+ appendRtkInitStep(steps, detectedBefore, args.init, args.timeout_ms);
1876
+ }
1638
1877
  return {
1639
1878
  ok: true,
1640
1879
  dry_run: args.dry_run,
@@ -1682,15 +1921,7 @@ function installRtk(args) {
1682
1921
  }
1683
1922
  const detectedAfterInstall = detectRtk();
1684
1923
  if (detectedAfterInstall.available && args.init !== "none") {
1685
- if (args.init === "global_no_patch")
1686
- steps.push(runInstallStep("rtk", ["init", "-g", "--no-patch"], args.timeout_ms));
1687
- if (args.init === "global_auto_patch")
1688
- steps.push(runInstallStep("rtk", ["init", "-g", "--auto-patch"], args.timeout_ms));
1689
- if (args.init === "global_hook_only") {
1690
- steps.push(runInstallStep("rtk", ["init", "-g", "--hook-only", "--no-patch"], args.timeout_ms));
1691
- }
1692
- if (args.init === "local")
1693
- steps.push(runInstallStep("rtk", ["init"], args.timeout_ms));
1924
+ appendRtkInitStep(steps, detectedAfterInstall, args.init, args.timeout_ms);
1694
1925
  }
1695
1926
  const detectedAfter = detectRtk();
1696
1927
  return {
@@ -1898,6 +2129,88 @@ function dotProduct(a, b) {
1898
2129
  s += a[i] * b[i];
1899
2130
  return s;
1900
2131
  }
2132
+ function parseMetadataJson(metadata) {
2133
+ if (!metadata)
2134
+ return {};
2135
+ try {
2136
+ const parsed = JSON.parse(metadata);
2137
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
2138
+ ? parsed
2139
+ : {};
2140
+ }
2141
+ catch {
2142
+ return {};
2143
+ }
2144
+ }
2145
+ function metadataStatus(row) {
2146
+ const meta = parseMetadataJson(row.metadata_json);
2147
+ return typeof meta.status === "string" ? meta.status : "";
2148
+ }
2149
+ function isSupersededMemory(row) {
2150
+ const meta = parseMetadataJson(row.metadata_json);
2151
+ return meta.superseded === true || meta.status === "superseded";
2152
+ }
2153
+ function semanticRecencyWeight(updatedAt) {
2154
+ if (!updatedAt)
2155
+ return 0;
2156
+ const t = Date.parse(updatedAt.endsWith("Z") ? updatedAt : `${updatedAt}Z`);
2157
+ if (!Number.isFinite(t))
2158
+ return 0;
2159
+ const ageDays = Math.max(0, (Date.now() - t) / 86_400_000);
2160
+ if (ageDays <= 1)
2161
+ return 0.8;
2162
+ if (ageDays <= 7)
2163
+ return 0.45;
2164
+ if (ageDays <= 30)
2165
+ return 0.2;
2166
+ return 0;
2167
+ }
2168
+ function semanticKindWeight(kind) {
2169
+ switch (kind) {
2170
+ case "decision":
2171
+ return 3.5;
2172
+ case "convention":
2173
+ return 2.6;
2174
+ case "project_summary":
2175
+ return 2.2;
2176
+ case "note":
2177
+ return 1.1;
2178
+ case "requirement":
2179
+ return 0.4;
2180
+ case "change_intent":
2181
+ return 0.2;
2182
+ default:
2183
+ return 0;
2184
+ }
2185
+ }
2186
+ function adjustSemanticScore(row, rawScore) {
2187
+ if (isSupersededMemory(row))
2188
+ return rawScore - 1000;
2189
+ let score = rawScore + semanticKindWeight(row.kind) + semanticRecencyWeight(row.updated_at);
2190
+ const status = metadataStatus(row);
2191
+ if (status === "active" || status === "current")
2192
+ score += 1.2;
2193
+ if (row.kind === "change_intent" && row.file_path && shouldIgnoreDbFilePath(row.file_path)) {
2194
+ // Human-synced intent for generated/build/runtime files is often the only durable
2195
+ // "why" for that change. Do not let built-in path ignores hide the decision trail.
2196
+ score += 0.4;
2197
+ }
2198
+ return score;
2199
+ }
2200
+ function filterAndRankSemanticRows(rows, scoreOf, opts) {
2201
+ return rows
2202
+ .map((r) => ({ row: r, score: adjustSemanticScore(r, scoreOf(r)) }))
2203
+ .filter(({ row }) => {
2204
+ if (isSupersededMemory(row))
2205
+ return false;
2206
+ if (shouldIgnoreDbFilePath(row.file_path) && row.kind !== "change_intent")
2207
+ return false;
2208
+ return true;
2209
+ })
2210
+ .sort((a, b) => b.score - a.score)
2211
+ .slice(0, opts.topK)
2212
+ .map(({ row, score }) => toSemanticMatch(row, score, opts.includeContent, opts.previewChars, opts.contentMaxChars));
2213
+ }
1901
2214
  function makePreviewText(content, max) {
1902
2215
  if (max <= 0)
1903
2216
  return "";
@@ -1972,6 +2285,15 @@ function getConventionPreviews(conventionsLimit, previewChars, contentMaxChars)
1972
2285
  const stored = listConventionsStmt.all(remaining).map((c) => toMemoryItemPreview(c, false, previewChars, contentMaxChars));
1973
2286
  return [...builtin, ...stored];
1974
2287
  }
2288
+ function getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars) {
2289
+ if (decisionsLimit <= 0)
2290
+ return [];
2291
+ const rows = listCurrentDecisionsStmt.all(Math.min(MAX_DECISIONS_LIMIT * 4, Math.max(decisionsLimit, decisionsLimit * 4)));
2292
+ return rows
2293
+ .filter((d) => !isSupersededMemory(d))
2294
+ .slice(0, decisionsLimit)
2295
+ .map((d) => toMemoryItemPreview(d, false, previewChars, contentMaxChars));
2296
+ }
1975
2297
  function toRequirementPreview(req, includeContent, previewChars, contentMaxChars) {
1976
2298
  const context = req.context_data ?? null;
1977
2299
  const contextPreview = context ? makePreviewText(context, previewChars) : null;
@@ -2016,6 +2338,46 @@ function completeAllActiveRequirementMemoryItems() {
2016
2338
  console.error("[vectormind] failed to complete all active requirement memory items:", err);
2017
2339
  }
2018
2340
  }
2341
+ function patchMemoryItemMetadata(id, patch) {
2342
+ const row = getMemoryItemByIdStmt.get(id);
2343
+ if (!row)
2344
+ return;
2345
+ const meta = { ...parseMetadataJson(row.metadata_json), ...patch };
2346
+ db.prepare(`UPDATE memory_items SET metadata_json = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`).run(safeJson(meta), id);
2347
+ }
2348
+ function supersedeMemoryItemIds(ids, replacement) {
2349
+ const updated = [];
2350
+ for (const id of Array.from(new Set(ids)).filter((n) => Number.isFinite(n) && n > 0)) {
2351
+ const row = getMemoryItemByIdStmt.get(id);
2352
+ if (!row)
2353
+ continue;
2354
+ patchMemoryItemMetadata(id, {
2355
+ ...parseMetadataJson(row.metadata_json),
2356
+ status: "superseded",
2357
+ superseded: true,
2358
+ superseded_at: new Date().toISOString(),
2359
+ superseded_reason: replacement.reason,
2360
+ superseded_by_req_id: replacement.req_id ?? null,
2361
+ superseded_by_memory_id: replacement.memory_id ?? null,
2362
+ superseded_by_decision_id: replacement.decision_id ?? null,
2363
+ });
2364
+ updated.push(id);
2365
+ }
2366
+ return updated;
2367
+ }
2368
+ function supersedeRequirementIds(reqIds, replacement) {
2369
+ const updatedReqs = [];
2370
+ for (const reqId of Array.from(new Set(reqIds)).filter((n) => Number.isFinite(n) && n > 0)) {
2371
+ const info = db.prepare(`UPDATE requirements SET status = 'superseded' WHERE id = ?`).run(reqId);
2372
+ if (info.changes > 0)
2373
+ updatedReqs.push(reqId);
2374
+ const rows = db
2375
+ .prepare(`SELECT id FROM memory_items WHERE req_id = ? OR (kind = 'requirement' AND req_id = ?)`)
2376
+ .all(reqId, reqId);
2377
+ supersedeMemoryItemIds(rows.map((r) => r.id), replacement);
2378
+ }
2379
+ return updatedReqs;
2380
+ }
2019
2381
  async function semanticSearchInternal(opts) {
2020
2382
  if (!embeddingsEnabled) {
2021
2383
  throw new Error("Embeddings are disabled");
@@ -2067,7 +2429,31 @@ async function semanticSearchInternal(opts) {
2067
2429
  return toSemanticMatch(item, t.score, opts.includeContent, opts.previewChars, opts.contentMaxChars);
2068
2430
  })
2069
2431
  .filter(Boolean);
2070
- const filtered = matches.filter((m) => !shouldIgnoreDbFilePath(m.item.file_path)).slice(0, opts.topK);
2432
+ const filtered = matches
2433
+ .filter((m) => {
2434
+ if (isSupersededMemory({ metadata_json: m.item.metadata_json }))
2435
+ return false;
2436
+ if (shouldIgnoreDbFilePath(m.item.file_path) && m.item.kind !== "change_intent")
2437
+ return false;
2438
+ return true;
2439
+ })
2440
+ .map((m) => ({
2441
+ ...m,
2442
+ score: adjustSemanticScore({
2443
+ id: m.item.id,
2444
+ kind: m.item.kind,
2445
+ title: m.item.title,
2446
+ content: m.item.content ?? m.item.preview,
2447
+ file_path: m.item.file_path,
2448
+ start_line: m.item.start_line,
2449
+ end_line: m.item.end_line,
2450
+ req_id: m.item.req_id,
2451
+ metadata_json: m.item.metadata_json,
2452
+ updated_at: m.item.updated_at,
2453
+ }, m.score),
2454
+ }))
2455
+ .sort((a, b) => b.score - a.score)
2456
+ .slice(0, opts.topK);
2071
2457
  return { query: q, top_k: opts.topK, mode: "embeddings", matches: filtered };
2072
2458
  }
2073
2459
  function buildFtsMatchQuery(raw) {
@@ -2136,10 +2522,7 @@ function ftsSearchInternal(opts) {
2136
2522
  `);
2137
2523
  return stmt.all(matchQuery, rawLimit);
2138
2524
  })();
2139
- const matches = rows
2140
- .map((r) => toSemanticMatch(r, -Number(r.rank), opts.includeContent, opts.previewChars, opts.contentMaxChars))
2141
- .filter((m) => !shouldIgnoreDbFilePath(m.item.file_path))
2142
- .slice(0, opts.topK);
2525
+ const matches = filterAndRankSemanticRows(rows, (r) => -Number(r.rank), opts);
2143
2526
  return { query: q, top_k: opts.topK, mode: "fts", matches };
2144
2527
  }
2145
2528
  function likeSearchInternal(opts) {
@@ -2205,10 +2588,7 @@ function likeSearchInternal(opts) {
2205
2588
  `);
2206
2589
  return stmt.all(like, like, like, like, like, rawLimit);
2207
2590
  })();
2208
- const matches = rows
2209
- .map((r) => toSemanticMatch(r, Number(r.score), opts.includeContent, opts.previewChars, opts.contentMaxChars))
2210
- .filter((m) => !shouldIgnoreDbFilePath(m.item.file_path))
2211
- .slice(0, opts.topK);
2591
+ const matches = filterAndRankSemanticRows(rows, (r) => Number(r.score), opts);
2212
2592
  return { query: q, top_k: opts.topK, mode: "like", matches };
2213
2593
  }
2214
2594
  async function semanticSearchHybridInternal(opts) {
@@ -2933,65 +3313,59 @@ function listProjectFilesInternal(opts) {
2933
3313
  function buildServerInstructions() {
2934
3314
  return [
2935
3315
  "VectorMind MCP is available in this session. Use it to avoid guessing project context.",
2936
- "This package ships built-in baseline policy. If a client supports MCP instructions, these rules auto-apply as soon as the MCP is installed and connected; no user-side config file is required.",
2937
- "The write-operation rules below are strict workflow constraints. Do not claim that the environment has real git branch locks, checkout APIs, or file-lock tools unless such tools are actually available in the current client/runtime. If such tools are absent, you must still enforce the same exclusivity semantics through explicit coordination and serialized same-file edits.",
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.",
2938
3317
  "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).",
2939
3318
  "If root_source is fallback, file watching/indexing is disabled (pass project_root to enable per-project tracking).",
2940
3319
  "",
2941
- "Built-in write-operation policy:",
3320
+ "Built-in write-operation quality policy:",
2942
3321
  BUILTIN_WRITE_POLICY_INSTRUCTIONS,
2943
3322
  "",
2944
- "Built-in task-list / Plan-Lite policy:",
3323
+ "Built-in task-list / Plan-Lite quality policy:",
2945
3324
  BUILTIN_PLAN_LITE_INSTRUCTIONS,
2946
3325
  "",
2947
- "Built-in destructive-operation guard policy:",
3326
+ "Built-in destructive-operation quality guard:",
2948
3327
  BUILTIN_DESTRUCTIVE_OPERATION_GUARD_INSTRUCTIONS,
2949
3328
  "",
2950
- "Built-in architecture and code-organization policy:",
3329
+ "Built-in architecture and code-organization quality policy:",
2951
3330
  BUILTIN_ARCHITECTURE_AND_CODE_ORGANIZATION_INSTRUCTIONS,
2952
3331
  "",
2953
- "Built-in frontend output-purity policy:",
3332
+ "Built-in frontend output-purity quality policy:",
2954
3333
  BUILTIN_FRONTEND_OUTPUT_PURITY_INSTRUCTIONS,
2955
3334
  "",
2956
- "Built-in git commit summary policy:",
3335
+ "Built-in git commit summary quality policy:",
2957
3336
  BUILTIN_GIT_COMMIT_SUMMARY_INSTRUCTIONS,
2958
3337
  "",
2959
- "Built-in low-overhead execution and heavy-thread policy:",
3338
+ "Built-in low-overhead execution and heavy-thread quality policy:",
2960
3339
  BUILTIN_LOW_OVERHEAD_WORKFLOW_INSTRUCTIONS,
2961
3340
  "",
2962
- "Built-in payload / oversized-thread guard policy:",
3341
+ "Built-in payload / oversized-thread quality guard:",
2963
3342
  BUILTIN_PAYLOAD_GUARD_INSTRUCTIONS,
2964
3343
  "",
2965
- "Built-in thread handoff / switch-gate policy:",
3344
+ "Built-in thread handoff / switch-gate quality policy:",
2966
3345
  BUILTIN_THREAD_HANDOFF_SWITCH_INSTRUCTIONS,
2967
3346
  "",
2968
- "Required workflow:",
3347
+ "VectorMind workflow:",
2969
3348
  "- Tool outputs are compact by default. Pass format=json only when you need full structured data.",
2970
3349
  "- 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).",
2971
3350
  " - Output is compact by default. Use include_content=true only when you truly need full text (it increases tokens).",
2972
- " - Tune output size with: requirements_limit/changes_limit/notes_limit, preview_chars, pending_limit/pending_offset.",
3351
+ " - Tune output size with: requirements_limit/changes_limit/notes_limit/decisions_limit, preview_chars, pending_limit/pending_offset.",
2973
3352
  " - Prefer read_memory_item(id, offset, limit) to fetch full text on demand instead of returning large content in other tool outputs.",
2974
3353
  "- 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.",
2975
- "- If rtk is installed and verified (detect_rtk with gain_ok=true), prefix shell commands with rtk where possible (rtk git status, rtk npm run build, rtk rg ...) so command output is compact before it reaches the model.",
3354
+ "- 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.",
2976
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.",
2977
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.",
2978
3357
  "- 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.",
2979
3358
  "- 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.",
2980
3359
  "- 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.",
2981
3360
  "- 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.",
2982
- "- If the current thread is heavy, recently compacted, has become slow, or has already hit a 413 / Payload Too Large style error, switch to payload guard mode: avoid unbounded shell dumps, prefer bounded MCP tools, and summarize outputs instead of pasting large raw blocks.",
2983
- "- In payload guard mode, do not use full-repo recursive listings, whole-file dumps, or broad raw match echo unless the user explicitly requests that raw output and accepts the size risk.",
2984
- "- Thread-switch judgment must not rely on a fixed token threshold; use observable signals plus the weight of the upcoming work. If the current thread is heavy, repeatedly compacting, slow, or has already hit a 413 / Payload Too Large style error and the next work still needs broad analysis, cross-module investigation, release validation, or other substantial continuation, pause once and ask whether to switch to a fresh thread before continuing.",
2985
- "- If the user declines that switch, continue in the current thread and do not raise the thread-switch reminder again in the same session.",
2986
- "- If the user accepts, or explicitly asks you to pack the current conversation for a new thread, do not attempt to create a new thread on the user's behalf and do not claim that you already created one. Use add_note(...) to persist a concise handoff note, include other relevant existing note ids only when they are truly needed, then reply briefly with the new handoff note id and tell the user that in the new thread they can say '读取 note <id> [和 note <id>] 继续'.",
2987
3361
  "- BEFORE editing code: call start_requirement(title, background) to set the active requirement.",
2988
3362
  "- 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.)",
2989
3363
  "- After major milestones/decisions: call upsert_project_summary(summary) and/or add_note(...) to persist durable context locally.",
3364
+ "- 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.",
2990
3365
  "- 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.",
2991
3366
  "- When you need full text for a specific note/summary/match: call read_memory_item(id, offset, limit) and page through it.",
2992
3367
  "- When asked to locate code (class/function/type): call query_codebase(query) instead of guessing.",
2993
3368
  "- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing.",
2994
- "- If the current thread is already heavy or the user reports it has become slow, switch to a lighter workflow: avoid redundant retrieval, keep outputs compact, and if the user refuses thread switching, continue in light mode without repeating the switch reminder in that same session.",
2995
3369
  "- Use get_token_savings({ format: 'compact' }) when you need to verify how many tokens VectorMind compact outputs saved.",
2996
3370
  "",
2997
3371
  "If tool output conflicts with assumptions, trust the tool output.",
@@ -3211,6 +3585,9 @@ function initDatabase() {
3211
3585
  CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_items_convention_key
3212
3586
  ON memory_items(kind, title) WHERE kind = 'convention';
3213
3587
 
3588
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_items_decision_key
3589
+ ON memory_items(kind, title) WHERE kind = 'decision';
3590
+
3214
3591
  CREATE INDEX IF NOT EXISTS idx_memory_items_kind_updated_at
3215
3592
  ON memory_items(kind, updated_at DESC);
3216
3593
 
@@ -3307,6 +3684,23 @@ function initDatabase() {
3307
3684
  WHERE kind = 'convention'
3308
3685
  ORDER BY updated_at DESC, id DESC
3309
3686
  LIMIT ?`);
3687
+ upsertDecisionStmt = db.prepare(`INSERT INTO memory_items (kind, title, content, metadata_json, content_hash)
3688
+ VALUES ('decision', ?, ?, ?, ?)
3689
+ ON CONFLICT DO UPDATE SET
3690
+ content = excluded.content,
3691
+ metadata_json = excluded.metadata_json,
3692
+ content_hash = excluded.content_hash,
3693
+ updated_at = CURRENT_TIMESTAMP`);
3694
+ getDecisionByKeyStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
3695
+ FROM memory_items
3696
+ WHERE kind = 'decision' AND title = ?
3697
+ ORDER BY updated_at DESC, id DESC
3698
+ LIMIT 1`);
3699
+ listCurrentDecisionsStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
3700
+ FROM memory_items
3701
+ WHERE kind = 'decision'
3702
+ ORDER BY updated_at DESC, id DESC
3703
+ LIMIT ?`);
3310
3704
  getRequirementMemoryItemIdStmt = db.prepare(`SELECT id
3311
3705
  FROM memory_items
3312
3706
  WHERE kind = 'requirement' AND req_id = ?
@@ -3329,6 +3723,11 @@ function initDatabase() {
3329
3723
  WHERE kind = 'note'
3330
3724
  ORDER BY updated_at DESC, id DESC
3331
3725
  LIMIT ?`);
3726
+ 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
+ FROM memory_items
3728
+ WHERE kind = 'change_intent' AND file_path = ?
3729
+ ORDER BY updated_at DESC, id DESC
3730
+ LIMIT 1`);
3332
3731
  deleteFileChunkItemsStmt = db.prepare(`DELETE FROM memory_items
3333
3732
  WHERE file_path = ?
3334
3733
  AND (kind = 'code_chunk' OR kind = 'doc_chunk')`);
@@ -3598,7 +3997,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3598
3997
  },
3599
3998
  {
3600
3999
  name: "detect_rtk",
3601
- description: "Detect whether rtk is available on PATH. When available, prefer rtk-prefixed shell commands to reduce command-output tokens.",
4000
+ description: "Detect whether rtk is available on PATH or via VectorMind's bundled RTK shim. When available, prefer the returned command as a shell prefix to reduce command-output tokens.",
3602
4001
  inputSchema: toJsonSchemaCompat(DetectRtkArgsSchema),
3603
4002
  },
3604
4003
  {
@@ -3651,6 +4050,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3651
4050
  description: "Save a durable project note (decision, constraint, TODO, architecture detail). Use this to persist important context locally instead of relying on chat memory.",
3652
4051
  inputSchema: toJsonSchemaCompat(AddNoteArgsSchema),
3653
4052
  },
4053
+ {
4054
+ name: "upsert_decision",
4055
+ description: "Save/update the current authoritative project decision for a key. Use it when requirements change, reverse, or supersede older behavior so future sessions prefer the latest decision over old history.",
4056
+ inputSchema: toJsonSchemaCompat(UpsertDecisionArgsSchema),
4057
+ },
4058
+ {
4059
+ name: "supersede_memory",
4060
+ description: "Mark old requirements or memory items as superseded by a newer requirement/decision. Superseded items are hidden from default semantic recall to avoid reverting to stale behavior.",
4061
+ inputSchema: toJsonSchemaCompat(SupersedeMemoryArgsSchema),
4062
+ },
3654
4063
  {
3655
4064
  name: "upsert_convention",
3656
4065
  description: "Save/update a project convention (framework choice, build command, naming rules, etc). Conventions are durable and should be applied automatically in future sessions.",
@@ -3882,24 +4291,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3882
4291
  }
3883
4292
  else {
3884
4293
  const pendingAll = listPendingChangesStmt.all();
3885
- if (pendingAll.length) {
3886
- const pending = pendingAll.filter((p) => !shouldIgnoreDbFilePath(p.file_path));
3887
- if (pending.length) {
3888
- for (const p of pending) {
3889
- targets.push({
3890
- rawFile: p.file_path,
3891
- dbFilePath: p.file_path,
3892
- event: p.last_event,
3893
- source: "pending",
3894
- });
3895
- }
3896
- }
3897
- else {
4294
+ const merged = mergePendingWithGit(pendingAll, { offset: 0, limit: MAX_PENDING_LIMIT });
4295
+ if (merged.page.length) {
4296
+ for (const p of merged.page) {
3898
4297
  targets.push({
3899
- rawFile: "(unspecified)",
3900
- dbFilePath: "(unspecified)",
3901
- event: "manual",
3902
- source: "unspecified",
4298
+ rawFile: p.file_path,
4299
+ dbFilePath: p.file_path,
4300
+ event: p.last_event,
4301
+ source: p.source === "git" ? "pending" : "pending",
3903
4302
  });
3904
4303
  }
3905
4304
  deleteAllPendingChangesStmt.run();
@@ -3917,7 +4316,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3917
4316
  const isUnspecified = t.dbFilePath === "(unspecified)";
3918
4317
  const changeInfo = insertChangeLogStmt.run(active.id, t.dbFilePath, args.intent);
3919
4318
  const change_log_id = Number(changeInfo.lastInsertRowid);
3920
- const memoryInfo = insertMemoryItemStmt.run("change_intent", active.title, args.intent, isUnspecified ? null : t.dbFilePath, null, null, active.id, safeJson({ change_log_id, event: t.event, source: t.source }), sha256Hex(args.intent));
4319
+ const memoryInfo = insertMemoryItemStmt.run("change_intent", active.title, args.intent, isUnspecified ? null : t.dbFilePath, null, null, active.id, safeJson({
4320
+ change_log_id,
4321
+ event: t.event,
4322
+ source: t.source,
4323
+ file_state_hash: isUnspecified ? null : getFileStateHash(t.rawFile),
4324
+ }), sha256Hex(args.intent));
3921
4325
  const memory_item_id = Number(memoryInfo.lastInsertRowid);
3922
4326
  enqueueEmbedding(memory_item_id);
3923
4327
  synced_files.push({ file_path: t.dbFilePath, event: t.event, source: t.source });
@@ -3968,6 +4372,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3968
4372
  const changesLimit = args.changes_limit;
3969
4373
  const notesLimit = args.notes_limit;
3970
4374
  const conventionsLimit = args.conventions_limit;
4375
+ const decisionsLimit = args.decisions_limit;
3971
4376
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
3972
4377
  const items = recent.map((req) => {
3973
4378
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -3981,12 +4386,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3981
4386
  ? toMemoryItemPreview(projectSummaryRow, includeContent, previewChars, contentMaxChars)
3982
4387
  : null;
3983
4388
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
4389
+ const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
3984
4390
  const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
3985
- const pending_total = Number(countPendingChangesStmt.get()?.total ?? 0);
3986
4391
  const pending_offset = args.pending_offset;
3987
4392
  const pending_limit = args.pending_limit;
3988
- const pending_truncated = pending_total > pending_offset + pending_limit;
3989
- const pending_changes = listPendingChangesPageStmt.all(pending_limit, pending_offset).filter((p) => !shouldIgnoreDbFilePath(p.file_path));
4393
+ const pendingDbRows = listPendingChangesStmt.all();
4394
+ const mergedPending = mergePendingWithGit(pendingDbRows, { offset: pending_offset, limit: pending_limit });
4395
+ const pending_total = mergedPending.total;
4396
+ const pending_truncated = mergedPending.truncated;
4397
+ const pending_changes = mergedPending.page;
3990
4398
  const q = args.query?.trim() ?? "";
3991
4399
  const semantic = q
3992
4400
  ? await Promise.race([
@@ -4009,6 +4417,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4009
4417
  pending_total,
4010
4418
  pending_returned: pending_changes.length,
4011
4419
  requirements_returned: items.length,
4420
+ decisions_returned: decisions.length,
4012
4421
  conventions_returned: conventions.length,
4013
4422
  semantic_mode: semantic?.mode ?? null,
4014
4423
  semantic_matches: semantic?.matches?.length ?? 0,
@@ -4034,9 +4443,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4034
4443
  requirements_limit: requirementsLimit,
4035
4444
  changes_limit: changesLimit,
4036
4445
  notes_limit: notesLimit,
4446
+ decisions_limit: decisionsLimit,
4037
4447
  conventions_limit: conventionsLimit,
4038
4448
  },
4039
4449
  project_summary,
4450
+ decisions,
4040
4451
  conventions,
4041
4452
  recent_notes,
4042
4453
  pending_total,
@@ -4066,6 +4477,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4066
4477
  const changesLimit = args.changes_limit;
4067
4478
  const notesLimit = args.notes_limit;
4068
4479
  const conventionsLimit = args.conventions_limit;
4480
+ const decisionsLimit = args.decisions_limit;
4069
4481
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
4070
4482
  const items = recent.map((req) => {
4071
4483
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -4079,17 +4491,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4079
4491
  ? toMemoryItemPreview(projectSummaryRow, includeContent, previewChars, contentMaxChars)
4080
4492
  : null;
4081
4493
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
4494
+ const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
4082
4495
  const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
4083
- const pending_total = Number(countPendingChangesStmt.get()?.total ?? 0);
4084
4496
  const pending_offset = args.pending_offset;
4085
4497
  const pending_limit = args.pending_limit;
4086
- const pending_truncated = pending_total > pending_offset + pending_limit;
4087
- const pending_changes = listPendingChangesPageStmt.all(pending_limit, pending_offset).filter((p) => !shouldIgnoreDbFilePath(p.file_path));
4498
+ const pendingDbRows = listPendingChangesStmt.all();
4499
+ const mergedPending = mergePendingWithGit(pendingDbRows, { offset: pending_offset, limit: pending_limit });
4500
+ const pending_total = mergedPending.total;
4501
+ const pending_truncated = mergedPending.truncated;
4502
+ const pending_changes = mergedPending.page;
4088
4503
  logActivity("get_brain_dump", {
4089
4504
  pending_total,
4090
4505
  pending_returned: pending_changes.length,
4091
4506
  requirements_returned: items.length,
4092
4507
  notes_returned: recent_notes.length,
4508
+ decisions_returned: decisions.length,
4093
4509
  conventions_returned: conventions.length,
4094
4510
  });
4095
4511
  const outputValue = {
@@ -4113,9 +4529,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4113
4529
  requirements_limit: requirementsLimit,
4114
4530
  changes_limit: changesLimit,
4115
4531
  notes_limit: notesLimit,
4532
+ decisions_limit: decisionsLimit,
4116
4533
  conventions_limit: conventionsLimit,
4117
4534
  },
4118
4535
  project_summary,
4536
+ decisions,
4119
4537
  conventions,
4120
4538
  recent_notes,
4121
4539
  pending_total,
@@ -4138,11 +4556,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4138
4556
  if (toolName === "get_pending_changes") {
4139
4557
  const args = GetPendingChangesArgsSchema.parse(rawArgs);
4140
4558
  flushPendingChangeBuffer();
4141
- const total = Number(countPendingChangesStmt.get()?.total ?? 0);
4142
4559
  const offset = args.offset;
4143
4560
  const limit = args.limit;
4144
- const truncated = total > offset + limit;
4145
- const pending = listPendingChangesPageStmt.all(limit, offset).filter((p) => !shouldIgnoreDbFilePath(p.file_path));
4561
+ const pendingDbRows = listPendingChangesStmt.all();
4562
+ const mergedPending = mergePendingWithGit(pendingDbRows, { offset, limit });
4563
+ const total = mergedPending.total;
4564
+ const truncated = mergedPending.truncated;
4565
+ const pending = mergedPending.page;
4146
4566
  logActivity("get_pending_changes", {
4147
4567
  total,
4148
4568
  offset,
@@ -4315,8 +4735,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4315
4735
  DetectRtkArgsSchema.parse(rawArgs);
4316
4736
  const result = detectRtk();
4317
4737
  const text = result.available
4318
- ? `rtk available: ${result.version ?? result.command}\ngain_ok=${result.gain_ok ?? false}${result.path ? ` path=${result.path}` : ""}\n${result.note}`
4319
- : `rtk unavailable: ${result.command}\ngain_ok=${result.gain_ok ?? false}${result.version ? ` version=${result.version}` : ""}\n${result.note}`;
4738
+ ? `rtk available: ${result.version ?? result.command}\ncommand=${result.command} source=${result.source ?? "unknown"} gain_ok=${result.gain_ok ?? false}${result.path ? ` path=${result.path}` : ""}\n${result.note}`
4739
+ : `rtk unavailable: ${result.command}\nsource=${result.source ?? "none"} gain_ok=${result.gain_ok ?? false}${result.version ? ` version=${result.version}` : ""}${result.path ? ` path=${result.path}` : ""}\n${result.note}`;
4320
4740
  return { content: [{ type: "text", text }] };
4321
4741
  }
4322
4742
  if (toolName === "install_rtk") {
@@ -4812,6 +5232,95 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4812
5232
  ],
4813
5233
  };
4814
5234
  }
5235
+ if (toolName === "upsert_decision") {
5236
+ const args = UpsertDecisionArgsSchema.parse(rawArgs);
5237
+ const key = args.key.trim();
5238
+ const title = args.title.trim() || key;
5239
+ const content = args.content.trim();
5240
+ const meta = {
5241
+ status: "current",
5242
+ key,
5243
+ title,
5244
+ tags: args.tags ?? [],
5245
+ supersedes_req_ids: args.supersedes_req_ids ?? [],
5246
+ supersedes_memory_ids: args.supersedes_memory_ids ?? [],
5247
+ related_files: (args.related_files ?? []).map((f) => normalizeToDbPath(f)),
5248
+ };
5249
+ upsertDecisionStmt.run(key, `${title}\n\n${content}`, safeJson(meta), sha256Hex(`${title}\n\n${content}`));
5250
+ const row = getDecisionByKeyStmt.get(key);
5251
+ if (row)
5252
+ enqueueEmbedding(row.id);
5253
+ const superseded_requirements = supersedeRequirementIds(args.supersedes_req_ids ?? [], {
5254
+ decision_id: row?.id,
5255
+ reason: `Superseded by decision ${key}: ${title}`,
5256
+ });
5257
+ const superseded_memory_items = supersedeMemoryItemIds(args.supersedes_memory_ids ?? [], {
5258
+ decision_id: row?.id,
5259
+ reason: `Superseded by decision ${key}: ${title}`,
5260
+ });
5261
+ logActivity("upsert_decision", {
5262
+ key,
5263
+ decision_id: row?.id ?? null,
5264
+ superseded_requirements,
5265
+ superseded_memory_items,
5266
+ });
5267
+ return {
5268
+ content: [
5269
+ {
5270
+ type: "text",
5271
+ text: toolJson({
5272
+ ok: true,
5273
+ decision: row ? { id: row.id, key, updated_at: row.updated_at } : null,
5274
+ superseded_requirements,
5275
+ superseded_memory_items,
5276
+ }),
5277
+ },
5278
+ ],
5279
+ };
5280
+ }
5281
+ if (toolName === "supersede_memory") {
5282
+ const args = SupersedeMemoryArgsSchema.parse(rawArgs);
5283
+ const supersededReqIds = args.superseded_req_ids ?? [];
5284
+ const supersededMemoryIds = args.superseded_memory_ids ?? [];
5285
+ if (!supersededReqIds.length && !supersededMemoryIds.length) {
5286
+ return {
5287
+ isError: true,
5288
+ content: [
5289
+ {
5290
+ type: "text",
5291
+ text: toolJson({
5292
+ ok: false,
5293
+ error: "Provide superseded_req_ids and/or superseded_memory_ids.",
5294
+ }),
5295
+ },
5296
+ ],
5297
+ };
5298
+ }
5299
+ const superseded_requirements = supersedeRequirementIds(supersededReqIds, {
5300
+ req_id: args.replacement_req_id,
5301
+ memory_id: args.replacement_memory_id,
5302
+ reason: args.reason,
5303
+ });
5304
+ const superseded_memory_items = supersedeMemoryItemIds(supersededMemoryIds, {
5305
+ req_id: args.replacement_req_id,
5306
+ memory_id: args.replacement_memory_id,
5307
+ reason: args.reason,
5308
+ });
5309
+ logActivity("supersede_memory", {
5310
+ superseded_requirements,
5311
+ superseded_memory_items,
5312
+ replacement_req_id: args.replacement_req_id ?? null,
5313
+ replacement_memory_id: args.replacement_memory_id ?? null,
5314
+ });
5315
+ return {
5316
+ content: [
5317
+ {
5318
+ type: "text",
5319
+ text: toolJson({ ok: true, superseded_requirements, superseded_memory_items }),
5320
+ },
5321
+ ],
5322
+ };
5323
+ }
4815
5324
  if (toolName === "upsert_convention") {
4816
5325
  const args = UpsertConventionArgsSchema.parse(rawArgs);
4817
5326
  const key = args.key.trim();