bilmem 0.1.0 → 0.2.0

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/mcp/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/mcp/server.ts
2
2
  import { McpApp, z } from "mcponce";
3
- import * as path8 from "path";
3
+ import * as path9 from "path";
4
4
 
5
5
  // src/storage/canonical.ts
6
6
  import * as fs3 from "fs/promises";
@@ -1180,6 +1180,7 @@ function formatWhyHistory(knowledge) {
1180
1180
  // src/core/resolver.ts
1181
1181
  var RELATIVE_COSTS = {
1182
1182
  USE_KNOWLEDGE: 1,
1183
+ REVALIDATE: 2,
1183
1184
  INSPECT_TEST: 2,
1184
1185
  INSPECT_FILE: 2,
1185
1186
  INSPECT_SYMBOL: 2.5,
@@ -1520,6 +1521,634 @@ async function resolveEpistemicQuery(options, storage, retriever) {
1520
1521
  };
1521
1522
  }
1522
1523
 
1524
+ // src/core/preflight.ts
1525
+ import { execFile } from "child_process";
1526
+ import { promisify } from "util";
1527
+ import * as path4 from "path";
1528
+ import * as fs4 from "fs/promises";
1529
+ import YAML2 from "yaml";
1530
+
1531
+ // src/core/dependencies.ts
1532
+ function normalizeSourceKey(source) {
1533
+ return source.replace(/^(file|code|test|doc):/, "").trim().toLowerCase();
1534
+ }
1535
+ function checkKnowledgeStaleness(knowledge, changedSources) {
1536
+ const normChanged = new Set(changedSources.map(normalizeSourceKey));
1537
+ const allDeps = [
1538
+ ...(knowledge.depends_on ?? []).map(normalizeSourceKey),
1539
+ ...knowledge.evidence.map(normalizeSourceKey)
1540
+ ];
1541
+ const changed = [];
1542
+ const unchanged = [];
1543
+ for (const dep of allDeps) {
1544
+ if (!dep) continue;
1545
+ const isChanged = normChanged.has(dep) || Array.from(normChanged).some((c) => dep.includes(c) || c.includes(dep));
1546
+ if (isChanged) {
1547
+ changed.push(dep);
1548
+ } else {
1549
+ unchanged.push(dep);
1550
+ }
1551
+ }
1552
+ if (changed.length > 0) {
1553
+ const reason = `Supporting dependency changed: ${changed.join(", ")}. Unchanged evidence: ${unchanged.join(", ") || "none"}`;
1554
+ return {
1555
+ knowledgeId: knowledge.id,
1556
+ affected: true,
1557
+ status: "needs_revalidation",
1558
+ changedSources: changed,
1559
+ unchangedSources: unchanged,
1560
+ reason
1561
+ };
1562
+ }
1563
+ return {
1564
+ knowledgeId: knowledge.id,
1565
+ affected: false,
1566
+ status: knowledge.status,
1567
+ changedSources: [],
1568
+ unchangedSources: allDeps
1569
+ };
1570
+ }
1571
+ function markStaleKnowledge(knowledgeList, changedSources) {
1572
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1573
+ let affectedCount = 0;
1574
+ const updatedKnowledge = [];
1575
+ for (const k of knowledgeList) {
1576
+ if (k.status === "superseded" || k.status === "invalid" || k.status === "invalidated") {
1577
+ updatedKnowledge.push(k);
1578
+ continue;
1579
+ }
1580
+ const res = checkKnowledgeStaleness(k, changedSources);
1581
+ if (res.affected) {
1582
+ affectedCount++;
1583
+ const history = k.history ?? [];
1584
+ const updated = {
1585
+ ...k,
1586
+ status: "needs_revalidation",
1587
+ updatedAt: now,
1588
+ history: [
1589
+ ...history,
1590
+ {
1591
+ timestamp: now,
1592
+ status: "needs_revalidation",
1593
+ action: "source_changed",
1594
+ reason: res.reason
1595
+ }
1596
+ ]
1597
+ };
1598
+ updatedKnowledge.push(updated);
1599
+ } else {
1600
+ updatedKnowledge.push(k);
1601
+ }
1602
+ }
1603
+ return { updatedKnowledge, affectedCount };
1604
+ }
1605
+ function checkKnowledgeListStaleness(knowledgeList, changedSources) {
1606
+ const { updatedKnowledge, affectedCount } = markStaleKnowledge(knowledgeList, changedSources);
1607
+ const affectedKnowledge = updatedKnowledge.filter((k) => k.status === "needs_revalidation");
1608
+ return { affectedKnowledge, updatedKnowledge, affectedCount };
1609
+ }
1610
+
1611
+ // src/core/preflight.ts
1612
+ var execFileAsync = promisify(execFile);
1613
+ var STOP_WORDS = /* @__PURE__ */ new Set([
1614
+ "add",
1615
+ "implement",
1616
+ "create",
1617
+ "support",
1618
+ "with",
1619
+ "the",
1620
+ "a",
1621
+ "an",
1622
+ "for",
1623
+ "in",
1624
+ "to",
1625
+ "and",
1626
+ "of",
1627
+ "on",
1628
+ "from",
1629
+ "using",
1630
+ "via",
1631
+ "into",
1632
+ "by",
1633
+ "our",
1634
+ "new",
1635
+ "use",
1636
+ "make",
1637
+ "allow",
1638
+ "enable",
1639
+ "provide",
1640
+ "setup",
1641
+ "set"
1642
+ ]);
1643
+ var DOMAIN_KEYWORDS = {
1644
+ authentication: ["auth", "oauth", "login", "logout", "session", "jwt", "token", "credential", "sso", "password"],
1645
+ session: ["session", "cookie", "cookies", "expiry", "refresh", "token"],
1646
+ "user-identity": ["user", "account", "profile", "identity", "member", "role", "permission"],
1647
+ database: ["db", "database", "sql", "postgres", "sqlite", "migration", "schema", "table", "query"],
1648
+ storage: ["storage", "s3", "bucket", "file", "upload", "disk", "blob"],
1649
+ api: ["api", "endpoint", "http", "rest", "route", "controller", "middleware", "graphql"],
1650
+ caching: ["cache", "redis", "memcached", "ttl", "invalidation"],
1651
+ billing: ["payment", "stripe", "checkout", "billing", "subscription", "invoice", "credit"],
1652
+ "data-processing": ["import", "export", "csv", "bulk", "batch", "stream", "pipeline"]
1653
+ };
1654
+ function tokenizeGoal(goal) {
1655
+ return goal.toLowerCase().replace(/[^a-z0-9_-]/g, " ").split(/\s+/).filter((t) => t.length > 2 && !STOP_WORDS.has(t));
1656
+ }
1657
+ function inferLikelyDomains(tokens, knowledgeTags) {
1658
+ const domains = /* @__PURE__ */ new Set();
1659
+ for (const token of tokens) {
1660
+ for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) {
1661
+ if (keywords.includes(token) || token.includes(domain)) {
1662
+ domains.add(domain);
1663
+ }
1664
+ }
1665
+ }
1666
+ for (const tag of knowledgeTags) {
1667
+ const norm = tag.toLowerCase().trim();
1668
+ if (DOMAIN_KEYWORDS[norm]) {
1669
+ domains.add(norm);
1670
+ }
1671
+ }
1672
+ return Array.from(domains);
1673
+ }
1674
+ function classifyUnknown(unknown, goalTokens, domainCues) {
1675
+ const text = `${unknown.id} ${unknown.statement || ""} ${unknown.question || unknown.next?.question || ""}`.toLowerCase();
1676
+ if (unknown.blocking === true) {
1677
+ const isDecision2 = /\b(should|whether|how to handle|what happens when|policy|strategy|linking|collision|merge|choice)\b/i.test(text);
1678
+ return {
1679
+ classification: "BLOCKING",
1680
+ isDecision: isDecision2 || true,
1681
+ reason: unknown.reason || "Blocking unknown identified for this goal"
1682
+ };
1683
+ }
1684
+ if (unknown.blocking === false) {
1685
+ const isDecision2 = /\b(should|whether|how to handle|what happens when|policy|strategy|choice)\b/i.test(text);
1686
+ return {
1687
+ classification: "NON_BLOCKING",
1688
+ isDecision: isDecision2,
1689
+ reason: "Operational or instrumentation detail not blocking core functionality"
1690
+ };
1691
+ }
1692
+ const isNonBlocking = /\b(analytics|telemetry|metric|event name|logging level|log format|button color|styling|theme)\b/i.test(text);
1693
+ if (isNonBlocking) {
1694
+ return {
1695
+ classification: "NON_BLOCKING",
1696
+ isDecision: /\b(should|which name|choice)\b/i.test(text),
1697
+ reason: "Operational or instrumentation detail not blocking core functionality"
1698
+ };
1699
+ }
1700
+ const isDecision = /\b(should|whether|how to handle|what happens when|policy|strategy|linking|collision|merge)\b/i.test(text);
1701
+ const isBlockingCandidate = /\b(link|linking|collision|duplicate|merge|security|permission|storage-engine|protocol|auth-flow|credentials)\b/i.test(text);
1702
+ const overlapsGoal = goalTokens.some((t) => text.includes(t)) || domainCues.some((d) => text.includes(d));
1703
+ if (isBlockingCandidate && (overlapsGoal || unknown.reason === "UNRESOLVABLE_LOCALLY")) {
1704
+ return {
1705
+ classification: "BLOCKING",
1706
+ isDecision: true,
1707
+ reason: "Unresolved architectural or behavioral decision critical to goal implementation"
1708
+ };
1709
+ }
1710
+ return {
1711
+ classification: "IMPORTANT",
1712
+ isDecision,
1713
+ reason: isDecision ? "Product or behavior decision requiring attention" : "Important implementation detail"
1714
+ };
1715
+ }
1716
+ function prioritizeQuestions(rawQuestions, blockingUnknowns) {
1717
+ const consolidated = /* @__PURE__ */ new Map();
1718
+ for (const rq of rawQuestions) {
1719
+ const qText = rq.question.trim();
1720
+ if (!qText) continue;
1721
+ const topic = rq.targetTopic || qText.toLowerCase().replace(/[^a-z0-9]/g, " ").slice(0, 30);
1722
+ let expectedResolutionValue = 1;
1723
+ if (rq.isBlocking) expectedResolutionValue += 2;
1724
+ if (rq.outcomeScore && rq.outcomeScore > 0) expectedResolutionValue += 1;
1725
+ if (rq.outcomeScore && rq.outcomeScore < 0) expectedResolutionValue -= 0.5;
1726
+ const interactionCost = 1;
1727
+ const priorityScore = Math.max(0.1, expectedResolutionValue) / interactionCost;
1728
+ const existing = consolidated.get(topic);
1729
+ if (!existing || priorityScore > (existing.priorityScore ?? 0)) {
1730
+ consolidated.set(topic, {
1731
+ question: qText,
1732
+ reason: rq.reason,
1733
+ targetTopic: rq.targetTopic,
1734
+ expectedResolutionValue,
1735
+ interactionCost,
1736
+ priorityScore
1737
+ });
1738
+ }
1739
+ }
1740
+ return Array.from(consolidated.values()).sort((a, b) => (b.priorityScore ?? 0) - (a.priorityScore ?? 0));
1741
+ }
1742
+ async function executePreflight(options, storage, retriever, index) {
1743
+ const goal = options.goal.trim();
1744
+ const explicitPositive = (options.positive ?? []).map((p) => p.trim().toLowerCase());
1745
+ const negative = (options.negative ?? []).map((n) => n.trim().toLowerCase());
1746
+ const scope = options.scope ?? "project";
1747
+ const goalTokens = tokenizeGoal(goal);
1748
+ const allKnowledge = await storage.getAllKnowledge();
1749
+ const allTags = allKnowledge.flatMap((k) => k.tags ?? []);
1750
+ const likelyDomains = inferLikelyDomains(goalTokens, allTags);
1751
+ const contextualPositive = Array.from(/* @__PURE__ */ new Set([...explicitPositive, ...likelyDomains, ...goalTokens]));
1752
+ let candidatePool = [];
1753
+ if (retriever) {
1754
+ const jevCandidates = await retriever.lookup({
1755
+ query: goal,
1756
+ positive: contextualPositive,
1757
+ negative,
1758
+ scope,
1759
+ limit: options.maxCandidates ?? 25,
1760
+ includeRejected: true
1761
+ });
1762
+ for (const r of jevCandidates) {
1763
+ if (r.knowledge) candidatePool.push(r.knowledge);
1764
+ }
1765
+ }
1766
+ if (candidatePool.length === 0) {
1767
+ candidatePool = allKnowledge.filter((k) => k.status !== "invalid" && k.status !== "superseded");
1768
+ } else {
1769
+ for (const k of allKnowledge) {
1770
+ if (!candidatePool.some((c) => c.id === k.id) && k.status !== "invalid" && k.status !== "superseded") {
1771
+ const matchesToken = goalTokens.some((t) => k.id.toLowerCase().includes(t) || k.statement.toLowerCase().includes(t));
1772
+ const matchesCues = k.positive.some((p) => contextualPositive.includes(p));
1773
+ if (matchesToken || matchesCues) {
1774
+ candidatePool.push(k);
1775
+ }
1776
+ }
1777
+ }
1778
+ }
1779
+ const jevResults = rerankCandidates(candidatePool, {
1780
+ query: goal,
1781
+ positive: contextualPositive,
1782
+ negative,
1783
+ scope
1784
+ });
1785
+ const matchingItems = [];
1786
+ const rejectedItems = [];
1787
+ for (const res of jevResults) {
1788
+ if (res.status === "REJECT") {
1789
+ const reason = res.explanation.negativeConflicts.length > 0 ? `Inapplicable: ${res.explanation.negativeConflicts.join(", ")} conflicts with current context` : res.explanation.reasons[0] || "Context mismatch";
1790
+ rejectedItems.push({ id: res.knowledge.id, reason });
1791
+ } else {
1792
+ matchingItems.push(res.knowledge);
1793
+ }
1794
+ }
1795
+ if (options.deep) {
1796
+ try {
1797
+ const { stdout: diffOutput } = await execFileAsync(
1798
+ "git",
1799
+ ["diff", "--name-only", "HEAD~1"],
1800
+ { cwd: storage.getBaseDir() ? path4.dirname(storage.getBaseDir()) : process.cwd() }
1801
+ );
1802
+ const changedFiles = diffOutput.split("\n").map((f) => f.trim()).filter(Boolean);
1803
+ if (changedFiles.length > 0) {
1804
+ const staleness = checkKnowledgeListStaleness(matchingItems, changedFiles);
1805
+ for (const affected of staleness.affectedKnowledge) {
1806
+ const matchIdx = matchingItems.findIndex((m) => m.id === affected.id);
1807
+ if (matchIdx >= 0) {
1808
+ matchingItems[matchIdx] = affected;
1809
+ }
1810
+ }
1811
+ }
1812
+ } catch {
1813
+ }
1814
+ }
1815
+ const constraints = [];
1816
+ const lessons = [];
1817
+ const pastFailures = [];
1818
+ const stale = [];
1819
+ const known = [];
1820
+ let conflictedCount = 0;
1821
+ const CONSTRAINT_PATTERN = /\b(must|must not|never|only|cannot|require|required|boundary|strictly|prohibited|forbidden)\b/i;
1822
+ const FAILURE_PATTERN = /(fail|bug|error|crash|duplicate write|race condition|pressure|leak|revert|timeout)/i;
1823
+ const rootDir = storage.getBaseDir() ? path4.dirname(storage.getBaseDir()) : process.cwd();
1824
+ for (const k of matchingItems) {
1825
+ const matchesContradiction = k.status === "contradicted" || k.contradicts && k.contradicts.some((cid) => matchingItems.some((m) => m.id === cid));
1826
+ if (matchesContradiction) {
1827
+ conflictedCount++;
1828
+ }
1829
+ let isStale = k.status === "needs_revalidation" || k.status === "stale";
1830
+ let staleReason = k.history?.[k.history.length - 1]?.reason || "Supporting source changed after last validation";
1831
+ const changedDeps = [];
1832
+ const candidateDeps = [...k.depends_on ?? [], ...k.evidence ?? []];
1833
+ const timestampStr = k.updated_at ?? k.updatedAt ?? k.createdAt;
1834
+ const knowledgeTime = new Date(timestampStr).getTime();
1835
+ for (const dep of candidateDeps) {
1836
+ const cleanPath = dep.replace(/^(file|code|test|doc):/, "").trim();
1837
+ if (!cleanPath) continue;
1838
+ const absPath = path4.isAbsolute(cleanPath) ? cleanPath : path4.resolve(rootDir, cleanPath);
1839
+ try {
1840
+ const stat4 = await fs4.stat(absPath);
1841
+ if (stat4.mtimeMs > knowledgeTime + 500) {
1842
+ isStale = true;
1843
+ changedDeps.push(cleanPath);
1844
+ staleReason = `Supporting source changed after last validation (${cleanPath})`;
1845
+ }
1846
+ } catch {
1847
+ }
1848
+ }
1849
+ if (isStale) {
1850
+ stale.push({
1851
+ id: k.id,
1852
+ statement: k.statement,
1853
+ reason: staleReason,
1854
+ changedSources: changedDeps.length > 0 ? changedDeps : k.depends_on
1855
+ });
1856
+ continue;
1857
+ }
1858
+ const isConstraint = k.type === "rule" || k.type === "decision" || k.type === "convention" || CONSTRAINT_PATTERN.test(k.statement);
1859
+ if (isConstraint) {
1860
+ constraints.push({
1861
+ id: k.id,
1862
+ statement: k.statement,
1863
+ type: k.type,
1864
+ confidence: k.confidence,
1865
+ confidenceTier: k.confidenceTier,
1866
+ source: k.evidence?.[0]
1867
+ });
1868
+ continue;
1869
+ }
1870
+ const isFailure = k.type === "failure" || k.statement.startsWith("[FAILURE]") || k.statement.startsWith("[REVERT]") || FAILURE_PATTERN.test(k.statement) || k.metrics.failed_outcomes > 0;
1871
+ if (isFailure) {
1872
+ pastFailures.push({
1873
+ id: k.id,
1874
+ statement: k.statement,
1875
+ context: k.context || {
1876
+ positive: k.positive,
1877
+ negative: k.negative
1878
+ },
1879
+ failure: k.statement,
1880
+ lesson: k.lesson || k.reasoning || k.resolves?.[0],
1881
+ evidence: k.evidence
1882
+ });
1883
+ continue;
1884
+ }
1885
+ const isLesson = k.type === "lesson" && (k.id.includes("lesson") || k.tags?.includes("lesson") || k.statement.toLowerCase().includes("lesson")) || k.statement.toLowerCase().startsWith("lesson:") || k.statement.toLowerCase().includes("lesson:");
1886
+ if (isLesson) {
1887
+ lessons.push({
1888
+ id: k.id,
1889
+ statement: k.statement,
1890
+ confidence: k.confidence,
1891
+ domain: k.tags?.[0],
1892
+ evidence: k.evidence
1893
+ });
1894
+ continue;
1895
+ }
1896
+ if (k.status === "active" || k.stage === "active" || k.stage === "probation" || !k.status) {
1897
+ known.push({
1898
+ id: k.id,
1899
+ statement: k.statement,
1900
+ confidence: k.confidence,
1901
+ confidenceTier: k.confidenceTier,
1902
+ stage: k.stage
1903
+ });
1904
+ }
1905
+ }
1906
+ const unknownsFromStorage = await storage.getAllUnknowns();
1907
+ const unknownKnowledge = allKnowledge.filter((k) => k.type === "unknown");
1908
+ const unifiedUnknowns = [];
1909
+ const seenUnknownIds = /* @__PURE__ */ new Set();
1910
+ for (const u of unknownsFromStorage) {
1911
+ seenUnknownIds.add(u.id);
1912
+ unifiedUnknowns.push({
1913
+ id: u.id,
1914
+ statement: u.statement || u.id,
1915
+ reason: u.reason,
1916
+ question: u.question || u.next?.question,
1917
+ next: u.next,
1918
+ blocking: u.blocking,
1919
+ positive: u.positive,
1920
+ negative: u.negative,
1921
+ checked: u.checked
1922
+ });
1923
+ }
1924
+ for (const k of unknownKnowledge) {
1925
+ if (!seenUnknownIds.has(k.id)) {
1926
+ seenUnknownIds.add(k.id);
1927
+ unifiedUnknowns.push({
1928
+ id: k.id,
1929
+ statement: k.statement,
1930
+ reason: k.history?.[0]?.reason,
1931
+ question: k.question || (k.statement.includes("?") ? k.statement : void 0),
1932
+ next: {
1933
+ type: "ask",
1934
+ target: k.id,
1935
+ question: k.question || k.statement
1936
+ },
1937
+ blocking: k.blocking,
1938
+ positive: k.positive,
1939
+ negative: k.negative,
1940
+ checked: k.depends_on || k.evidence
1941
+ });
1942
+ }
1943
+ }
1944
+ const unknowns = [];
1945
+ const missing = [];
1946
+ const rawQuestions = [];
1947
+ for (const u of unifiedUnknowns) {
1948
+ const text = `${u.id} ${u.statement || ""} ${u.question || ""}`.toLowerCase();
1949
+ const overlaps = goalTokens.some((t) => text.includes(t)) || likelyDomains.some((d) => text.includes(d)) || u.positive && u.positive.some((p) => contextualPositive.includes(p));
1950
+ if (u.negative && u.negative.some((n) => contextualPositive.includes(n))) {
1951
+ continue;
1952
+ }
1953
+ if (overlaps) {
1954
+ const { classification, isDecision, reason } = classifyUnknown(u, goalTokens, likelyDomains);
1955
+ const isBlocking = classification === "BLOCKING";
1956
+ unknowns.push({
1957
+ id: u.id,
1958
+ statement: u.statement,
1959
+ question: u.question || u.statement,
1960
+ classification,
1961
+ blocking: isBlocking,
1962
+ reason
1963
+ });
1964
+ if (isDecision) {
1965
+ missing.push({
1966
+ type: "decision",
1967
+ subject: u.id,
1968
+ question: u.question || u.statement,
1969
+ reason
1970
+ });
1971
+ } else {
1972
+ missing.push({
1973
+ type: "evidence",
1974
+ subject: u.id,
1975
+ question: u.question || u.statement,
1976
+ reason
1977
+ });
1978
+ }
1979
+ const qText = u.question || (u.statement && u.statement.includes("?") ? u.statement : void 0);
1980
+ if (qText) {
1981
+ rawQuestions.push({
1982
+ question: qText,
1983
+ reason: `Unknown: ${u.id} (${reason})`,
1984
+ targetTopic: u.id,
1985
+ isBlocking
1986
+ });
1987
+ }
1988
+ }
1989
+ }
1990
+ const questionKnowledge = allKnowledge.filter((k) => k.type === "question");
1991
+ for (const qk of questionKnowledge) {
1992
+ const matchesCues = qk.positive.some((p) => contextualPositive.includes(p));
1993
+ const conflicts = qk.negative.some((n) => negative.includes(n));
1994
+ if (matchesCues && !conflicts && qk.question) {
1995
+ rawQuestions.push({
1996
+ question: qk.question,
1997
+ reason: `Learned question for [${qk.positive.join(", ")}] uncertainty`,
1998
+ targetTopic: qk.resolves?.[0] || qk.id,
1999
+ outcomeScore: (qk.metrics?.successful_outcomes ?? 0) - (qk.metrics?.failed_outcomes ?? 0)
2000
+ });
2001
+ }
2002
+ }
2003
+ const questions = prioritizeQuestions(rawQuestions, unknowns);
2004
+ const receipts = await storage.getAllReceipts();
2005
+ const alreadyInvestigated = [];
2006
+ const investigatedSources = /* @__PURE__ */ new Set();
2007
+ for (const r of receipts) {
2008
+ const qTokens = r.question.toLowerCase().split(/\s+/).filter(Boolean);
2009
+ const overlapsGoal = qTokens.some((t) => goalTokens.includes(t));
2010
+ const overlapsCues = r.positive && r.positive.some((p) => contextualPositive.includes(p));
2011
+ if (overlapsGoal || overlapsCues) {
2012
+ for (const checked of r.checked) {
2013
+ investigatedSources.add(checked.toLowerCase());
2014
+ alreadyInvestigated.push({
2015
+ source: checked,
2016
+ result: r.findings[0] || r.rejected[0]?.reason || "Did not establish requested behavior",
2017
+ nextSuggestion: "ASK_USER"
2018
+ });
2019
+ }
2020
+ }
2021
+ }
2022
+ let readiness = "READY";
2023
+ const totalRelevantKnowledge = known.length + constraints.length + lessons.length + pastFailures.length + stale.length;
2024
+ const hasBlockingUnknown = unknowns.some((u) => u.blocking);
2025
+ const hasMissingDecision = missing.some((m) => m.type === "decision");
2026
+ if (totalRelevantKnowledge === 0 && unknowns.length === 0) {
2027
+ readiness = "INSUFFICIENT_KNOWLEDGE";
2028
+ } else if (conflictedCount > 0) {
2029
+ readiness = "CONFLICTED";
2030
+ } else if (hasBlockingUnknown || hasMissingDecision) {
2031
+ readiness = "NEEDS_CLARIFICATION";
2032
+ } else if (stale.length > 0) {
2033
+ readiness = "NEEDS_REVALIDATION";
2034
+ } else {
2035
+ readiness = "READY";
2036
+ }
2037
+ const next = [];
2038
+ for (const m of missing) {
2039
+ if (m.type === "decision") {
2040
+ next.push({
2041
+ type: "ASK_USER",
2042
+ target: m.subject,
2043
+ reason: m.question || m.reason,
2044
+ relativeCost: 1,
2045
+ expectedInfoGain: 0.95
2046
+ });
2047
+ }
2048
+ }
2049
+ for (const s of stale) {
2050
+ next.push({
2051
+ type: "REVALIDATE",
2052
+ target: s.id,
2053
+ reason: s.reason,
2054
+ relativeCost: 2,
2055
+ expectedInfoGain: 0.85
2056
+ });
2057
+ }
2058
+ for (const m of missing) {
2059
+ if (m.type === "evidence") {
2060
+ const wasChecked = investigatedSources.has(m.subject.toLowerCase()) || alreadyInvestigated.length > 0;
2061
+ if (wasChecked) {
2062
+ if (!next.some((n) => n.type === "ASK_USER")) {
2063
+ next.push({
2064
+ type: "ASK_USER",
2065
+ target: m.subject,
2066
+ reason: `Already investigated (${Array.from(investigatedSources).join(", ")}); did not establish requested behavior. Clarification required.`,
2067
+ relativeCost: 1,
2068
+ expectedInfoGain: 0.9
2069
+ });
2070
+ }
2071
+ } else {
2072
+ const isFilePath = m.subject.includes("/") || m.subject.includes(".");
2073
+ next.push({
2074
+ type: isFilePath ? "INSPECT_FILE" : "SEARCH_REPOSITORY",
2075
+ target: m.subject,
2076
+ reason: m.reason,
2077
+ relativeCost: isFilePath ? 2 : 2.5,
2078
+ expectedInfoGain: 0.8
2079
+ });
2080
+ }
2081
+ }
2082
+ }
2083
+ if (alreadyInvestigated.length > 0 && !next.some((n) => n.type === "ASK_USER")) {
2084
+ next.push({
2085
+ type: "ASK_USER",
2086
+ target: alreadyInvestigated[0].source,
2087
+ reason: `Already investigated ${alreadyInvestigated[0].source}; clarification required`,
2088
+ relativeCost: 1,
2089
+ expectedInfoGain: 0.9
2090
+ });
2091
+ }
2092
+ if (next.length === 0 && known.length > 0) {
2093
+ next.push({
2094
+ type: "USE_KNOWLEDGE",
2095
+ target: known[0].id,
2096
+ reason: "Proceed with implementation using verified knowledge and constraints",
2097
+ relativeCost: 1,
2098
+ expectedInfoGain: 1
2099
+ });
2100
+ }
2101
+ const knowledgeImpact = {
2102
+ relevantKnowledge: totalRelevantKnowledge,
2103
+ validated: known.filter((k) => k.confidenceTier === "high").length + constraints.length,
2104
+ needsRevalidation: stale.length,
2105
+ conflicted: conflictedCount,
2106
+ blockingUnknowns: unknowns.filter((u) => u.blocking).length,
2107
+ relevantFailures: pastFailures.length
2108
+ };
2109
+ const similarWork = [];
2110
+ for (const l of lessons) {
2111
+ similarWork.push({
2112
+ topic: l.domain || l.id,
2113
+ lesson: l.statement
2114
+ });
2115
+ }
2116
+ for (const f of pastFailures) {
2117
+ similarWork.push({
2118
+ topic: f.id,
2119
+ failure: f.statement
2120
+ });
2121
+ }
2122
+ await storage.appendMetricEvent({
2123
+ type: "preflight",
2124
+ goal,
2125
+ readiness,
2126
+ blockingUnknownsCount: knowledgeImpact.blockingUnknowns,
2127
+ staleKnowledgeCount: stale.length,
2128
+ pastFailuresCount: pastFailures.length,
2129
+ questionsCount: questions.length,
2130
+ repeatedChecksAvoidedCount: alreadyInvestigated.length,
2131
+ timestamp: Date.now()
2132
+ });
2133
+ return {
2134
+ goal,
2135
+ readiness,
2136
+ known,
2137
+ constraints,
2138
+ lessons,
2139
+ past_failures: pastFailures,
2140
+ unknowns,
2141
+ stale,
2142
+ missing,
2143
+ questions,
2144
+ likely_domains: likelyDomains,
2145
+ similar_work: similarWork.length > 0 ? similarWork.slice(0, 5) : void 0,
2146
+ already_investigated: alreadyInvestigated.length > 0 ? alreadyInvestigated : void 0,
2147
+ knowledge_impact: knowledgeImpact,
2148
+ next
2149
+ };
2150
+ }
2151
+
1523
2152
  // src/core/knowledge.ts
