@coreyuan/vector-mind 1.0.39 → 1.0.41

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.41";
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) {
@@ -2969,10 +3349,10 @@ function buildServerInstructions() {
2969
3349
  "- Tool outputs are compact by default. Pass format=json only when you need full structured data.",
2970
3350
  "- 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
3351
  " - 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.",
3352
+ " - Tune output size with: requirements_limit/changes_limit/notes_limit/decisions_limit, preview_chars, pending_limit/pending_offset.",
2973
3353
  " - Prefer read_memory_item(id, offset, limit) to fetch full text on demand instead of returning large content in other tool outputs.",
2974
3354
  "- 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.",
3355
+ "- 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
3356
  "- 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
3357
  "- 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
3358
  "- 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.",
@@ -2987,6 +3367,7 @@ function buildServerInstructions() {
2987
3367
  "- BEFORE editing code: call start_requirement(title, background) to set the active requirement.",
2988
3368
  "- 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
3369
  "- After major milestones/decisions: call upsert_project_summary(summary) and/or add_note(...) to persist durable context locally.",
3370
+ "- 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
3371
  "- 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
3372
  "- When you need full text for a specific note/summary/match: call read_memory_item(id, offset, limit) and page through it.",
2992
3373
  "- When asked to locate code (class/function/type): call query_codebase(query) instead of guessing.",
@@ -3211,6 +3592,9 @@ function initDatabase() {
3211
3592
  CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_items_convention_key
3212
3593
  ON memory_items(kind, title) WHERE kind = 'convention';
3213
3594
 
3595
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_items_decision_key
3596
+ ON memory_items(kind, title) WHERE kind = 'decision';
3597
+
3214
3598
  CREATE INDEX IF NOT EXISTS idx_memory_items_kind_updated_at
3215
3599
  ON memory_items(kind, updated_at DESC);
3216
3600
 
@@ -3307,6 +3691,23 @@ function initDatabase() {
3307
3691
  WHERE kind = 'convention'
3308
3692
  ORDER BY updated_at DESC, id DESC
3309
3693
  LIMIT ?`);
3694
+ upsertDecisionStmt = db.prepare(`INSERT INTO memory_items (kind, title, content, metadata_json, content_hash)
3695
+ VALUES ('decision', ?, ?, ?, ?)
3696
+ ON CONFLICT DO UPDATE SET
3697
+ content = excluded.content,
3698
+ metadata_json = excluded.metadata_json,
3699
+ content_hash = excluded.content_hash,
3700
+ updated_at = CURRENT_TIMESTAMP`);
3701
+ getDecisionByKeyStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
3702
+ FROM memory_items
3703
+ WHERE kind = 'decision' AND title = ?
3704
+ ORDER BY updated_at DESC, id DESC
3705
+ LIMIT 1`);
3706
+ listCurrentDecisionsStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
3707
+ FROM memory_items
3708
+ WHERE kind = 'decision'
3709
+ ORDER BY updated_at DESC, id DESC
3710
+ LIMIT ?`);
3310
3711
  getRequirementMemoryItemIdStmt = db.prepare(`SELECT id
3311
3712
  FROM memory_items
3312
3713
  WHERE kind = 'requirement' AND req_id = ?
@@ -3329,6 +3730,11 @@ function initDatabase() {
3329
3730
  WHERE kind = 'note'
3330
3731
  ORDER BY updated_at DESC, id DESC
3331
3732
  LIMIT ?`);
3733
+ getLatestChangeIntentForFileStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
3734
+ FROM memory_items
3735
+ WHERE kind = 'change_intent' AND file_path = ?
3736
+ ORDER BY updated_at DESC, id DESC
3737
+ LIMIT 1`);
3332
3738
  deleteFileChunkItemsStmt = db.prepare(`DELETE FROM memory_items
3333
3739
  WHERE file_path = ?
3334
3740
  AND (kind = 'code_chunk' OR kind = 'doc_chunk')`);
@@ -3598,7 +4004,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3598
4004
  },
3599
4005
  {
3600
4006
  name: "detect_rtk",
3601
- description: "Detect whether rtk is available on PATH. When available, prefer rtk-prefixed shell commands to reduce command-output tokens.",
4007
+ 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
4008
  inputSchema: toJsonSchemaCompat(DetectRtkArgsSchema),
3603
4009
  },
3604
4010
  {
@@ -3651,6 +4057,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3651
4057
  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
4058
  inputSchema: toJsonSchemaCompat(AddNoteArgsSchema),
3653
4059
  },
4060
+ {
4061
+ name: "upsert_decision",
4062
+ 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.",
4063
+ inputSchema: toJsonSchemaCompat(UpsertDecisionArgsSchema),
4064
+ },
4065
+ {
4066
+ name: "supersede_memory",
4067
+ 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.",
4068
+ inputSchema: toJsonSchemaCompat(SupersedeMemoryArgsSchema),
4069
+ },
3654
4070
  {
3655
4071
  name: "upsert_convention",
3656
4072
  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 +4298,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3882
4298
  }
3883
4299
  else {
3884
4300
  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 {
4301
+ const merged = mergePendingWithGit(pendingAll, { offset: 0, limit: MAX_PENDING_LIMIT });
4302
+ if (merged.page.length) {
4303
+ for (const p of merged.page) {
3898
4304
  targets.push({
3899
- rawFile: "(unspecified)",
3900
- dbFilePath: "(unspecified)",
3901
- event: "manual",
3902
- source: "unspecified",
4305
+ rawFile: p.file_path,
4306
+ dbFilePath: p.file_path,
4307
+ event: p.last_event,
4308
+ source: p.source === "git" ? "pending" : "pending",
3903
4309
  });
3904
4310
  }
3905
4311
  deleteAllPendingChangesStmt.run();
@@ -3917,7 +4323,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3917
4323
  const isUnspecified = t.dbFilePath === "(unspecified)";
3918
4324
  const changeInfo = insertChangeLogStmt.run(active.id, t.dbFilePath, args.intent);
3919
4325
  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));
4326
+ const memoryInfo = insertMemoryItemStmt.run("change_intent", active.title, args.intent, isUnspecified ? null : t.dbFilePath, null, null, active.id, safeJson({
4327
+ change_log_id,
4328
+ event: t.event,
4329
+ source: t.source,
4330
+ file_state_hash: isUnspecified ? null : getFileStateHash(t.rawFile),
4331
+ }), sha256Hex(args.intent));
3921
4332
  const memory_item_id = Number(memoryInfo.lastInsertRowid);
3922
4333
  enqueueEmbedding(memory_item_id);
3923
4334
  synced_files.push({ file_path: t.dbFilePath, event: t.event, source: t.source });
@@ -3968,6 +4379,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3968
4379
  const changesLimit = args.changes_limit;
3969
4380
  const notesLimit = args.notes_limit;
3970
4381
  const conventionsLimit = args.conventions_limit;
4382
+ const decisionsLimit = args.decisions_limit;
3971
4383
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
3972
4384
  const items = recent.map((req) => {
3973
4385
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -3981,12 +4393,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3981
4393
  ? toMemoryItemPreview(projectSummaryRow, includeContent, previewChars, contentMaxChars)
3982
4394
  : null;
3983
4395
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
4396
+ const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
3984
4397
  const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
3985
- const pending_total = Number(countPendingChangesStmt.get()?.total ?? 0);
3986
4398
  const pending_offset = args.pending_offset;
3987
4399
  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));
4400
+ const pendingDbRows = listPendingChangesStmt.all();
4401
+ const mergedPending = mergePendingWithGit(pendingDbRows, { offset: pending_offset, limit: pending_limit });
4402
+ const pending_total = mergedPending.total;
4403
+ const pending_truncated = mergedPending.truncated;
4404
+ const pending_changes = mergedPending.page;
3990
4405
  const q = args.query?.trim() ?? "";
3991
4406
  const semantic = q
3992
4407
  ? await Promise.race([
@@ -4009,6 +4424,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4009
4424
  pending_total,
4010
4425
  pending_returned: pending_changes.length,
4011
4426
  requirements_returned: items.length,
4427
+ decisions_returned: decisions.length,
4012
4428
  conventions_returned: conventions.length,
4013
4429
  semantic_mode: semantic?.mode ?? null,
4014
4430
  semantic_matches: semantic?.matches?.length ?? 0,
@@ -4034,9 +4450,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4034
4450
  requirements_limit: requirementsLimit,
4035
4451
  changes_limit: changesLimit,
4036
4452
  notes_limit: notesLimit,
4453
+ decisions_limit: decisionsLimit,
4037
4454
  conventions_limit: conventionsLimit,
4038
4455
  },
4039
4456
  project_summary,
4457
+ decisions,
4040
4458
  conventions,
4041
4459
  recent_notes,
4042
4460
  pending_total,
@@ -4066,6 +4484,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4066
4484
  const changesLimit = args.changes_limit;
4067
4485
  const notesLimit = args.notes_limit;
4068
4486
  const conventionsLimit = args.conventions_limit;
4487
+ const decisionsLimit = args.decisions_limit;
4069
4488
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
4070
4489
  const items = recent.map((req) => {
4071
4490
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -4079,17 +4498,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4079
4498
  ? toMemoryItemPreview(projectSummaryRow, includeContent, previewChars, contentMaxChars)
4080
4499
  : null;
4081
4500
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
4501
+ const decisions = getDecisionPreviews(decisionsLimit, previewChars, contentMaxChars);
4082
4502
  const conventions = getConventionPreviews(conventionsLimit, previewChars, contentMaxChars);
4083
- const pending_total = Number(countPendingChangesStmt.get()?.total ?? 0);
4084
4503
  const pending_offset = args.pending_offset;
4085
4504
  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));
4505
+ const pendingDbRows = listPendingChangesStmt.all();
4506
+ const mergedPending = mergePendingWithGit(pendingDbRows, { offset: pending_offset, limit: pending_limit });
4507
+ const pending_total = mergedPending.total;
4508
+ const pending_truncated = mergedPending.truncated;
4509
+ const pending_changes = mergedPending.page;
4088
4510
  logActivity("get_brain_dump", {
4089
4511
  pending_total,
4090
4512
  pending_returned: pending_changes.length,
4091
4513
  requirements_returned: items.length,
4092
4514
  notes_returned: recent_notes.length,
4515
+ decisions_returned: decisions.length,
4093
4516
  conventions_returned: conventions.length,
4094
4517
  });
4095
4518
  const outputValue = {
@@ -4113,9 +4536,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4113
4536
  requirements_limit: requirementsLimit,
4114
4537
  changes_limit: changesLimit,
4115
4538
  notes_limit: notesLimit,
4539
+ decisions_limit: decisionsLimit,
4116
4540
  conventions_limit: conventionsLimit,
4117
4541
  },
4118
4542
  project_summary,
4543
+ decisions,
4119
4544
  conventions,
4120
4545
  recent_notes,
4121
4546
  pending_total,
@@ -4138,11 +4563,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4138
4563
  if (toolName === "get_pending_changes") {
4139
4564
  const args = GetPendingChangesArgsSchema.parse(rawArgs);
4140
4565
  flushPendingChangeBuffer();
4141
- const total = Number(countPendingChangesStmt.get()?.total ?? 0);
4142
4566
  const offset = args.offset;
4143
4567
  const limit = args.limit;
4144
- const truncated = total > offset + limit;
4145
- const pending = listPendingChangesPageStmt.all(limit, offset).filter((p) => !shouldIgnoreDbFilePath(p.file_path));
4568
+ const pendingDbRows = listPendingChangesStmt.all();
4569
+ const mergedPending = mergePendingWithGit(pendingDbRows, { offset, limit });
4570
+ const total = mergedPending.total;
4571
+ const truncated = mergedPending.truncated;
4572
+ const pending = mergedPending.page;
4146
4573
  logActivity("get_pending_changes", {
4147
4574
  total,
4148
4575
  offset,
@@ -4315,8 +4742,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4315
4742
  DetectRtkArgsSchema.parse(rawArgs);
4316
4743
  const result = detectRtk();
4317
4744
  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}`;
4745
+ ? `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}`
4746
+ : `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
4747
  return { content: [{ type: "text", text }] };
4321
4748
  }
4322
4749
  if (toolName === "install_rtk") {
@@ -4812,6 +5239,95 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4812
5239
  ],
4813
5240
  };
4814
5241
  }
5242
+ if (toolName === "upsert_decision") {
5243
+ const args = UpsertDecisionArgsSchema.parse(rawArgs);
5244
+ const key = args.key.trim();
5245
+ const title = args.title.trim() || key;
5246
+ const content = args.content.trim();
5247
+ const meta = {
5248
+ status: "current",
5249
+ key,
5250
+ title,
5251
+ tags: args.tags ?? [],
5252
+ supersedes_req_ids: args.supersedes_req_ids ?? [],
5253
+ supersedes_memory_ids: args.supersedes_memory_ids ?? [],
5254
+ related_files: (args.related_files ?? []).map((f) => normalizeToDbPath(f)),
5255
+ };
5256
+ upsertDecisionStmt.run(key, `${title}\n\n${content}`, safeJson(meta), sha256Hex(`${title}\n\n${content}`));
5257
+ const row = getDecisionByKeyStmt.get(key);
5258
+ if (row)
5259
+ enqueueEmbedding(row.id);
5260
+ const superseded_requirements = supersedeRequirementIds(args.supersedes_req_ids ?? [], {
5261
+ decision_id: row?.id,
5262
+ reason: `Superseded by decision ${key}: ${title}`,
5263
+ });
5264
+ const superseded_memory_items = supersedeMemoryItemIds(args.supersedes_memory_ids ?? [], {
5265
+ decision_id: row?.id,
5266
+ reason: `Superseded by decision ${key}: ${title}`,
5267
+ });
5268
+ logActivity("upsert_decision", {
5269
+ key,
5270
+ decision_id: row?.id ?? null,
5271
+ superseded_requirements,
5272
+ superseded_memory_items,
5273
+ });
5274
+ return {
5275
+ content: [
5276
+ {
5277
+ type: "text",
5278
+ text: toolJson({
5279
+ ok: true,
5280
+ decision: row ? { id: row.id, key, updated_at: row.updated_at } : null,
5281
+ superseded_requirements,
5282
+ superseded_memory_items,
5283
+ }),
5284
+ },
5285
+ ],
5286
+ };
5287
+ }
5288
+ if (toolName === "supersede_memory") {
5289
+ const args = SupersedeMemoryArgsSchema.parse(rawArgs);
5290
+ const supersededReqIds = args.superseded_req_ids ?? [];
5291
+ const supersededMemoryIds = args.superseded_memory_ids ?? [];
5292
+ if (!supersededReqIds.length && !supersededMemoryIds.length) {
5293
+ return {
5294
+ isError: true,
5295
+ content: [
5296
+ {
5297
+ type: "text",
5298
+ text: toolJson({
5299
+ ok: false,
5300
+ error: "Provide superseded_req_ids and/or superseded_memory_ids.",
5301
+ }),
5302
+ },
5303
+ ],
5304
+ };
5305
+ }
5306
+ const superseded_requirements = supersedeRequirementIds(supersededReqIds, {
5307
+ req_id: args.replacement_req_id,
5308
+ memory_id: args.replacement_memory_id,
5309
+ reason: args.reason,
5310
+ });
5311
+ const superseded_memory_items = supersedeMemoryItemIds(supersededMemoryIds, {
5312
+ req_id: args.replacement_req_id,
5313
+ memory_id: args.replacement_memory_id,
5314
+ reason: args.reason,
5315
+ });
5316
+ logActivity("supersede_memory", {
5317
+ superseded_requirements,
5318
+ superseded_memory_items,
5319
+ replacement_req_id: args.replacement_req_id ?? null,
5320
+ replacement_memory_id: args.replacement_memory_id ?? null,
5321
+ });
5322
+ return {
5323
+ content: [
5324
+ {
5325
+ type: "text",
5326
+ text: toolJson({ ok: true, superseded_requirements, superseded_memory_items }),
5327
+ },
5328
+ ],
5329
+ };
5330
+ }
4815
5331
  if (toolName === "upsert_convention") {
4816
5332
  const args = UpsertConventionArgsSchema.parse(rawArgs);
4817
5333
  const key = args.key.trim();