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