@hiveai/mcp 0.10.4 → 0.10.8

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/server.js CHANGED
@@ -1415,9 +1415,8 @@ async function memSessionEnd(input, ctx) {
1415
1415
  }
1416
1416
 
1417
1417
  // src/tools/get-briefing.ts
1418
- import { readFile as readFile3, readdir as readdir3, writeFile as writeFile11 } from "fs/promises";
1419
- import { existsSync as existsSync18 } from "fs";
1420
- import path9 from "path";
1418
+ import { readFile as readFile4, writeFile as writeFile11 } from "fs/promises";
1419
+ import { existsSync as existsSync19 } from "fs";
1421
1420
  import {
1422
1421
  allocateBudget,
1423
1422
  DEFAULT_AUTO_PROMOTE_RULE,
@@ -1426,11 +1425,9 @@ import {
1426
1425
  extractActionsBriefBody,
1427
1426
  getUsage as getUsage5,
1428
1427
  inferModulesFromPaths as inferModulesFromPaths2,
1429
- isGlobPath,
1430
- isRetiredMemory,
1431
1428
  isAutoPromoteEligible,
1432
1429
  isDecaying,
1433
- isStackPackSeed,
1430
+ isRetiredMemory,
1434
1431
  literalMatchesAllTokens as literalMatchesAllTokens2,
1435
1432
  literalMatchesAnyToken as literalMatchesAnyToken2,
1436
1433
  loadCodeMap,
@@ -1438,7 +1435,6 @@ import {
1438
1435
  loadMemoriesFromDir as loadMemoriesFromDir13,
1439
1436
  loadUsageIndex as loadUsageIndex7,
1440
1437
  memoryMatchesAnchorPaths as memoryMatchesAnchorPaths2,
1441
- pathsOverlap,
1442
1438
  queryCodeMap,
1443
1439
  resolveBriefingBudget,
1444
1440
  serializeMemory as serializeMemory9,
@@ -1450,6 +1446,152 @@ import {
1450
1446
  writeBriefingMarker
1451
1447
  } from "@hiveai/core";
1452
1448
  import { z as z17 } from "zod";
1449
+
1450
+ // src/tools/briefing-helpers.ts
1451
+ import { readdir as readdir3, readFile as readFile3 } from "fs/promises";
1452
+ import { existsSync as existsSync18 } from "fs";
1453
+ import path9 from "path";
1454
+ import { isGlobPath, isStackPackSeed, pathsOverlap } from "@hiveai/core";
1455
+ function compactSummary(body) {
1456
+ for (const line of body.split("\n")) {
1457
+ const trimmed = line.replace(/^#+\s*/, "").trim();
1458
+ if (trimmed.length > 0) return trimmed.slice(0, 120);
1459
+ }
1460
+ return body.slice(0, 120);
1461
+ }
1462
+ function classifyMemoryPriority(memory, loaded, inputFiles, inputSymbols) {
1463
+ const fm = loaded?.memory.frontmatter;
1464
+ const directAnchor = Boolean(
1465
+ fm && inputFiles.length > 0 && fm.anchor.paths.some((p) => inputFiles.some((file) => pathsOverlap(p, file)))
1466
+ );
1467
+ const directSymbol = Boolean(
1468
+ fm && inputSymbols.length > 0 && fm.anchor.symbols.some(
1469
+ (sym) => inputSymbols.some((wanted) => wanted.toLowerCase() === sym.toLowerCase())
1470
+ )
1471
+ );
1472
+ const strongSemantic = (memory.semantic_score ?? 0) >= 0.65;
1473
+ const usefulSemantic = (memory.semantic_score ?? 0) >= 0.35;
1474
+ if (fm?.requires_human_approval || directAnchor || directSymbol || memory.type === "attempt" && (memory.match_quality === "exact" || strongSemantic) || memory.type === "skill" && (memory.match_quality === "exact" || strongSemantic)) {
1475
+ return "must_read";
1476
+ }
1477
+ if (isStackPackSeed(fm)) {
1478
+ return "background";
1479
+ }
1480
+ if (memory.type === "skill" || memory.reasons.includes("module") || memory.reasons.includes("domain") || memory.match_quality === "exact" || usefulSemantic) {
1481
+ return "useful";
1482
+ }
1483
+ return "background";
1484
+ }
1485
+ function priorityRank(priority) {
1486
+ return priority === "must_read" ? 3 : priority === "useful" ? 2 : 1;
1487
+ }
1488
+ function classifyBriefingQuality(memories, context) {
1489
+ const mustRead = memories.filter((m) => m.priority === "must_read").length;
1490
+ const useful = memories.filter((m) => m.priority === "useful").length;
1491
+ const background = memories.filter((m) => m.priority === "background").length;
1492
+ const weakSemantic = memories.filter(
1493
+ (m) => m.reasons.length === 1 && m.reasons.includes("semantic") && (m.semantic_score ?? 0) > 0 && (m.semantic_score ?? 0) < 0.35
1494
+ ).length;
1495
+ const reasons = [];
1496
+ if (memories.length === 0) reasons.push("no memories matched the task or files");
1497
+ if (context.isTemplateContext && !context.autoContextGenerated) reasons.push("project context is still a template");
1498
+ if (!context.hasLastSession) reasons.push("no previous session recap");
1499
+ if (mustRead > 0) reasons.push(`${mustRead} must_read memor${mustRead === 1 ? "y" : "ies"} matched directly`);
1500
+ if (useful > 0) reasons.push(`${useful} useful memor${useful === 1 ? "y" : "ies"} matched`);
1501
+ if (background > useful + mustRead && background > 2) reasons.push(`${background} background memories dominate the result`);
1502
+ if (weakSemantic > 0) reasons.push(`${weakSemantic} weak semantic-only match${weakSemantic === 1 ? "" : "es"}`);
1503
+ if (context.searchMode === "literal_fallback") reasons.push("semantic index unavailable or empty; literal fallback used");
1504
+ if (memories.length === 0 || mustRead === 0 && useful === 0) {
1505
+ return { level: "thin", reasons };
1506
+ }
1507
+ if (background > useful + mustRead && background > 2) {
1508
+ return { level: "noisy", reasons };
1509
+ }
1510
+ return { level: "strong", reasons };
1511
+ }
1512
+ function explainWhySurfaced(memory, loaded, inputFiles, inferredModules) {
1513
+ const why = [];
1514
+ const fm = loaded?.memory.frontmatter;
1515
+ if (memory.reasons.includes("anchor") && fm) {
1516
+ const matching = fm.anchor.paths.filter(
1517
+ (p) => inputFiles.length === 0 || inputFiles.some((file) => pathsOverlap(p, file))
1518
+ );
1519
+ if (matching.length > 0) {
1520
+ const exact = matching.filter(
1521
+ (p) => !isGlobPath(p) && inputFiles.some((file) => p === file || pathsOverlap(p, file))
1522
+ );
1523
+ const glob = matching.filter((p) => isGlobPath(p));
1524
+ if (exact.length > 0) {
1525
+ why.push(`Exact/file anchor match: ${exact.slice(0, 4).join(", ")}`);
1526
+ }
1527
+ if (glob.length > 0) {
1528
+ why.push(`Glob anchor match: ${glob.slice(0, 4).join(", ")}`);
1529
+ }
1530
+ if (exact.length === 0 && glob.length === 0) {
1531
+ why.push(`Anchored to touched path${matching.length === 1 ? "" : "s"}: ${matching.slice(0, 4).join(", ")}`);
1532
+ }
1533
+ } else if (fm.anchor.paths.length > 0) {
1534
+ why.push(`Pulled by related anchor: ${fm.anchor.paths.slice(0, 4).join(", ")}`);
1535
+ }
1536
+ if (fm.anchor.symbols.length > 0) {
1537
+ why.push(`Anchor symbol${fm.anchor.symbols.length === 1 ? "" : "s"}: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
1538
+ }
1539
+ }
1540
+ if (memory.reasons.includes("symbol") && fm) {
1541
+ why.push(`Explicit symbol match: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
1542
+ }
1543
+ if (memory.reasons.includes("module")) {
1544
+ const moduleHints = [
1545
+ ...memory.module ? [memory.module] : [],
1546
+ ...memory.tags.filter((tag) => inferredModules.includes(tag))
1547
+ ];
1548
+ const shown = moduleHints.length > 0 ? [...new Set(moduleHints)].join(", ") : inferredModules.join(", ");
1549
+ why.push(shown ? `Matched inferred module/tag: ${shown}` : "Matched inferred module context.");
1550
+ }
1551
+ if (memory.reasons.includes("domain")) {
1552
+ why.push("Matched inferred domain from the target file paths.");
1553
+ }
1554
+ if (memory.reasons.includes("semantic")) {
1555
+ const score = memory.semantic_score !== void 0 ? ` score=${Math.round(memory.semantic_score * 100) / 100}` : "";
1556
+ why.push(`${memory.match_quality === "exact" ? "Literal task match" : "Semantic/task relevance"}${score}.`);
1557
+ }
1558
+ why.push(`Confidence: ${memory.confidence}; read ${memory.read_count} time${memory.read_count === 1 ? "" : "s"}.`);
1559
+ if (memory.type === "attempt") why.push("Failed-approach record; read before repeating the same path.");
1560
+ if (memory.type === "skill") why.push("Skill (reusable procedure/playbook) \u2014 follow the steps described when doing this type of task.");
1561
+ if (memory.status === "proposed" || memory.status === "draft") {
1562
+ why.push("Unvalidated record; use cautiously or ask a human before treating it as policy.");
1563
+ }
1564
+ return why;
1565
+ }
1566
+ async function trySemanticHits(ctx, task, limit) {
1567
+ let mod;
1568
+ try {
1569
+ mod = await import("@hiveai/embeddings");
1570
+ } catch {
1571
+ return null;
1572
+ }
1573
+ const result = await mod.semanticSearch(ctx.paths, task, { limit });
1574
+ if (!result) return null;
1575
+ return result.hits.map((h) => ({ id: h.id, score: h.score }));
1576
+ }
1577
+ async function loadModuleContexts2(ctx, modules) {
1578
+ if (modules.length === 0) return [];
1579
+ if (!existsSync18(ctx.paths.modulesContextDir)) return [];
1580
+ const available = new Set(
1581
+ (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
1582
+ );
1583
+ const out = [];
1584
+ for (const m of modules) {
1585
+ if (!available.has(m)) continue;
1586
+ const file = path9.join(ctx.paths.modulesContextDir, m, "context.md");
1587
+ if (existsSync18(file)) {
1588
+ out.push({ name: m, content: await readFile3(file, "utf8") });
1589
+ }
1590
+ }
1591
+ return out;
1592
+ }
1593
+
1594
+ // src/tools/get-briefing.ts
1453
1595
  var GetBriefingInputSchema = {
1454
1596
  task: z17.string().optional().describe(
1455
1597
  "What you are about to do, in 1\u20132 sentences. Used to rank relevant memories semantically."
@@ -1495,7 +1637,7 @@ async function getBriefing(input, ctx) {
1495
1637
  let usage = { version: 1, updated_at: "", by_id: {} };
1496
1638
  let byId = /* @__PURE__ */ new Map();
1497
1639
  let lastSession;
1498
- if (existsSync18(ctx.paths.memoriesDir)) {
1640
+ if (existsSync19(ctx.paths.memoriesDir)) {
1499
1641
  const allLoaded = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1500
1642
  const recaps = allLoaded.filter(({ memory }) => memory.frontmatter.type === "session_recap").sort(
1501
1643
  (a, b) => new Date(b.memory.frontmatter.created_at).getTime() - new Date(a.memory.frontmatter.created_at).getTime()
@@ -1580,34 +1722,25 @@ async function getBriefing(input, ctx) {
1580
1722
  if (input.task) {
1581
1723
  const tokens = tokenizeQuery2(input.task);
1582
1724
  const andHits = allMemories.filter((m) => literalMatchesAllTokens2(m.memory, tokens));
1583
- for (const loaded of andHits) {
1584
- addOrUpdate(loaded, "semantic", void 0, "exact");
1585
- }
1725
+ for (const loaded of andHits) addOrUpdate(loaded, "semantic", void 0, "exact");
1586
1726
  if (andHits.length === 0 && tokens.length > 1) {
1587
1727
  for (const loaded of allMemories) {
1588
- if (literalMatchesAnyToken2(loaded.memory, tokens)) {
1589
- addOrUpdate(loaded, "semantic", void 0, "partial");
1590
- }
1728
+ if (literalMatchesAnyToken2(loaded.memory, tokens)) addOrUpdate(loaded, "semantic", void 0, "partial");
1591
1729
  }
1592
1730
  }
1593
1731
  if (semanticHits) {
1594
1732
  for (const hit of semanticHits) {
1595
- if (hit.score < input.min_semantic_score) {
1596
- const existing = seen.get(hit.id);
1597
- if (!existing) continue;
1598
- }
1733
+ if (hit.score < input.min_semantic_score && !seen.has(hit.id)) continue;
1599
1734
  const loaded = byId.get(hit.id);
1600
1735
  if (loaded) addOrUpdate(loaded, "semantic", hit.score, "semantic");
1601
1736
  }
1602
1737
  }
1603
1738
  }
1604
1739
  const ranked = [...seen.values()].sort((a, b) => {
1605
- const priorityScore = (m) => priorityRank(classifyMemoryPriority(m, byId.get(m.id), input.files, input.symbols));
1606
- const reasonScore = (m) => (m.type === "attempt" ? 3 : 0) + // attempt = negative knowledge, surface first to prevent repeating mistakes
1607
- (m.reasons.includes("anchor") ? 4 : 0) + (m.reasons.includes("symbol") ? 4 : 0) + (m.reasons.includes("module") ? 2 : 0) + (m.reasons.includes("semantic") ? 2 : 0) + (m.reasons.includes("domain") ? 1 : 0);
1740
+ const reasonScore = (m) => (m.type === "attempt" ? 3 : 0) + (m.reasons.includes("anchor") ? 4 : 0) + (m.reasons.includes("symbol") ? 4 : 0) + (m.reasons.includes("module") ? 2 : 0) + (m.reasons.includes("semantic") ? 2 : 0) + (m.reasons.includes("domain") ? 1 : 0);
1608
1741
  const confidenceScore = (m) => m.confidence === "authoritative" ? 4 : m.confidence === "trusted" ? 3 : m.confidence === "low" ? 1 : m.confidence === "stale" ? -2 : 0;
1609
- const sa = priorityScore(a) * 100 + reasonScore(a) + confidenceScore(a) + (a.semantic_score ?? 0);
1610
- const sb = priorityScore(b) * 100 + reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
1742
+ const sa = priorityRank(classifyMemoryPriority(a, byId.get(a.id), input.files, input.symbols)) * 100 + reasonScore(a) + confidenceScore(a) + (a.semantic_score ?? 0);
1743
+ const sb = priorityRank(classifyMemoryPriority(b, byId.get(b.id), input.files, input.symbols)) * 100 + reasonScore(b) + confidenceScore(b) + (b.semantic_score ?? 0);
1611
1744
  return sb - sa;
1612
1745
  });
1613
1746
  for (const mem of ranked.slice(0, briefingMaxMemories)) {
@@ -1636,11 +1769,7 @@ async function getBriefing(input, ctx) {
1636
1769
  if (!isAutoPromoteEligible(loaded.memory.frontmatter, u, rule)) continue;
1637
1770
  const newFm = { ...loaded.memory.frontmatter, status: "validated" };
1638
1771
  try {
1639
- await writeFile11(
1640
- loaded.filePath,
1641
- serializeMemory9({ frontmatter: newFm, body: loaded.memory.body }),
1642
- "utf8"
1643
- );
1772
+ await writeFile11(loaded.filePath, serializeMemory9({ frontmatter: newFm, body: loaded.memory.body }), "utf8");
1644
1773
  m.status = "validated";
1645
1774
  m.confidence = "trusted";
1646
1775
  } catch {
@@ -1648,12 +1777,12 @@ async function getBriefing(input, ctx) {
1648
1777
  }
1649
1778
  }
1650
1779
  }
1651
- const projectContextRaw = input.include_project_context && existsSync18(ctx.paths.projectContext) ? await readFile3(ctx.paths.projectContext, "utf8") : "";
1780
+ const projectContextRaw = input.include_project_context && existsSync19(ctx.paths.projectContext) ? await readFile4(ctx.paths.projectContext, "utf8") : "";
1652
1781
  const isTemplateContext = projectContextRaw.includes("TODO \u2014 high-level overview") || projectContextRaw.includes("Generated by `haive init`");
1653
1782
  const setupWarnings = [];
1654
1783
  let autoContextGenerated = false;
1655
1784
  let projectContext = isTemplateContext ? "" : projectContextRaw;
1656
- if ((isTemplateContext || !existsSync18(ctx.paths.projectContext)) && input.include_project_context) {
1785
+ if ((isTemplateContext || !existsSync19(ctx.paths.projectContext)) && input.include_project_context) {
1657
1786
  const haiveConfig = await loadConfig3(ctx.paths);
1658
1787
  if (haiveConfig.autoContext) {
1659
1788
  const codeMap = await loadCodeMap(ctx.paths);
@@ -1689,15 +1818,9 @@ async function getBriefing(input, ctx) {
1689
1818
  );
1690
1819
  }
1691
1820
  } else {
1692
- if (isTemplateContext) {
1693
- setupWarnings.push(
1694
- "project-context.md still contains the default template. Invoke the bootstrap_project MCP prompt to auto-fill it from your codebase. Until then, get_briefing returns no project context."
1695
- );
1696
- } else {
1697
- setupWarnings.push(
1698
- "No project-context.md found. Run `haive init` then invoke the bootstrap_project MCP prompt."
1699
- );
1700
- }
1821
+ setupWarnings.push(
1822
+ isTemplateContext ? "project-context.md still contains the default template. Invoke the bootstrap_project MCP prompt to auto-fill it from your codebase. Until then, get_briefing returns no project context." : "No project-context.md found. Run `haive init` then invoke the bootstrap_project MCP prompt."
1823
+ );
1701
1824
  }
1702
1825
  }
1703
1826
  const moduleContents = briefingIncludeModules ? await loadModuleContexts2(ctx, inferred) : [];
@@ -1760,10 +1883,7 @@ ${m.content}`).join("\n\n---\n\n"),
1760
1883
  const createdAt = loaded?.memory.frontmatter.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
1761
1884
  if (isDecaying(u, createdAt)) decayWarnings.push(m.id);
1762
1885
  }
1763
- const formattedMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({
1764
- ...m,
1765
- body: extractActionsBriefBody(m.body)
1766
- })) : trimmedMemories;
1886
+ const formattedMemories = input.format === "compact" ? trimmedMemories.map((m) => ({ ...m, body: compactSummary(m.body) })) : input.format === "actions" ? trimmedMemories.map((m) => ({ ...m, body: extractActionsBriefBody(m.body) })) : trimmedMemories;
1767
1887
  const outputMemories = formattedMemories.map((m) => ({
1768
1888
  ...m,
1769
1889
  priority: classifyMemoryPriority(m, byId.get(m.id), input.files, input.symbols),
@@ -1778,8 +1898,7 @@ ${m.content}`).join("\n\n---\n\n"),
1778
1898
  let symbolLocations;
1779
1899
  const symbolsToLookup = new Set(input.symbols);
1780
1900
  for (const m of outputMemories) {
1781
- const loaded = byId.get(m.id);
1782
- for (const sym of loaded?.memory.frontmatter.anchor.symbols ?? []) {
1901
+ for (const sym of byId.get(m.id)?.memory.frontmatter.anchor.symbols ?? []) {
1783
1902
  symbolsToLookup.add(sym);
1784
1903
  }
1785
1904
  }
@@ -1807,41 +1926,37 @@ ${m.content}`).join("\n\n---\n\n"),
1807
1926
  }
1808
1927
  }
1809
1928
  const actionRequired = [];
1810
- for (const m of outputMemories) {
1811
- const loaded = byId.get(m.id);
1812
- if (!loaded?.memory.frontmatter.requires_human_approval) continue;
1813
- const bodyLines = loaded.memory.body.split("\n");
1929
+ const extractActionItem = (id, body) => {
1930
+ const bodyLines = body.split("\n");
1814
1931
  const quoteBlock = bodyLines.filter((l) => l.startsWith("> ")).map((l) => l.slice(2)).join(" ").replace(/^\*«\s*/, "").replace(/\s*»\*$/, "").trim();
1815
1932
  const headingLine = bodyLines.find((l) => l.startsWith("## "));
1816
- const summary = headingLine?.replace(/^##\s*/, "").trim() ?? m.id;
1817
- actionRequired.push({
1818
- id: m.id,
1933
+ const summary = headingLine?.replace(/^##\s*/, "").trim() ?? id;
1934
+ return {
1935
+ id,
1819
1936
  summary,
1820
- developer_message: quoteBlock || `Une modification externe potentiellement incompatible a \xE9t\xE9 d\xE9tect\xE9e (${m.id}). Veux-tu que j'analyse l'impact et que je propose des mises \xE0 jour ?`
1821
- });
1937
+ developer_message: quoteBlock || `Une modification externe potentiellement incompatible a \xE9t\xE9 d\xE9tect\xE9e (${id}). Veux-tu que j'analyse l'impact et que je propose des mises \xE0 jour ?`
1938
+ };
1939
+ };
1940
+ for (const m of outputMemories) {
1941
+ const loaded = byId.get(m.id);
1942
+ if (loaded?.memory.frontmatter.requires_human_approval) {
1943
+ actionRequired.push(extractActionItem(m.id, loaded.memory.body));
1944
+ }
1822
1945
  }
1823
- if (existsSync18(ctx.paths.memoriesDir)) {
1946
+ if (existsSync19(ctx.paths.memoriesDir)) {
1824
1947
  const allMems = await loadMemoriesFromDir13(ctx.paths.memoriesDir);
1825
1948
  for (const { memory } of allMems) {
1826
1949
  const fm = memory.frontmatter;
1827
1950
  if (!fm.requires_human_approval) continue;
1828
1951
  if (fm.status === "rejected" || fm.status === "deprecated") continue;
1829
1952
  if (actionRequired.some((a) => a.id === fm.id)) continue;
1830
- const bodyLines = memory.body.split("\n");
1831
- const quoteBlock = bodyLines.filter((l) => l.startsWith("> ")).map((l) => l.slice(2)).join(" ").replace(/^\*«\s*/, "").replace(/\s*»\*$/, "").trim();
1832
- const headingLine = bodyLines.find((l) => l.startsWith("## "));
1833
- const summary = headingLine?.replace(/^##\s*/, "").trim() ?? fm.id;
1834
- actionRequired.push({
1835
- id: fm.id,
1836
- summary,
1837
- developer_message: quoteBlock || `Une modification externe potentiellement incompatible a \xE9t\xE9 d\xE9tect\xE9e (${fm.id}). Veux-tu que j'analyse l'impact et que je propose des mises \xE0 jour ?`
1838
- });
1953
+ actionRequired.push(extractActionItem(fm.id, memory.body));
1839
1954
  }
1840
1955
  }
1841
1956
  const pendingDistillFile = pendingDistillPath(ctx);
1842
- if (existsSync18(pendingDistillFile)) {
1957
+ if (existsSync19(pendingDistillFile)) {
1843
1958
  try {
1844
- const raw = await readFile3(pendingDistillFile, "utf8");
1959
+ const raw = await readFile4(pendingDistillFile, "utf8");
1845
1960
  const pd = JSON.parse(raw);
1846
1961
  const ageMs = Date.now() - new Date(pd.session_end).getTime();
1847
1962
  const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1e3;
@@ -1868,7 +1983,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
1868
1983
  }
1869
1984
  }
1870
1985
  const memoriesEmpty = outputMemories.length === 0;
1871
- const hasMemoriesDir = existsSync18(ctx.paths.memoriesDir);
1986
+ const hasMemoriesDir = existsSync19(ctx.paths.memoriesDir);
1872
1987
  const isColdStart = isTemplateContext && memoriesEmpty && !lastSession && !autoContextGenerated;
1873
1988
  const hasUnguessableSignal = outputMemories.some(
1874
1989
  (m) => (m.priority === "must_read" || m.priority === "useful") && specificityScore(m.body) >= GUESSABLE_THRESHOLD
@@ -1915,7 +2030,7 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
1915
2030
  "No team-specific policy matched these files/task \u2014 nothing here a capable model can't infer. The auto-generated project context was trimmed to keep this briefing near-zero-cost; proceed with normal Read/Grep."
1916
2031
  );
1917
2032
  }
1918
- if (existsSync18(ctx.paths.haiveDir)) {
2033
+ if (existsSync19(ctx.paths.haiveDir)) {
1919
2034
  await writeBriefingMarker(ctx.paths, {
1920
2035
  sessionId: process.env.HAIVE_SESSION_ID,
1921
2036
  ...input.task ? { task: input.task } : {},
@@ -1963,144 +2078,6 @@ When done, call \`mem_session_end\` to acknowledge \u2014 this clears the pendin
1963
2078
  }
1964
2079
  };
1965
2080
  }
1966
- function compactSummary(body) {
1967
- for (const line of body.split("\n")) {
1968
- const trimmed = line.replace(/^#+\s*/, "").trim();
1969
- if (trimmed.length > 0) return trimmed.slice(0, 120);
1970
- }
1971
- return body.slice(0, 120);
1972
- }
1973
- function classifyMemoryPriority(memory, loaded, inputFiles, inputSymbols) {
1974
- const fm = loaded?.memory.frontmatter;
1975
- const directAnchor = Boolean(
1976
- fm && inputFiles.length > 0 && fm.anchor.paths.some((p) => inputFiles.some((file) => pathsOverlap(p, file)))
1977
- );
1978
- const directSymbol = Boolean(
1979
- fm && inputSymbols.length > 0 && fm.anchor.symbols.some(
1980
- (sym) => inputSymbols.some((wanted) => wanted.toLowerCase() === sym.toLowerCase())
1981
- )
1982
- );
1983
- const strongSemantic = (memory.semantic_score ?? 0) >= 0.65;
1984
- const usefulSemantic = (memory.semantic_score ?? 0) >= 0.35;
1985
- if (fm?.requires_human_approval || directAnchor || directSymbol || memory.type === "attempt" && (memory.match_quality === "exact" || strongSemantic) || memory.type === "skill" && (memory.match_quality === "exact" || strongSemantic)) {
1986
- return "must_read";
1987
- }
1988
- if (isStackPackSeed(fm)) {
1989
- return "background";
1990
- }
1991
- if (memory.type === "skill" || memory.reasons.includes("module") || memory.reasons.includes("domain") || memory.match_quality === "exact" || usefulSemantic) {
1992
- return "useful";
1993
- }
1994
- return "background";
1995
- }
1996
- function priorityRank(priority) {
1997
- return priority === "must_read" ? 3 : priority === "useful" ? 2 : 1;
1998
- }
1999
- function classifyBriefingQuality(memories, context) {
2000
- const mustRead = memories.filter((m) => m.priority === "must_read").length;
2001
- const useful = memories.filter((m) => m.priority === "useful").length;
2002
- const background = memories.filter((m) => m.priority === "background").length;
2003
- const weakSemantic = memories.filter(
2004
- (m) => m.reasons.length === 1 && m.reasons.includes("semantic") && (m.semantic_score ?? 0) > 0 && (m.semantic_score ?? 0) < 0.35
2005
- ).length;
2006
- const reasons = [];
2007
- if (memories.length === 0) reasons.push("no memories matched the task or files");
2008
- if (context.isTemplateContext && !context.autoContextGenerated) reasons.push("project context is still a template");
2009
- if (!context.hasLastSession) reasons.push("no previous session recap");
2010
- if (mustRead > 0) reasons.push(`${mustRead} must_read memor${mustRead === 1 ? "y" : "ies"} matched directly`);
2011
- if (useful > 0) reasons.push(`${useful} useful memor${useful === 1 ? "y" : "ies"} matched`);
2012
- if (background > useful + mustRead && background > 2) reasons.push(`${background} background memories dominate the result`);
2013
- if (weakSemantic > 0) reasons.push(`${weakSemantic} weak semantic-only match${weakSemantic === 1 ? "" : "es"}`);
2014
- if (context.searchMode === "literal_fallback") reasons.push("semantic index unavailable or empty; literal fallback used");
2015
- if (memories.length === 0 || mustRead === 0 && useful === 0) {
2016
- return { level: "thin", reasons };
2017
- }
2018
- if (background > useful + mustRead && background > 2) {
2019
- return { level: "noisy", reasons };
2020
- }
2021
- return { level: "strong", reasons };
2022
- }
2023
- function explainWhySurfaced(memory, loaded, inputFiles, inferredModules) {
2024
- const why = [];
2025
- const fm = loaded?.memory.frontmatter;
2026
- if (memory.reasons.includes("anchor") && fm) {
2027
- const matching = fm.anchor.paths.filter(
2028
- (p) => inputFiles.length === 0 || inputFiles.some((file) => pathsOverlap(p, file))
2029
- );
2030
- if (matching.length > 0) {
2031
- const exact = matching.filter(
2032
- (p) => !isGlobPath(p) && inputFiles.some((file) => p === file || pathsOverlap(p, file))
2033
- );
2034
- const glob = matching.filter((p) => isGlobPath(p));
2035
- if (exact.length > 0) {
2036
- why.push(`Exact/file anchor match: ${exact.slice(0, 4).join(", ")}`);
2037
- }
2038
- if (glob.length > 0) {
2039
- why.push(`Glob anchor match: ${glob.slice(0, 4).join(", ")}`);
2040
- }
2041
- if (exact.length === 0 && glob.length === 0) {
2042
- why.push(`Anchored to touched path${matching.length === 1 ? "" : "s"}: ${matching.slice(0, 4).join(", ")}`);
2043
- }
2044
- } else if (fm.anchor.paths.length > 0) {
2045
- why.push(`Pulled by related anchor: ${fm.anchor.paths.slice(0, 4).join(", ")}`);
2046
- }
2047
- if (fm.anchor.symbols.length > 0) {
2048
- why.push(`Anchor symbol${fm.anchor.symbols.length === 1 ? "" : "s"}: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
2049
- }
2050
- }
2051
- if (memory.reasons.includes("symbol") && fm) {
2052
- why.push(`Explicit symbol match: ${fm.anchor.symbols.slice(0, 4).join(", ")}`);
2053
- }
2054
- if (memory.reasons.includes("module")) {
2055
- const moduleHints = [
2056
- ...memory.module ? [memory.module] : [],
2057
- ...memory.tags.filter((tag) => inferredModules.includes(tag))
2058
- ];
2059
- const shown = moduleHints.length > 0 ? [...new Set(moduleHints)].join(", ") : inferredModules.join(", ");
2060
- why.push(shown ? `Matched inferred module/tag: ${shown}` : "Matched inferred module context.");
2061
- }
2062
- if (memory.reasons.includes("domain")) {
2063
- why.push("Matched inferred domain from the target file paths.");
2064
- }
2065
- if (memory.reasons.includes("semantic")) {
2066
- const score = memory.semantic_score !== void 0 ? ` score=${Math.round(memory.semantic_score * 100) / 100}` : "";
2067
- why.push(`${memory.match_quality === "exact" ? "Literal task match" : "Semantic/task relevance"}${score}.`);
2068
- }
2069
- why.push(`Confidence: ${memory.confidence}; read ${memory.read_count} time${memory.read_count === 1 ? "" : "s"}.`);
2070
- if (memory.type === "attempt") why.push("Failed-approach record; read before repeating the same path.");
2071
- if (memory.type === "skill") why.push("Skill (reusable procedure/playbook) \u2014 follow the steps described when doing this type of task.");
2072
- if (memory.status === "proposed" || memory.status === "draft") {
2073
- why.push("Unvalidated record; use cautiously or ask a human before treating it as policy.");
2074
- }
2075
- return why;
2076
- }
2077
- async function trySemanticHits(ctx, task, limit) {
2078
- let mod;
2079
- try {
2080
- mod = await import("@hiveai/embeddings");
2081
- } catch {
2082
- return null;
2083
- }
2084
- const result = await mod.semanticSearch(ctx.paths, task, { limit });
2085
- if (!result) return null;
2086
- return result.hits.map((h) => ({ id: h.id, score: h.score }));
2087
- }
2088
- async function loadModuleContexts2(ctx, modules) {
2089
- if (modules.length === 0) return [];
2090
- if (!existsSync18(ctx.paths.modulesContextDir)) return [];
2091
- const available = new Set(
2092
- (await readdir3(ctx.paths.modulesContextDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name)
2093
- );
2094
- const out = [];
2095
- for (const m of modules) {
2096
- if (!available.has(m)) continue;
2097
- const file = path9.join(ctx.paths.modulesContextDir, m, "context.md");
2098
- if (existsSync18(file)) {
2099
- out.push({ name: m, content: await readFile3(file, "utf8") });
2100
- }
2101
- }
2102
- return out;
2103
- }
2104
2081
 
2105
2082
  // src/tools/code-map.ts
2106
2083
  import { estimateTokens as estimateTokens2, loadCodeMap as loadCodeMap2, queryCodeMap as queryCodeMap2 } from "@hiveai/core";
@@ -2185,7 +2162,7 @@ function estimateFileEntryTokens(f) {
2185
2162
  }
2186
2163
 
2187
2164
  // src/tools/mem-diff.ts
2188
- import { existsSync as existsSync19 } from "fs";
2165
+ import { existsSync as existsSync20 } from "fs";
2189
2166
  import { loadMemoriesFromDir as loadMemoriesFromDir14 } from "@hiveai/core";
2190
2167
  import { z as z19 } from "zod";
2191
2168
  var MemDiffInputSchema = {
@@ -2193,7 +2170,7 @@ var MemDiffInputSchema = {
2193
2170
  id_b: z19.string().min(1).describe("Second memory id")
2194
2171
  };
2195
2172
  async function memDiff(input, ctx) {
2196
- if (!existsSync19(ctx.paths.memoriesDir)) {
2173
+ if (!existsSync20(ctx.paths.memoriesDir)) {
2197
2174
  throw new Error(`No .ai/memories at ${ctx.paths.root}.`);
2198
2175
  }
2199
2176
  const all = await loadMemoriesFromDir14(ctx.paths.memoriesDir);
@@ -2230,7 +2207,7 @@ async function memDiff(input, ctx) {
2230
2207
  }
2231
2208
 
2232
2209
  // src/tools/get-recap.ts
2233
- import { existsSync as existsSync20 } from "fs";
2210
+ import { existsSync as existsSync21 } from "fs";
2234
2211
  import { loadMemoriesFromDir as loadMemoriesFromDir15 } from "@hiveai/core";
2235
2212
  import { z as z20 } from "zod";
2236
2213
  var GetRecapInputSchema = {
@@ -2239,7 +2216,7 @@ var GetRecapInputSchema = {
2239
2216
  )
2240
2217
  };
2241
2218
  async function getRecap(input, ctx) {
2242
- if (!existsSync20(ctx.paths.memoriesDir)) {
2219
+ if (!existsSync21(ctx.paths.memoriesDir)) {
2243
2220
  return { recap: null, notice: "No .ai/memories directory \u2014 haive not initialized here." };
2244
2221
  }
2245
2222
  const all = await loadMemoriesFromDir15(ctx.paths.memoriesDir);
@@ -2338,7 +2315,7 @@ async function codeSearch(input, ctx) {
2338
2315
  }
2339
2316
 
2340
2317
  // src/tools/why-this-file.ts
2341
- import { existsSync as existsSync21 } from "fs";
2318
+ import { existsSync as existsSync22 } from "fs";
2342
2319
  import { spawn } from "child_process";
2343
2320
  import path10 from "path";
2344
2321
  import {
@@ -2358,7 +2335,7 @@ var WhyThisFileInputSchema = {
2358
2335
  memory_limit: z23.number().int().positive().max(20).default(5).describe("Cap on memories anchored to this path.")
2359
2336
  };
2360
2337
  async function whyThisFile(input, ctx) {
2361
- const fileExists = existsSync21(path10.join(ctx.paths.root, input.path));
2338
+ const fileExists = existsSync22(path10.join(ctx.paths.root, input.path));
2362
2339
  const [commits, memories, codeMap] = await Promise.all([
2363
2340
  runGitLog(ctx.paths.root, input.path, input.git_log_limit).catch(() => []),
2364
2341
  collectAnchoredMemories(ctx, input.path, input.memory_limit),
@@ -2399,7 +2376,7 @@ async function whyThisFile(input, ctx) {
2399
2376
  };
2400
2377
  }
2401
2378
  async function collectAnchoredMemories(ctx, filePath, limit) {
2402
- if (!existsSync21(ctx.paths.memoriesDir)) return [];
2379
+ if (!existsSync22(ctx.paths.memoriesDir)) return [];
2403
2380
  const all = await loadMemoriesFromDir16(ctx.paths.memoriesDir);
2404
2381
  const usage = await loadUsageIndex8(ctx.paths);
2405
2382
  const out = [];
@@ -2454,7 +2431,7 @@ function runCommand(cmd, args, cwd) {
2454
2431
  }
2455
2432
 
2456
2433
  // src/tools/anti-patterns-check.ts
2457
- import { existsSync as existsSync22 } from "fs";
2434
+ import { existsSync as existsSync23 } from "fs";
2458
2435
  import {
2459
2436
  addedLinesFromDiff,
2460
2437
  deriveConfidence as deriveConfidence6,
@@ -2548,7 +2525,7 @@ async function antiPatternsCheck(input, ctx) {
2548
2525
  notice: "Nothing to check \u2014 provide either `diff` text or `paths`."
2549
2526
  };
2550
2527
  }
2551
- if (!existsSync22(ctx.paths.memoriesDir)) {
2528
+ if (!existsSync23(ctx.paths.memoriesDir)) {
2552
2529
  return { scanned: 0, warnings: [], notice: "No .ai/memories directory \u2014 nothing to check against." };
2553
2530
  }
2554
2531
  const all = await loadMemoriesFromDir17(ctx.paths.memoriesDir);
@@ -2583,6 +2560,7 @@ async function antiPatternsCheck(input, ctx) {
2583
2560
  reasons: [reason],
2584
2561
  tags: fm.tags ?? [],
2585
2562
  anchor_paths: fm.anchor?.paths ?? [],
2563
+ ...fm.sensor != null ? { has_sensor: true } : {},
2586
2564
  ...score !== void 0 ? { semantic_score: score } : {}
2587
2565
  });
2588
2566
  };
@@ -2651,7 +2629,7 @@ async function antiPatternsCheck(input, ctx) {
2651
2629
  }
2652
2630
 
2653
2631
  // src/tools/mem-distill.ts
2654
- import { existsSync as existsSync23 } from "fs";
2632
+ import { existsSync as existsSync24 } from "fs";
2655
2633
  import {
2656
2634
  loadMemoriesFromDir as loadMemoriesFromDir18,
2657
2635
  tokenizeQuery as tokenizeQuery4
@@ -2703,7 +2681,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
2703
2681
  "error"
2704
2682
  ]);
2705
2683
  async function memDistill(input, ctx) {
2706
- if (!existsSync23(ctx.paths.memoriesDir)) {
2684
+ if (!existsSync24(ctx.paths.memoriesDir)) {
2707
2685
  return { scanned: 0, singletons: 0, clusters: [], notice: "No .ai/memories directory." };
2708
2686
  }
2709
2687
  const cutoff = Date.now() - input.since_days * MS_PER_DAY;
@@ -2811,7 +2789,7 @@ function firstHeading(body) {
2811
2789
  }
2812
2790
 
2813
2791
  // src/tools/why-this-decision.ts
2814
- import { existsSync as existsSync24 } from "fs";
2792
+ import { existsSync as existsSync25 } from "fs";
2815
2793
  import { spawn as spawn2 } from "child_process";
2816
2794
  import {
2817
2795
  deriveConfidence as deriveConfidence7,
@@ -2826,7 +2804,7 @@ var WhyThisDecisionInputSchema = {
2826
2804
  git_log_limit: z26.number().int().positive().max(20).default(5).describe("How many recent commits per anchor path to surface.")
2827
2805
  };
2828
2806
  async function whyThisDecision(input, ctx) {
2829
- if (!existsSync24(ctx.paths.memoriesDir)) {
2807
+ if (!existsSync25(ctx.paths.memoriesDir)) {
2830
2808
  return {
2831
2809
  found: false,
2832
2810
  related: [],
@@ -2958,7 +2936,7 @@ function runCommand2(cmd, args, cwd) {
2958
2936
  }
2959
2937
 
2960
2938
  // src/tools/mem-conflicts.ts
2961
- import { existsSync as existsSync25 } from "fs";
2939
+ import { existsSync as existsSync26 } from "fs";
2962
2940
  import {
2963
2941
  deriveConfidence as deriveConfidence8,
2964
2942
  getUsage as getUsage9,
@@ -2976,7 +2954,7 @@ var MemConflictsInputSchema = {
2976
2954
  var POSITIVE_PATTERNS = /\b(use|prefer|always|should use|do this|recommended|ok to)\b/i;
2977
2955
  var NEGATIVE_PATTERNS = /\b(do not use|don'?t use|never|avoid|forbidden|deprecated|stop using|do NOT|❌)\b/i;
2978
2956
  async function memConflicts(input, ctx) {
2979
- if (!existsSync25(ctx.paths.memoriesDir)) {
2957
+ if (!existsSync26(ctx.paths.memoriesDir)) {
2980
2958
  return { found: false, scanned: 0, conflicts: [], notice: "No .ai/memories directory." };
2981
2959
  }
2982
2960
  const all = await loadMemoriesFromDir20(ctx.paths.memoriesDir);
@@ -3200,6 +3178,15 @@ function classifyWarning(warning, paths, anchoredBlocks = false) {
3200
3178
  };
3201
3179
  }
3202
3180
  if (isBlockingWarning(warning)) {
3181
+ if (warning.has_sensor && !warning.reasons.includes("sensor")) {
3182
+ return {
3183
+ ...warning,
3184
+ level: "review",
3185
+ rationale: "memory has a sensor that did not fire \u2014 sensor is the authoritative check; strong semantic match alone is insufficient to block",
3186
+ affected_files: affectedFiles,
3187
+ repair_command: repairCommand
3188
+ };
3189
+ }
3203
3190
  return {
3204
3191
  ...warning,
3205
3192
  level: "blocking",
@@ -3212,6 +3199,15 @@ function classifyWarning(warning, paths, anchoredBlocks = false) {
3212
3199
  const semanticScore = warning.semantic_score ?? 0;
3213
3200
  const highConfidence = warning.confidence === "authoritative" || warning.confidence === "trusted";
3214
3201
  if (anchoredBlocks && highConfidence && warning.reasons.includes("anchor") && (warning.reasons.includes("literal") || hasSemantic && semanticScore >= 0.45)) {
3202
+ if (warning.has_sensor && !warning.reasons.includes("sensor")) {
3203
+ return {
3204
+ ...warning,
3205
+ level: "review",
3206
+ rationale: "memory has a sensor that did not fire \u2014 literal match alone is insufficient to block; sensor is the authoritative check",
3207
+ affected_files: affectedFiles,
3208
+ repair_command: repairCommand
3209
+ };
3210
+ }
3215
3211
  return {
3216
3212
  ...warning,
3217
3213
  level: "blocking",
@@ -3342,7 +3338,7 @@ function repairCommandForWarning(warning, paths) {
3342
3338
 
3343
3339
  // src/tools/pattern-detect.ts
3344
3340
  import { mkdir as mkdir7, writeFile as writeFile12 } from "fs/promises";
3345
- import { existsSync as existsSync26 } from "fs";
3341
+ import { existsSync as existsSync27 } from "fs";
3346
3342
  import path11 from "path";
3347
3343
  import { execSync as execSync2 } from "child_process";
3348
3344
  import {
@@ -3379,7 +3375,7 @@ var PatternDetectInputSchema = {
3379
3375
  scope: z29.enum(["personal", "team"]).default("team").describe("Scope for proposed memories.")
3380
3376
  };
3381
3377
  async function patternDetect(input, ctx) {
3382
- if (!existsSync26(ctx.paths.haiveDir)) {
3378
+ if (!existsSync27(ctx.paths.haiveDir)) {
3383
3379
  return {
3384
3380
  scanned_events: 0,
3385
3381
  matches: [],
@@ -3508,7 +3504,7 @@ async function patternDetect(input, ctx) {
3508
3504
  fm.id,
3509
3505
  void 0
3510
3506
  );
3511
- if (existsSync26(file)) continue;
3507
+ if (existsSync27(file)) continue;
3512
3508
  await mkdir7(path11.dirname(file), { recursive: true });
3513
3509
  await writeFile12(
3514
3510
  file,
@@ -3554,7 +3550,7 @@ function gitFileDiff(root, file, sinceDays) {
3554
3550
  }
3555
3551
 
3556
3552
  // src/tools/mem-conflict-candidates.ts
3557
- import { existsSync as existsSync27 } from "fs";
3553
+ import { existsSync as existsSync28 } from "fs";
3558
3554
  import {
3559
3555
  findLexicalConflictPairs,
3560
3556
  findTopicStatusConflictPairs,
@@ -3572,7 +3568,7 @@ var MemConflictCandidatesInputSchema = {
3572
3568
  )
3573
3569
  };
3574
3570
  async function memConflictCandidates(input, ctx) {
3575
- if (!existsSync27(ctx.paths.memoriesDir)) {
3571
+ if (!existsSync28(ctx.paths.memoriesDir)) {
3576
3572
  return {
3577
3573
  pairs: [],
3578
3574
  topic_status_pairs: [],
@@ -3624,7 +3620,7 @@ async function memSuggestTopic(input, _ctx) {
3624
3620
  }
3625
3621
 
3626
3622
  // src/tools/mem-timeline.ts
3627
- import { existsSync as existsSync28 } from "fs";
3623
+ import { existsSync as existsSync29 } from "fs";
3628
3624
  import { collectTimelineEntries, loadMemoriesFromDir as loadMemoriesFromDir22 } from "@hiveai/core";
3629
3625
  import { z as z33 } from "zod";
3630
3626
  var MemTimelineInputSchema = {
@@ -3633,7 +3629,7 @@ var MemTimelineInputSchema = {
3633
3629
  limit: z33.number().int().positive().max(100).default(30).describe("Max timeline entries returned")
3634
3630
  };
3635
3631
  async function memTimeline(input, ctx) {
3636
- if (!existsSync28(ctx.paths.memoriesDir)) {
3632
+ if (!existsSync29(ctx.paths.memoriesDir)) {
3637
3633
  return { entries: [], total: 0, notice: "No .ai/memories directory." };
3638
3634
  }
3639
3635
  const all = await loadMemoriesFromDir22(ctx.paths.memoriesDir);
@@ -3923,9 +3919,9 @@ When done, respond with: "Imported N memories: [list of IDs]" or "Nothing action
3923
3919
  }
3924
3920
 
3925
3921
  // src/server.ts
3926
- import { loadConfigSync } from "@hiveai/core";
3922
+ import { hasRecentBriefingMarker, loadConfigSync } from "@hiveai/core";
3927
3923
  var SERVER_NAME = "haive";
3928
- var SERVER_VERSION = "0.10.4";
3924
+ var SERVER_VERSION = "0.10.8";
3929
3925
  function jsonResult(data) {
3930
3926
  return {
3931
3927
  content: [
@@ -4029,11 +4025,16 @@ function createHaiveServer(options = {}) {
4029
4025
  return await handler(input);
4030
4026
  }
4031
4027
  if (requireBriefingFirst && MUTATING_TOOLS.has(name) && !briefingLoaded) {
4032
- return jsonResult({
4033
- error: "haive_briefing_required",
4034
- message: "This hAIve project requires get_briefing or mem_relevant_to before state-changing hAIve tools. Call get_briefing({ task: '...' }) first.",
4035
- tool: name
4036
- });
4028
+ const hasDiskMarker = await hasRecentBriefingMarker(context.paths).catch(() => false);
4029
+ if (hasDiskMarker) {
4030
+ briefingLoaded = true;
4031
+ } else {
4032
+ return jsonResult({
4033
+ error: "haive_briefing_required",
4034
+ message: "This hAIve project requires get_briefing or mem_relevant_to before state-changing hAIve tools. Call get_briefing({ task: '...' }) first.",
4035
+ tool: name
4036
+ });
4037
+ }
4037
4038
  }
4038
4039
  return await handler(input);
4039
4040
  }