1524
2153
  import { createHash } from "crypto";
1525
2154
 
@@ -1666,7 +2295,7 @@ function createKnowledge(params) {
1666
2295
  type: params.type ?? "lesson",
1667
2296
  status,
1668
2297
  stage: initialStage,
1669
- statement: params.statement.trim(),
2298
+ statement: (params.statement ?? params.question ?? params.id).trim(),
1670
2299
  positive: Array.from(new Set(params.positive?.map((p) => p.trim().toLowerCase()) ?? [])),
1671
2300
  negative: Array.from(new Set(params.negative?.map((n) => n.trim().toLowerCase()) ?? [])),
1672
2301
  relations: Array.from(new Set(params.relations?.map((r) => r.trim()) ?? [])),
@@ -1696,6 +2325,11 @@ function createKnowledge(params) {
1696
2325
  },
1697
2326
  history,
1698
2327
  value: 0,
2328
+ lesson: params.lesson,
2329
+ failure_details: params.failure_details,
2330
+ context: params.context,
2331
+ contradicts: params.contradicts ? Array.from(new Set(params.contradicts)) : void 0,
2332
+ blocking: params.blocking,
1699
2333
  createdAt: now,
1700
2334
  updatedAt: now
1701
2335
  };
@@ -2474,7 +3108,7 @@ function processUserCorrection(params) {
2474
3108
  }
2475
3109
 
2476
3110
  // src/scanner/engine.ts
2477
- import * as path7 from "path";
3111
+ import * as path8 from "path";
2478
3112
 
2479
3113
  // src/core/relations.ts
2480
3114
  var EvidenceGraph = class _EvidenceGraph {
@@ -2565,8 +3199,8 @@ var EvidenceGraph = class _EvidenceGraph {
2565
3199
  };
2566
3200
 
2567
3201
  // src/scanner/filesystem.ts
2568
- import * as fs4 from "fs/promises";
2569
- import * as path4 from "path";
3202
+ import * as fs5 from "fs/promises";
3203
+ import * as path5 from "path";
2570
3204
  var DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([
2571
3205
  ".git",
2572
3206
  ".bilmem",
@@ -2587,12 +3221,12 @@ var DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([
2587
3221
  "temp"
2588
3222
  ]);
2589
3223
  function normalizeRelPath(rel) {
2590
- return rel.split(path4.sep).join("/");
3224
+ return rel.split(path5.sep).join("/");
2591
3225
  }
2592
3226
  async function loadGitignorePatterns(rootDir) {
2593
3227
  try {
2594
- const gitignorePath = path4.join(rootDir, ".gitignore");
2595
- const content = await fs4.readFile(gitignorePath, "utf-8");
3228
+ const gitignorePath = path5.join(rootDir, ".gitignore");
3229
+ const content = await fs5.readFile(gitignorePath, "utf-8");
2596
3230
  const regexes = [];
2597
3231
  for (const rawLine of content.split("\n")) {
2598
3232
  const line = rawLine.trim();
@@ -2640,7 +3274,7 @@ async function walkDirectory(dir, optionsOrRoot, legacyFileList) {
2640
3274
  return;
2641
3275
  }
2642
3276
  try {
2643
- const real = await fs4.realpath(currentDir);
3277
+ const real = await fs5.realpath(currentDir);
2644
3278
  if (visitedRealPaths.has(real)) {
2645
3279
  return;
2646
3280
  }
@@ -2650,14 +3284,14 @@ async function walkDirectory(dir, optionsOrRoot, legacyFileList) {
2650
3284
  }
2651
3285
  let entries;
2652
3286
  try {
2653
- entries = await fs4.readdir(currentDir, { withFileTypes: true });
3287
+ entries = await fs5.readdir(currentDir, { withFileTypes: true });
2654
3288
  } catch {
2655
3289
  return;
2656
3290
  }
2657
3291
  for (const entry of entries) {
2658
3292
  if (fileList.length >= maxFiles) break;
2659
- const fullPath = path4.join(currentDir, entry.name);
2660
- const rawRelPath = path4.relative(root, fullPath);
3293
+ const fullPath = path5.join(currentDir, entry.name);
3294
+ const rawRelPath = path5.relative(root, fullPath);
2661
3295
  const relPath = normalizeRelPath(rawRelPath);
2662
3296
  const lowerName = entry.name.toLowerCase();
2663
3297
  if (gitignoreRules.some((rule) => rule.test(relPath))) {
@@ -2673,16 +3307,16 @@ async function walkDirectory(dir, optionsOrRoot, legacyFileList) {
2673
3307
  if (!includeHidden && entry.name.startsWith(".") && lowerName !== ".env.example") {
2674
3308
  continue;
2675
3309
  }
2676
- const ext = path4.extname(entry.name).toLowerCase();
3310
+ const ext = path5.extname(entry.name).toLowerCase();
2677
3311
  try {
2678
- const stat3 = await fs4.stat(fullPath);
3312
+ const stat4 = await fs5.stat(fullPath);
2679
3313
  const fileInfo = {
2680
3314
  path: fullPath,
2681
3315
  relativePath: relPath,
2682
3316
  name: entry.name,
2683
3317
  ext,
2684
- size: stat3.size,
2685
- mtimeMs: stat3.mtimeMs,
3318
+ size: stat4.size,
3319
+ mtimeMs: stat4.mtimeMs,
2686
3320
  isDirectory: false
2687
3321
  };
2688
3322
  if (!options.filter || options.filter(fileInfo)) {
@@ -2725,9 +3359,9 @@ var ProjectContext = class {
2725
3359
  if (this.textCache.has(key)) {
2726
3360
  return this.textCache.get(key);
2727
3361
  }
2728
- const fullPath = path4.resolve(this.rootDir, relativePath);
3362
+ const fullPath = path5.resolve(this.rootDir, relativePath);
2729
3363
  try {
2730
- const handle = await fs4.open(fullPath, "r");
3364
+ const handle = await fs5.open(fullPath, "r");
2731
3365
  const buffer = Buffer.alloc(maxBytes);
2732
3366
  const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
2733
3367
  await handle.close();
@@ -3153,12 +3787,12 @@ async function analyzeProjectStructure(files, rootDir, registry = defaultRegistr
3153
3787
  }
3154
3788
 
3155
3789
  // src/scanner/markdown.ts
3156
- import * as fs5 from "fs/promises";
3157
- import * as path5 from "path";
3158
- import YAML2 from "yaml";
3790
+ import * as fs6 from "fs/promises";
3791
+ import * as path6 from "path";
3792
+ import YAML3 from "yaml";
3159
3793
  function parseMarkdownDocument(content, filePath, relativePath) {
3160
3794
  const lines = content.split("\n");
3161
- const baseName = path5.basename(relativePath, ".md").toLowerCase();
3795
+ const baseName = path6.basename(relativePath, ".md").toLowerCase();
3162
3796
  let frontmatter = {};
3163
3797
  const headings = [];
3164
3798
  const alerts = [];
@@ -3187,7 +3821,7 @@ function parseMarkdownDocument(content, filePath, relativePath) {
3187
3821
  bodyStartIndex = i + 1;
3188
3822
  try {
3189
3823
  const yamlText = frontmatterLines.join("\n");
3190
- const parsed = YAML2.parse(yamlText);
3824
+ const parsed = YAML3.parse(yamlText);
3191
3825
  if (parsed && typeof parsed === "object") {
3192
3826
  frontmatter = parsed;
3193
3827
  }
@@ -3575,8 +4209,8 @@ var BUILTIN_MARKDOWN_EXTRACTORS = [
3575
4209
  if (!target.startsWith("http://") && !target.startsWith("https://") && !target.startsWith("mailto:") && (target.startsWith(".") || target.includes("/") || /\.(md|ts|js|json|ya?ml|toml|rs|go|py)$/i.test(target))) {
3576
4210
  const rawTarget = target.split("#")[0].split("?")[0].trim();
3577
4211
  if (rawTarget) {
3578
- const dir = path5.dirname(doc.relativePath);
3579
- const resolved = dir === "." || path5.isAbsolute(rawTarget) ? rawTarget.replace(/^\.\//, "") : path5.normalize(path5.join(dir, rawTarget));
4212
+ const dir = path6.dirname(doc.relativePath);
4213
+ const resolved = dir === "." || path6.isAbsolute(rawTarget) ? rawTarget.replace(/^\.\//, "") : path6.normalize(path6.join(dir, rawTarget));
3580
4214
  relations.push({
3581
4215
  from: doc.relativePath,
3582
4216
  to: resolved,
@@ -3629,7 +4263,7 @@ for (const ext of BUILTIN_MARKDOWN_EXTRACTORS) {
3629
4263
  async function parseMarkdownFile(filePath, relativePath, registry = defaultMarkdownRegistry) {
3630
4264
  let content = "";
3631
4265
  try {
3632
- content = await fs5.readFile(filePath, "utf-8");
4266
+ content = await fs6.readFile(filePath, "utf-8");
3633
4267
  } catch {
3634
4268
  return { observations: [], relations: [] };
3635
4269
  }
@@ -3638,11 +4272,11 @@ async function parseMarkdownFile(filePath, relativePath, registry = defaultMarkd
3638
4272
  }
3639
4273
 
3640
4274
  // src/scanner/git.ts
3641
- import { execFile } from "child_process";
3642
- import { promisify } from "util";
3643
- var execFileAsync = promisify(execFile);
4275
+ import { execFile as execFile2 } from "child_process";
4276
+ import { promisify as promisify2 } from "util";
4277
+ var execFileAsync2 = promisify2(execFile2);
3644
4278
  async function runGit(args, cwd) {
3645
- const { stdout } = await execFileAsync("git", args, {
4279
+ const { stdout } = await execFileAsync2("git", args, {
3646
4280
  cwd,
3647
4281
  maxBuffer: 10 * 1024 * 1024
3648
4282
  });
@@ -3836,9 +4470,9 @@ async function scanGitHistory(cwd, options = {}) {
3836
4470
  }
3837
4471
 
3838
4472
  // src/scanner/source.ts
3839
- import * as fs6 from "fs/promises";
4473
+ import * as fs7 from "fs/promises";
3840
4474
  import * as fsSync from "fs";
3841
- import * as path6 from "path";
4475
+ import * as path7 from "path";
3842
4476
  import { createRequire } from "module";
3843
4477
  import Parser from "web-tree-sitter";
3844
4478
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
@@ -3892,7 +4526,7 @@ function getWasmDir() {
3892
4526
  try {
3893
4527
  const req = createRequire(import.meta.url);
3894
4528
  const pkgJson = req.resolve("tree-sitter-wasms/package.json");
3895
- const outDir = path6.join(path6.dirname(pkgJson), "out");
4529
+ const outDir = path7.join(path7.dirname(pkgJson), "out");
3896
4530
  if (fsSync.existsSync(outDir)) {
3897
4531
  wasmBaseDir = outDir;
3898
4532
  return wasmBaseDir;
@@ -3900,8 +4534,8 @@ function getWasmDir() {
3900
4534
  } catch {
3901
4535
  }
3902
4536
  const candidateDirs = [
3903
- path6.resolve(process.cwd(), "node_modules/tree-sitter-wasms/out"),
3904
- path6.resolve(process.cwd(), "../node_modules/tree-sitter-wasms/out")
4537
+ path7.resolve(process.cwd(), "node_modules/tree-sitter-wasms/out"),
4538
+ path7.resolve(process.cwd(), "../node_modules/tree-sitter-wasms/out")
3905
4539
  ];
3906
4540
  for (const dir of candidateDirs) {
3907
4541
  if (fsSync.existsSync(dir)) {
@@ -3914,7 +4548,7 @@ function getWasmDir() {
3914
4548
  async function getParserForWasm(wasmName) {
3915
4549
  const wasmDir = getWasmDir();
3916
4550
  if (!wasmDir) return null;
3917
- const wasmPath = path6.join(wasmDir, wasmName);
4551
+ const wasmPath = path7.join(wasmDir, wasmName);
3918
4552
  if (!fsSync.existsSync(wasmPath)) return null;
3919
4553
  try {
3920
4554
  if (!parserInitialized) {
@@ -4273,7 +4907,7 @@ async function analyzeSourceFile(filePath, relativePath) {
4273
4907
  const relations = [];
4274
4908
  let content = "";
4275
4909
  try {
4276
- content = await fs6.readFile(filePath, "utf-8");
4910
+ content = await fs7.readFile(filePath, "utf-8");
4277
4911
  } catch {
4278
4912
  return { observations, relations };
4279
4913
  }
@@ -4281,8 +4915,8 @@ async function analyzeSourceFile(filePath, relativePath) {
4281
4915
  return { observations, relations };
4282
4916
  }
4283
4917
  const now = (/* @__PURE__ */ new Date()).toISOString();
4284
- const baseName = path6.basename(relativePath);
4285
- const ext = path6.extname(relativePath).toLowerCase();
4918
+ const baseName = path7.basename(relativePath);
4919
+ const ext = path7.extname(relativePath).toLowerCase();
4286
4920
  const wasmName = EXT_TO_WASM[ext];
4287
4921
  if (wasmName) {
4288
4922
  try {
@@ -4480,9 +5114,9 @@ async function scanRepository(rootDir = process.cwd(), options = {}) {
4480
5114
  const plannedActions = [];
4481
5115
  const config = dryRun ? await storage.isInitialized() ? await storage.getConfig() : {
4482
5116
  schemaVersion: 1,
4483
- projectName: path7.basename(rootDir),
5117
+ projectName: path8.basename(rootDir),
4484
5118
  defaultScope: "project",
4485
- dataDir: path7.join(rootDir, ".bilmem")
5119
+ dataDir: path8.join(rootDir, ".bilmem")
4486
5120
  } : await storage.init();
4487
5121
  const incremental = options.incremental ?? false;
4488
5122
  const lastScanCommit = incremental ? config.lastScanCommit : void 0;
@@ -4555,7 +5189,7 @@ async function scanRepository(rootDir = process.cwd(), options = {}) {
4555
5189
  ];
4556
5190
  for (const o of mdObs) {
4557
5191
  if (o.source.includes(":adr") || o.claim.startsWith("[WARNING]") || o.claim.startsWith("[CAUTION]")) {
4558
- const id = `decision.${path7.basename(o.provenance.path || "doc", ".md")}.${o.provenance.line || "adr"}`;
5192
+ const id = `decision.${path8.basename(o.provenance.path || "doc", ".md")}.${o.provenance.line || "adr"}`;
4559
5193
  candidateLessons.push({
4560
5194
  id,
4561
5195
  type: "decision",
@@ -4789,8 +5423,34 @@ async function computeStats(storage, events, options) {
4789
5423
  }
4790
5424
  };
4791
5425
  const actions = {};
5426
+ const preflight = {
5427
+ runs: 0,
5428
+ ready: 0,
5429
+ needsClarification: 0,
5430
+ needsRevalidation: 0,
5431
+ conflicted: 0,
5432
+ insufficientKnowledge: 0,
5433
+ blockingUnknownsFound: 0,
5434
+ staleKnowledgeFound: 0,
5435
+ pastFailuresSurfaced: 0,
5436
+ questionsSuggested: 0,
5437
+ repeatedChecksAvoided: 0
5438
+ };
4792
5439
  for (const e of events) {
4793
5440
  switch (e.type) {
5441
+ case "preflight":
5442
+ preflight.runs++;
5443
+ if (e.readiness === "READY") preflight.ready++;
5444
+ else if (e.readiness === "NEEDS_CLARIFICATION") preflight.needsClarification++;
5445
+ else if (e.readiness === "NEEDS_REVALIDATION") preflight.needsRevalidation++;
5446
+ else if (e.readiness === "CONFLICTED") preflight.conflicted++;
5447
+ else if (e.readiness === "INSUFFICIENT_KNOWLEDGE") preflight.insufficientKnowledge++;
5448
+ preflight.blockingUnknownsFound += e.blockingUnknownsCount || 0;
5449
+ preflight.staleKnowledgeFound += e.staleKnowledgeCount || 0;
5450
+ preflight.pastFailuresSurfaced += e.pastFailuresCount || 0;
5451
+ preflight.questionsSuggested += e.questionsCount || 0;
5452
+ preflight.repeatedChecksAvoided += e.repeatedChecksAvoidedCount || 0;
5453
+ break;
4794
5454
  case "knowledge_created":
4795
5455
  learning.new++;
4796
5456
  break;
@@ -4961,7 +5621,8 @@ async function computeStats(storage, events, options) {
4961
5621
  questionResolutionRate,
4962
5622
  jevRejectionRate
4963
5623
  },
4964
- health
5624
+ health,
5625
+ preflight: preflight.runs > 0 ? preflight : void 0
4965
5626
  };
4966
5627
  }
4967
5628
  async function computeKnowledgeStats(storage, knowledgeId, events) {
@@ -5030,9 +5691,9 @@ async function computeKnowledgeStats(storage, knowledgeId, events) {
5030
5691
  }
5031
5692
 
5032
5693
  // src/stats/filter.ts
5033
- import { execFile as execFile2 } from "child_process";
5034
- import { promisify as promisify2 } from "util";
5035
- var execFileAsync2 = promisify2(execFile2);
5694
+ import { execFile as execFile3 } from "child_process";
5695
+ import { promisify as promisify3 } from "util";
5696
+ var execFileAsync3 = promisify3(execFile3);
5036
5697
  async function resolveSinceTimestamp(since, projectDir = process.cwd()) {
5037
5698
  const trimmed = since.trim();
5038
5699
  const durationMatch = trimmed.match(/^(\d+)\s*([smhdw])$/i);
@@ -5064,7 +5725,7 @@ async function resolveSinceTimestamp(since, projectDir = process.cwd()) {
5064
5725
  return parsedDate;
5065
5726
  }
5066
5727
  try {
5067
- const { stdout } = await execFileAsync2(
5728
+ const { stdout } = await execFileAsync3(
5068
5729
  "git",
5069
5730
  ["log", "-1", "--format=%ct", trimmed],
5070
5731
  { cwd: projectDir }
@@ -5108,17 +5769,56 @@ async function runStats(options = {}) {
5108
5769
 
5109
5770
  // src/mcp/server.ts
5110
5771
  function createBilmemMcpApp(options = {}) {
5111
- const rootDir = options.rootDir ? path8.resolve(options.rootDir) : process.cwd();
5112
- const storage = new Storage(rootDir);
5113
- const index = new SearchIndex();
5114
- let initialized = false;
5115
- async function ensureInitialized() {
5116
- if (!initialized) {
5117
- await storage.init();
5118
- const all = await storage.getAllKnowledge();
5119
- await index.buildIndex(all);
5120
- initialized = true;
5772
+ const contexts = /* @__PURE__ */ new Map();
5773
+ let lastActiveDir = options.rootDir ? path9.resolve(options.rootDir) : null;
5774
+ function isSystemRoot(dir) {
5775
+ const resolved = path9.resolve(dir);
5776
+ return resolved === "/" || path9.parse(resolved).root === resolved;
5777
+ }
5778
+ function resolveTargetDir(requestedPath) {
5779
+ if (requestedPath && requestedPath.trim()) {
5780
+ const resolved = path9.resolve(requestedPath.trim());
5781
+ lastActiveDir = resolved;
5782
+ return resolved;
5783
+ }
5784
+ if (lastActiveDir) {
5785
+ return lastActiveDir;
5786
+ }
5787
+ if (process.env.BILMEM_PROJECT_DIR) {
5788
+ const envDir = path9.resolve(process.env.BILMEM_PROJECT_DIR);
5789
+ lastActiveDir = envDir;
5790
+ return envDir;
5791
+ }
5792
+ const cwd = process.cwd();
5793
+ if (cwd && !isSystemRoot(cwd)) {
5794
+ lastActiveDir = cwd;
5795
+ return cwd;
5796
+ }
5797
+ throw new Error(
5798
+ `No project directory specified and the MCP server was started in system root ('/'). Please provide the 'path' parameter in your tool call (e.g. { path: "/path/to/project", ... }).`
5799
+ );
5800
+ }
5801
+ async function getProjectContext(requestedPath) {
5802
+ const targetDir = resolveTargetDir(requestedPath);
5803
+ let ctx = contexts.get(targetDir);
5804
+ if (!ctx) {
5805
+ const storage = new Storage(targetDir);
5806
+ const index = new SearchIndex();
5807
+ ctx = {
5808
+ rootDir: targetDir,
5809
+ storage,
5810
+ index,
5811
+ initialized: false
5812
+ };
5813
+ contexts.set(targetDir, ctx);
5814
+ }
5815
+ if (!ctx.initialized) {
5816
+ await ctx.storage.init();
5817
+ const all = await ctx.storage.getAllKnowledge();
5818
+ await ctx.index.buildIndex(all);
5819
+ ctx.initialized = true;
5121
5820
  }
5821
+ return ctx;
5122
5822
  }
5123
5823
  const app = new McpApp({
5124
5824
  name: options.name ?? "bilmem",
@@ -5128,6 +5828,7 @@ function createBilmemMcpApp(options = {}) {
5128
5828
  name: "bilmem_resolve",
5129
5829
  description: "Determine if enough is known to answer or act, what contextually does not apply, what is missing, and the cheapest next step to resolve uncertainty.",
5130
5830
  inputSchema: {
5831
+ path: z.string().optional().describe("Project root directory (defaults to active workspace or current working directory)"),
5131
5832
  question: z.string().describe("The question, goal, or uncertainty to resolve"),
5132
5833
  positive: z.array(z.string()).optional().describe("Applicable context cues (e.g. server-session, node, postgres)"),
5133
5834
  negative: z.array(z.string()).optional().describe("Inapplicable context cues (e.g. stateless-jwt, browser, sqlite)"),
@@ -5135,8 +5836,8 @@ function createBilmemMcpApp(options = {}) {
5135
5836
  maxDepth: z.number().optional().describe("Max candidates to inspect (default: 5)")
5136
5837
  },
5137
5838
  handler: async (args) => {
5138
- await ensureInitialized();
5139
- const retriever = new Retriever(index, storage);
5839
+ const ctx = await getProjectContext(args.path);
5840
+ const retriever = new Retriever(ctx.index, ctx.storage);
5140
5841
  const res = await resolveEpistemicQuery(
5141
5842
  {
5142
5843
  question: args.question,
@@ -5145,7 +5846,7 @@ function createBilmemMcpApp(options = {}) {
5145
5846
  scope: args.scope,
5146
5847
  maxDepth: args.maxDepth
5147
5848
  },
5148
- storage,
5849
+ ctx.storage,
5149
5850
  retriever
5150
5851
  );
5151
5852
  return {
@@ -5162,10 +5863,53 @@ function createBilmemMcpApp(options = {}) {
5162
5863
  };
5163
5864
  }
5164
5865
  });
5866
+ app.tool({
5867
+ name: "bilmem_preflight",
5868
+ description: "Determine what must be known before beginning a proposed goal (surfaces constraints, relevant lessons, past failures, stale knowledge, and blocking unknowns).",
5869
+ inputSchema: {
5870
+ path: z.string().optional().describe("Project root directory (defaults to active workspace or current working directory)"),
5871
+ goal: z.string().describe("The proposed goal, feature, or work to analyze before implementation"),
5872
+ positive: z.array(z.string()).optional().describe("Applicable context cues (e.g. authentication, postgres)"),
5873
+ negative: z.array(z.string()).optional().describe("Inapplicable context cues (e.g. browser, sqlite)"),
5874
+ deep: z.boolean().optional().describe("Check targeted git diff / unindexed files for staleness")
5875
+ },
5876
+ handler: async (args) => {
5877
+ const ctx = await getProjectContext(args.path);
5878
+ const retriever = new Retriever(ctx.index, ctx.storage);
5879
+ const res = await executePreflight(
5880
+ {
5881
+ goal: args.goal,
5882
+ positive: args.positive,
5883
+ negative: args.negative,
5884
+ deep: args.deep
5885
+ },
5886
+ ctx.storage,
5887
+ retriever,
5888
+ ctx.index
5889
+ );
5890
+ return {
5891
+ goal: res.goal,
5892
+ readiness: res.readiness,
5893
+ known: res.known.slice(0, 5).map((k) => k.statement),
5894
+ constraints: res.constraints.map((c) => c.statement),
5895
+ lessons: res.lessons.slice(0, 5).map((l) => l.statement),
5896
+ pastFailures: res.past_failures.slice(0, 5).map((f) => f.statement),
5897
+ blockingUnknowns: res.unknowns.filter((u) => u.blocking).map((u) => u.question || u.statement || u.id),
5898
+ stale: res.stale.map((s) => s.id),
5899
+ questions: res.questions.slice(0, 5).map((q) => q.question),
5900
+ next: res.next.slice(0, 3).map((n) => ({
5901
+ type: n.type,
5902
+ target: n.target,
5903
+ reason: n.reason
5904
+ }))
5905
+ };
5906
+ }
5907
+ });
5165
5908
  app.tool({
5166
5909
  name: "bilmem_lookup",
5167
5910
  description: "Lookup reusable knowledge, conventions, and lessons using JEV contextual reranking. Returns compact, highly applicable knowledge units.",
5168
5911
  inputSchema: {
5912
+ path: z.string().optional().describe("Project root directory (defaults to active workspace or current working directory)"),
5169
5913
  query: z.string().optional().describe("Query text, intent keywords, or problem description"),
5170
5914
  positive: z.array(z.string()).optional().describe("Contextual positive cues (e.g. node, esm, server-session)"),
5171
5915
  negative: z.array(z.string()).optional().describe("Contextual negative cues (e.g. commonjs, browser)"),
@@ -5173,8 +5917,8 @@ function createBilmemMcpApp(options = {}) {
5173
5917
  limit: z.number().optional().describe("Maximum number of results to return (default 5)")
5174
5918
  },
5175
5919
  handler: async (args) => {
5176
- await ensureInitialized();
5177
- const retriever = new Retriever(index, storage);
5920
+ const ctx = await getProjectContext(args.path);
5921
+ const retriever = new Retriever(ctx.index, ctx.storage);
5178
5922
  const results = await retriever.lookup({
5179
5923
  query: args.query,
5180
5924
  positive: args.positive,
@@ -5201,6 +5945,7 @@ function createBilmemMcpApp(options = {}) {
5201
5945
  name: "bilmem_observe",
5202
5946
  description: "Record a new observation from code, git, test execution, or environment. Preserves provenance.",
5203
5947
  inputSchema: {
5948
+ path: z.string().optional().describe("Project root directory (defaults to active workspace or current working directory)"),
5204
5949
  claim: z.string().describe("What was observed"),
5205
5950
  source: z.string().describe("Source identifier (e.g. file:src/auth.ts#L42, git:commit:81ac23, user:correction)"),
5206
5951
  confidence: z.number().optional().describe("Confidence 0.0 - 1.0 (default: 0.85)"),
@@ -5209,7 +5954,7 @@ function createBilmemMcpApp(options = {}) {
5209
5954
  details: z.record(z.any()).optional().describe("Optional structured metadata")
5210
5955
  },
5211
5956
  handler: async (args) => {
5212
- await ensureInitialized();
5957
+ const ctx = await getProjectContext(args.path);
5213
5958
  const obs = createObservation({
5214
5959
  source: args.source,
5215
5960
  claim: args.claim,
@@ -5223,10 +5968,10 @@ function createBilmemMcpApp(options = {}) {
5223
5968
  negative: args.negative,
5224
5969
  details: args.details
5225
5970
  });
5226
- await storage.saveObservation(obs);
5971
+ await ctx.storage.saveObservation(obs);
5227
5972
  const evidence = createEvidence(obs, "supports");
5228
- await storage.saveEvidence(evidence);
5229
- const existing = await storage.getAllKnowledge();
5973
+ await ctx.storage.saveEvidence(evidence);
5974
+ const existing = await ctx.storage.getAllKnowledge();
5230
5975
  const rec = reconcileKnowledge(
5231
5976
  {
5232
5977
  statement: obs.claim,
@@ -5247,10 +5992,10 @@ function createBilmemMcpApp(options = {}) {
5247
5992
  evidence: [evidence.id, obs.source]
5248
5993
  },
5249
5994
  existing,
5250
- storage
5995
+ ctx.storage
5251
5996
  );
5252
- await storage.saveKnowledge(updatedKnowledge);
5253
- await index.upsert(updatedKnowledge);
5997
+ await ctx.storage.saveKnowledge(updatedKnowledge);
5998
+ await ctx.index.upsert(updatedKnowledge);
5254
5999
  return {
5255
6000
  observationId: obs.id,
5256
6001
  evidenceId: evidence.id,
@@ -5264,6 +6009,7 @@ function createBilmemMcpApp(options = {}) {
5264
6009
  name: "bilmem_learn",
5265
6010
  description: "Learn from a completed task trajectory or explicit user correction. Converts experiences into refined, reusable lessons.",
5266
6011
  inputSchema: {
6012
+ path: z.string().optional().describe("Project root directory (defaults to active workspace or current working directory)"),
5267
6013
  goal: z.string().describe("The task or goal that was pursued"),
5268
6014
  taskId: z.string().optional().describe("Task identifier"),
5269
6015
  outcome: z.enum(["success", "failure", "reverted", "neutral"]).describe("Outcome of the task"),
@@ -5279,16 +6025,16 @@ function createBilmemMcpApp(options = {}) {
5279
6025
  }).optional().describe("Explicit user correction, if any occurred")
5280
6026
  },
5281
6027
  handler: async (args) => {
5282
- await ensureInitialized();
6028
+ const ctx = await getProjectContext(args.path);
5283
6029
  if (args.userCorrection) {
5284
6030
  const { observation, knowledge } = processUserCorrection(args.userCorrection);
5285
- await storage.saveObservation(observation);
5286
- await storage.saveEvidence(createEvidence(observation, "supports"));
5287
- const existing = await storage.getAllKnowledge();
6031
+ await ctx.storage.saveObservation(observation);
6032
+ await ctx.storage.saveEvidence(createEvidence(observation, "supports"));
6033
+ const existing = await ctx.storage.getAllKnowledge();
5288
6034
  const rec = reconcileKnowledge(knowledge, existing);
5289
- const { updatedKnowledge } = applyReconciliation(rec, knowledge, existing, storage);
5290
- await storage.saveKnowledge(updatedKnowledge);
5291
- await index.upsert(updatedKnowledge);
6035
+ const { updatedKnowledge } = applyReconciliation(rec, knowledge, existing, ctx.storage);
6036
+ await ctx.storage.saveKnowledge(updatedKnowledge);
6037
+ await ctx.index.upsert(updatedKnowledge);
5292
6038
  return {
5293
6039
  type: "user_correction",
5294
6040
  knowledgeId: updatedKnowledge.id,
@@ -5304,20 +6050,20 @@ function createBilmemMcpApp(options = {}) {
5304
6050
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5305
6051
  finalOutcome: args.outcome
5306
6052
  };
5307
- const existing = await storage.getAllKnowledge();
6053
+ const existing = await ctx.storage.getAllKnowledge();
5308
6054
  const learningResult = learnFromTrajectory(trajectory, existing);
5309
6055
  for (const obs of learningResult.observations) {
5310
- await storage.saveObservation(obs);
6056
+ await ctx.storage.saveObservation(obs);
5311
6057
  }
5312
6058
  for (const ev of learningResult.evidence) {
5313
- await storage.saveEvidence(ev);
6059
+ await ctx.storage.saveEvidence(ev);
5314
6060
  }
5315
6061
  let updatedKnowledge;
5316
6062
  let action = "NEW";
5317
6063
  for (const item of learningResult.reconciliations) {
5318
6064
  if (item.appliedKnowledge) {
5319
- await storage.saveKnowledge(item.appliedKnowledge);
5320
- await index.upsert(item.appliedKnowledge);
6065
+ await ctx.storage.saveKnowledge(item.appliedKnowledge);
6066
+ await ctx.index.upsert(item.appliedKnowledge);
5321
6067
  updatedKnowledge = item.appliedKnowledge;
5322
6068
  action = item.action;
5323
6069
  }
@@ -5329,9 +6075,9 @@ function createBilmemMcpApp(options = {}) {
5329
6075
  result: args.outcome,
5330
6076
  context: { positive: updatedKnowledge.positive, negative: updatedKnowledge.negative }
5331
6077
  });
5332
- await storage.appendOutcome(out);
6078
+ await ctx.storage.appendOutcome(out);
5333
6079
  applyOutcomeToKnowledge(updatedKnowledge, out);
5334
- await storage.saveKnowledge(updatedKnowledge);
6080
+ await ctx.storage.saveKnowledge(updatedKnowledge);
5335
6081
  return {
5336
6082
  type: "trajectory_lesson",
5337
6083
  action,
@@ -5353,12 +6099,13 @@ function createBilmemMcpApp(options = {}) {
5353
6099
  name: "bilmem_explain",
5354
6100
  description: "Explain why a knowledge ID exists: statement, evidence provenance, confidence, support/contradiction history, and applicable context.",
5355
6101
  inputSchema: {
6102
+ path: z.string().optional().describe("Project root directory (defaults to active workspace or current working directory)"),
5356
6103
  id: z.string().describe("The knowledge ID to explain"),
5357
6104
  history: z.boolean().optional().describe("Whether to format lifecycle transition history (--history)")
5358
6105
  },
5359
6106
  handler: async (args) => {
5360
- await ensureInitialized();
5361
- const k = await storage.getKnowledge(args.id);
6107
+ const ctx = await getProjectContext(args.path);
6108
+ const k = await ctx.storage.getKnowledge(args.id);
5362
6109
  if (!k) {
5363
6110
  return { error: `Knowledge not found: "${args.id}"` };
5364
6111
  }
@@ -5392,6 +6139,7 @@ function createBilmemMcpApp(options = {}) {
5392
6139
  name: "bilmem_unknown",
5393
6140
  description: "Query or record first-class unknowns (NOT_OBSERVED, INSUFFICIENT_EVIDENCE, CONFLICTING_EVIDENCE, UNRESOLVABLE_LOCALLY) to prevent repeating useless investigations.",
5394
6141
  inputSchema: {
6142
+ path: z.string().optional().describe("Project root directory (defaults to active workspace or current working directory)"),
5395
6143
  action: z.enum(["query", "check", "record", "resolve"]).describe("Action to perform"),
5396
6144
  id: z.string().optional().describe("Unknown ID or query subject"),
5397
6145
  reason: z.enum(["NOT_OBSERVED", "INSUFFICIENT_EVIDENCE", "CONFLICTING_EVIDENCE", "STALE", "UNRESOLVABLE_LOCALLY"]).optional(),
@@ -5400,16 +6148,16 @@ function createBilmemMcpApp(options = {}) {
5400
6148
  question: z.string().optional().describe("Clarifying question for the user or next agent")
5401
6149
  },
5402
6150
  handler: async (args) => {
5403
- await ensureInitialized();
6151
+ const ctx = await getProjectContext(args.path);
5404
6152
  if (args.action === "query") {
5405
- const list = await storage.getAllUnknowns();
6153
+ const list = await ctx.storage.getAllUnknowns();
5406
6154
  return {
5407
6155
  count: list.length,
5408
6156
  unknowns: list
5409
6157
  };
5410
6158
  }
5411
6159
  if (args.action === "check") {
5412
- const list = await storage.getAllUnknowns();
6160
+ const list = await ctx.storage.getAllUnknowns();
5413
6161
  const checkResult = hasBeenInvestigated(list, args.id || "");
5414
6162
  return checkResult;
5415
6163
  }
@@ -5423,12 +6171,12 @@ function createBilmemMcpApp(options = {}) {
5423
6171
  checked: args.checked ?? [],
5424
6172
  next: args.question ? { type: "ask", question: args.question } : void 0
5425
6173
  });
5426
- await storage.saveUnknown(record);
6174
+ await ctx.storage.saveUnknown(record);
5427
6175
  return { success: true, recorded: record };
5428
6176
  }
5429
6177
  if (args.action === "resolve") {
5430
6178
  if (!args.id) throw new Error("ID is required to resolve an unknown");
5431
- const deleted = await storage.deleteUnknown(args.id);
6179
+ const deleted = await ctx.storage.deleteUnknown(args.id);
5432
6180
  return { success: deleted, resolvedId: args.id };
5433
6181
  }
5434
6182
  return { error: "Unknown action" };
@@ -5438,16 +6186,16 @@ function createBilmemMcpApp(options = {}) {
5438
6186
  name: "bilmem_scan",
5439
6187
  description: "Trigger a language-independent scan of the repository filesystem, markdown documentation, and git history.",
5440
6188
  inputSchema: {
5441
- path: z.string().optional().describe("Root directory to scan (defaults to project root)"),
6189
+ path: z.string().optional().describe("Project root directory to scan (defaults to active workspace or current working directory)"),
5442
6190
  incremental: z.boolean().optional().describe("Whether to scan incrementally from last scanned git commit")
5443
6191
  },
5444
6192
  handler: async (args) => {
5445
- const targetDir = args.path ? path8.resolve(args.path) : rootDir;
5446
- const report = await scanRepository(targetDir, {
6193
+ const ctx = await getProjectContext(args.path);
6194
+ const report = await scanRepository(ctx.rootDir, {
5447
6195
  incremental: args.incremental ?? false
5448
6196
  });
5449
- const all = await storage.getAllKnowledge();
5450
- await index.buildIndex(all);
6197
+ const all = await ctx.storage.getAllKnowledge();
6198
+ await ctx.index.buildIndex(all);
5451
6199
  return report;
5452
6200
  }
5453
6201
  });
@@ -5455,13 +6203,14 @@ function createBilmemMcpApp(options = {}) {
5455
6203
  name: "bilmem_stats",
5456
6204
  description: "Inspect local-first statistics, knowledge health, and effectiveness metrics (measuring reuse, investigation avoidance, and JEV accuracy). Read-only.",
5457
6205
  inputSchema: {
6206
+ path: z.string().optional().describe("Project root directory (defaults to active workspace or current working directory)"),
5458
6207
  since: z.string().optional().describe("Time window (e.g. 24h, 7d, 30d) or Git revision (e.g. HEAD~20, commit-sha)"),
5459
6208
  knowledgeId: z.string().optional().describe("Specific knowledge ID to inspect single-item lifecycle and diagnostics")
5460
6209
  },
5461
6210
  handler: async (args) => {
5462
- await ensureInitialized();
6211
+ const ctx = await getProjectContext(args.path);
5463
6212
  const res = await runStats({
5464
- rootDir,
6213
+ rootDir: ctx.rootDir,
5465
6214
  since: args.since,
5466
6215
  knowledgeId: args.knowledgeId
5467
6216
  });